Own nothing
Comments are ordinary ATProto replies. No proprietary comment database, no export story, no lock-in.
Turn any ATProto thread into a native comment section. Readers reply with their own accounts, posts stay in their own repos, and your site stays refreshingly database-free.
MIT licensed · framework-agnostic · hosted backend included
<!-- no install, straight from jsDelivr -->
<script type="module"
src="https://cdn.jsdelivr.net/npm/@svebcomponents/atproto.comments">
</script>
<!-- one component -->
<atproto-comments
thread="https://bsky.app/…"
></atproto-comments> Comments are ordinary ATProto replies. No proprietary comment database, no export story, no lock-in.
Shadow DOM keeps it reliable; CSS variables and parts let it settle into the typography of your site.
Live signals replace polling. A new reply triggers one focused AppView refresh, with coalescing and retry built in.
Pick your depth
Every tier below talks to the same free hosted backend. Self-hosting it is a separate switch — flip it from any tier, whenever you want to.
Two tags, no build step — paste them into any HTML page or CMS. See the snippet above.
pnpm add the package and import it into your own
bundle. See the snippet ↓
Hydrate in place instead of flashing a loading skeleton. SvelteKit is supported today — Nuxt, Astro, Next.js, and SolidStart are planned. Read the SSR guide ↗
Run OAuth, posting, and live events on your own infrastructure instead of the hosted default. See self-hosting ↓
Quickstart
The custom element works with Svelte, Astro, Eleventy, React, plain HTML, or anything else that can load an ES module.
pnpm add @svebcomponents/atproto.comments
import "@svebcomponents/atproto.comments";<atproto-comments
thread="https://bsky.app/profile/bsky.app/post/…"
></atproto-comments>The real component
This thread is server-rendered below, hydrated in place, and subscribed to the same hosted event bridge your site gets by default.
but does it work?
Try another post with ?thread=<AT URI or bsky.app URL>.
Designed for the public web
Reads go straight to a public AppView. The backend only coordinates OAuth, publishes replies, and tells the component when the public snapshot may have changed.
One upstream. Threads are multiplexed per process.
No polling. Refreshes happen on connection or reply.
No replay dependency. Reconnect forces a fresh read.
Component reference
Defaults are intentionally complete. Most sites only set thread; everything else exists for policy, presentation, or
infrastructure choices.
| Property | Type | Default | Purpose |
|---|---|---|---|
thread | string | — | An AT URI or bsky.app post URL. |
service | string | hosted | One backend URL for OAuth, posting, and live events. Set /atproto to self-host. |
readonly | boolean | false | Hides in-page sign-in and posting. Live updates remain enabled. |
sort | oldest | newest | likes | oldest | How comments are ordered at each level. |
max-depth | number | 6 | Maximum nested reply depth rendered inline. |
labels | hide | collapse | show | collapse | Treatment for moderation-labelled posts. |
viewer | URL | bsky.app | Viewer used for profile and post links. |
appview | URL | public.api.bsky.app | Public AppView used to fetch thread snapshots. |
Call element.revalidate() whenever your application knows more
than the live stream does. Concurrent calls are deduplicated.
atproto-comments:loadedThe first client-side snapshot loaded.
atproto-comments:revalidatedA connected 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.
One backend switch
The service property is the only backend setting in the component.
Change it once and OAuth, posting, and live SSE all move together.
A process opens one filtered Spacedust connection only while someone is watching. It updates the subject set as threads come and go, applies jittered reconnect backoff, and enforces bounded viewer and thread capacity.
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 ↗Comments should belong to the conversation
Theo-Steiner @theosteiner.de
I did a breakthrough 👀