troposphere is the AT Protocol arm of Coop, ROOST's
open-source trust & safety platform. It's what lets Coop stand in for Ozone on atproto: it
ingests AT Protocol content into Coop, signs and serves moderation labels (the standard
subscribeLabels firehose and queryLabels endpoint), routes user reports into Coop's review
queue, and enforces takedowns on your PDS.
Coop is the moderation tool where you set up your own policies, rules, and actions and where your reviewers work; troposphere is the atproto plumbing Coop uses. It works for any AT Protocol project regardless of its lexicons, PDS, or AppView.
Built on the maintained @atproto/* primitives (@atproto/crypto, @atproto/xrpc-server,
@atproto/common). The label and enforcement endpoints are plain authenticated HTTP, so
non-Coop tooling can use them too, but Coop is what troposphere is designed for.
About this project. troposphere and its documentation were generated by Claude Code, Anthropic's agentic coding tool. Spot something wrong or unclear? Reach out to @julietshen.online on Bluesky, or open a pull request.
- Ingests AT Protocol content (Jetstream) into Coop's item intake (
npm run ingest). - Signs labels with a secp256k1 key (canonical dag-cbor, verifiable against the labeler DID).
- Serves
com.atproto.label.queryLabels(public HTTP). - Serves
com.atproto.label.subscribeLabels(WebSocket firehose) with cursor backfill and a live tail, so a consumer can catch up from any point and cut over to live with no gap. - Exposes
POST /admin/labels, a bearer-authenticated API to create and negate labels. - Accepts
com.atproto.moderation.createReportfrom any AT Protocol AppView (verifying the reporter's inter-service auth against their DID), resolves the reported subject to its content, and forwards the enriched report to Coop's review queue (Coop's/api/v1/reportenvelope withREPORT_FORWARD_FORMAT=coop, or raw JSON for other tooling). - Enforces takedowns and restores on your own PDS via
POST /admin/enforce, alongside labeling. - Accepts Coop's
CUSTOM_ACTIONwebhook directly atPOST /coop/action(label and/or take down), so a Coop action uses it with no adapter. - Persists labels and reports in Postgres, with a monotonic sequence for firehose cursors. Emitted labels are permanent on the network, so the store is durable by design.
Coop is the moderation tool (your policies and actions, review
queues, reviewer roles, event history), playing the role Ozone's UI would; troposphere is the
atproto plumbing it uses.
Requires Node 24+ and Postgres.
npm install
# 1. Generate a signing key. Prints LABELER_SIGNING_KEY and the publicKeyMultibase
# to publish in your DID document.
npm run keygen
# 2. Configure. Copy .env.example to .env and fill in LABELER_DID, LABELER_SIGNING_KEY,
# ADMIN_TOKEN, DATABASE_URL.
cp .env.example .env
# 3. Create the schema, then run.
npm run db:init
npm run dev # or: npm run build && npm start
# 4. Smoke-test a running instance (emits, queries, verifies signatures, tails the firehose).
LABELER_URL=http://localhost:4100 ADMIN_TOKEN=... SIGNING_DID_KEY=did:key:z... npm run smokeCoop (or other tooling) posts a subject and the label values to create and/or negate:
curl -X POST http://localhost:4100/admin/labels \
-H "authorization: Bearer $ADMIN_TOKEN" \
-H "content-type: application/json" \
-d '{
"subject": { "uri": "at://did:plc:.../app.bsky.feed.post/abc", "cid": "bafy..." },
"create": ["spam"]
}'To retract a label, negate it (emits a neg label with a later timestamp, per the spec):
-d '{ "subject": { "uri": "at://..." }, "negate": ["spam"] }'Account-level labels use a bare did: as the subject uri and omit cid.
The service signs labels, but the network only trusts them if the labeler's DID document advertises the matching key and endpoint. Both DID methods are supported:
did:web(recommended for self-hosting under your own domain, no dependency on the PLC directory). Host adid.jsonthat includes a verification method#atproto_labelwith thepublicKeyMultibasefromnpm run keygen, and a service#atproto_labelerof typeAtprotoLabelerwhose endpoint is this server's public URL.did:plc(portable, with PLC key recovery). Update the labeler account's PLC identity to add the same key and service entry.
Either way, publish an app.bsky.labeler.service declaration record so clients can discover
the labeler and the label values it uses.
- Multi-PDS enforcement: resolve the subject's PDS from its DID and select the matching admin credential, rather than a single configured PDS.
did:web/did:plcidentity provisioning helpers and declaration publishing.- Multi-tenant operation (one deployment serving several labeler identities).
- A helper to generate the labeler declaration from Coop's configured labels. (The Coop
integration itself is done:
/coop/actionfor labels/takedowns out,REPORT_FORWARD_FORMAT=coopfor reports in. Creating the actions and queues inside Coop is operator setup, not troposphere code.)
Licensed under the Apache License, Version 2.0. See LICENSE.