Open social comments

Your post.
Their voices.

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

article.html
<!-- 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>
Live updates connected
01

Own nothing

Comments are ordinary ATProto replies. No proprietary comment database, no export story, no lock-in.

02

Feel native

Shadow DOM keeps it reliable; CSS variables and parts let it settle into the typography of your site.

03

Stay current

Live signals replace polling. A new reply triggers one focused AppView refresh, with coalescing and retry built in.

Pick your depth

Start simple.
Go deeper when you need to.

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.

1

CDN drop-in

Two tags, no build step — paste them into any HTML page or CMS. See the snippet above.

3

Server-rendered

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 ↗

4

Self-hosted backend

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

Quickstart

Two snippets.
That’s the setup.

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>

The real component

Not a screenshot.

This thread is server-rendered below, hydrated in place, and subscribed to the same hosted event bridge your site gets by default.

Live ATProto thread Open original ↗

Try another post with ?thread=<AT URI or bsky.app URL>.

Designed for the public web

A signal, not another source of truth.

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.

Your page <atproto-comments> renders + refreshes
Hosted or yours Event bridge one SSE per viewer
Community infra Microcosm one filtered WebSocket

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

Small surface.
Useful escape hatches.

Defaults are intentionally complete. Most sites only set thread; everything else exists for policy, presentation, or infrastructure choices.

PropertyTypeDefaultPurpose
threadstringAn AT URI or bsky.app post URL.
servicestringhostedOne backend URL for OAuth, posting, and live events. Set /atproto to self-host.
readonlybooleanfalseHides in-page sign-in and posting. Live updates remain enabled.
sortoldest | newest | likesoldestHow comments are ordered at each level.
max-depthnumber6Maximum nested reply depth rendered inline.
labelshide | collapse | showcollapseTreatment for moderation-labelled posts.
viewerURLbsky.appViewer used for profile and post links.
appviewURLpublic.api.bsky.appPublic AppView used to fetch thread snapshots.

Imperative refresh

Call element.revalidate() whenever your application knows more than the live stream does. Concurrent calls are deduplicated.

DOM events

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

Hosted by default.
Yours when you want it.

The service property is the only backend setting in the component. Change it once and OAuth, posting, and live SSE all move together.

  • Cross-origin hosted mode uses a short-lived, origin-bound bridge JWT. ATProto OAuth tokens never leave the server.
  • Same-origin self-hosted mode uses an HttpOnly, SameSite cookie instead—no browser-readable session credential.
  • Both modes run the exact same open-source bridge and store no comment bodies.
Being gentle with Microcosm

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

Make the social web
feel like the web again.

Start with one thread