<atproto-comments>, a drop-in web component that renders an AT Protocol (Bluesky) thread as a comment section

Live ATProto thread
<script type="module"
  src="https://atproto.svebcomponents.dev/cdn">
</script>

<atproto-comments
  thread="https://bsky.app/profile/theosteiner.de/post/3mreni33v7k2c"
></atproto-comments>

<style>
  atproto-comments {
    --atproto-comments-accent: #2563eb;
    --atproto-comments-radius: 4px;
    font-family: ui-monospace, monospace;
  }
</style>
01

Privacy Respecting

By default, <atproto-comments> only sends requests to its hosted bridge if the user signs in and therefore explicitly opts into using the service. It stores only what it needs and is commited to respecting your privacy.

02

Customizable

The styles for <atproto-comments> are scoped by shadow DOM. CSS custom properties and parts adapt the component to the typography of the host page.

03

Event-driven updates

Once users sign in, they receive live updates via server sent events from the hosted bridge.
You can also self-host the bridge for advanced security (cookie based auth) and control every layer of the stack.

Setup options

Four levels
of control.

Each level unlocks more fine-grained control over how you distribute the comment component and where data flows from.

1

CDN script tag

Pasted into static HTML or a CMS. See the snippet above.

2

npm install

Add the package and import it once into your own bundle. Type definitions and a custom elements manifest ship with it. Install guide ↓

3

Server-rendered

The thread is rendered into declarative shadow DOM on the server and adopted in place on the client. SvelteKit, Vue, Astro & React are supported by svebcomponents as of today. See the SSR guide ↓

4

Self-hosted backend

Run OAuth, posting, and live events on your own infrastructure instead of the hosted service. See self-hosting ↓

Quickstart

Install from npm & go.

The custom element works with Svelte, Astro, Eleventy, React, plain HTML, or anything else that can load an ES module.

1

Install and import

pnpm add @svebcomponents/atproto.comments

import "@svebcomponents/atproto.comments";
2

Point at a post

<atproto-comments
  thread="https://bsky.app/profile/bsky.app/post/…"
></atproto-comments>

Server rendering

One recipe,
every host.

Server rendering works the same way everywhere: import the component's renderer entry (@svebcomponents/atproto.comments/ssr) once on the server, its browser entry on the client, and write <atproto-comments> like any other tag. A small adapter renders declarative shadow DOM during SSR — including the thread fetch — and the browser adopts it in place on hydration, so the first paint already shows comments.

Svelte

@svebcomponents/ssr

Add the Vite plugin before Svelte's and import the renderer entry in hooks.server.ts.

Setup guide ↗

React

@svebcomponents/ssr-react/rsc

Point jsxImportSource at the adapter, load both entries, use the tag as JSX.

Setup guide ↗

Vue

@svebcomponents/ssr-vue

Add the Vite plugin before Vue's and app.use() the adapter on both app instances.

Setup guide ↗

Astro

@svebcomponents/ssr-astro

Add the integration, import the renderer entry in frontmatter and the browser entry in a script.

Setup guide ↗

Architecture

Reads, writes,
and live updates.

Reads go straight from the browser to a public AppView. Writes run through the bridge's OAuth flow and land as ordinary posts in the commenter's own PDS repo. Live updates travel the other way: Spacedust signals the bridge, which fans out one SSE stream per viewer. The bridge coordinates all of this; it never stores or serves comment content.

Component reference

Properties
and events.

You probably only need to set thread. The remaining properties allow fine control over sorting, depth limits, moderation handling, and infrastructure choices.

PropertyTypeDefaultPurpose
threadstringAn AT URI, or a post URL from any viewer using the /profile/{did}/post/{record_id} scheme.
servicestringhostedBridge backend service OAuth, posting, and live events. Change when self-hosting.
readonlybooleanfalseHides in-page sign-in and posting.
livesigned-in | all | offsigned-inWho gets live updates? Affects when data is sent to the bridge, so please check the privacy policy before changing.
sortoldest | newest | likesoldestHow comments are ordered at each level.
max-depthnumber6Maximum nested reply depth rendered inline.
labelshide | collapse | showcollapseTreatment for moderation-labelled posts.
viewerURLbsky.appApp view used for profile and post links. Think bluesky, mu.social, etc.
viewer-namestringhostname of the viewer property“Reply on …”. Defaults to Bluesky for the default viewer.
appviewURLpublic.api.bsky.appPublic AppView used to fetch thread snapshots.
show-rootbooleanfalseRender the discussion's root node above the replies.
page-urlstring-The embedding page's canonical URL. Enables no-JavaScript sign-in on a same-origin, cookie-mode service.
fetched-atepoch ms | ISO date-When threadData was fetched. Set automatically by SSR prefetch; age older than stale-time triggers one background refresh.
stale-timenumber (ms)60000How long a preloaded snapshot is trusted before one client-side refresh from the public AppView. Infinity disables it.

Imperative refresh

Call element.revalidate() to request a refresh outside the normal event path. Concurrent calls are deduplicated.

DOM events

atproto-comments:loadedThe first client-side snapshot loaded.

atproto-comments:revalidatedAn interaction or comment event refreshed the snapshot.

atproto-comments:commentThe live service observed a new reply URI.

atproto-comments:live-statusThe upstream changed between connected and reconnecting.

atproto-comments:signed-inThe reader completed ATProto OAuth.

atproto-comments:postedThe reader published a reply.

atproto-comments:errorA load or background refresh failed.

Metrics from our Hosted Bridge

Right now

Threads being watched
0
Open event streams
0

Since 2026-08-23

Embedding sites
3
Sign-ins
15
Replies posted
4
Likes & reposts
14

This is almost everything the bridge will ever track. We are committed to your privacy.

Self-hosting

Use the default hosted bridge,
or run your own.

The service property selects the backend used for OAuth, posting, and the SSE stream. Set it to use your own deployment instead of the hosted bridge.

In cross-origin deployments, such as the hosted bridge, the component holds a short-lived, origin-bound bridge JWT.

A self-hosted bridge can be same-origin and therefore use a http only same-site cookie instead, bolstering security.
Both modes run the same open-source bridge code, which stores only transient auth data and basic, privacy respecting metrics.

Live Thread Data via Spacedust & Constellation

The bridge sends live thread updates via SSE to connected users, leveraging the incredible spacedust project. Showing readers which posts they have already liked or reposted is a backlink question, answered by its sibling constellation. Both are part of microcosm.

Component

<atproto-comments
  thread="at://did:plc:…/app.bsky.feed.post/…"
  service="/atproto"
></atproto-comments>

Server

createAtprotoCommentsService({
  publicUrl: "https://your.blog",
  basePath: "/atproto",
  sessionMode: "cookie",
  sessionSecret,
  keys,
  stateStore,
  sessionStore,
  serviceSessionStore,
});
Read the deployment guide ↗