Reference #
Configuration #
Set via environment (a .env file is loaded in development).
| Variable | Required | Default | Meaning |
|---|---|---|---|
LABELER_DID |
yes | The labeler's DID (did:web:... or did:plc:...). |
|
LABELER_SIGNING_KEY |
yes | 64-char hex secp256k1 private key. From npm run keygen. |
|
ADMIN_TOKEN |
yes | Bearer token for POST /admin/labels. |
|
DATABASE_URL |
yes | Postgres connection string. | |
PORT |
no | 4100 |
HTTP port. |
REPORT_FORWARD_URL |
no | Backend endpoint to forward inbound reports to. | |
REPORT_FORWARD_TOKEN |
no | Bearer token sent with forwarded reports. | |
REPORT_ENRICH |
no | true |
Resolve and attach reported content to forwarded reports. |
REPORT_FORWARD_FORMAT |
no | raw |
raw (Bearer JSON) or coop (Coop /api/v1/report + X-API-KEY). |
COOP_POST_TYPE |
no | ATproto-post |
Coop item type id for posts (coop format). |
COOP_ACCOUNT_TYPE |
no | ATproto-account |
Coop item type id for accounts (coop format). |
PDS_URL |
no | Your PDS, for enforcement. Enables POST /admin/enforce. |
|
PDS_ADMIN_PASSWORD |
no | Admin password for PDS_URL. |
Ingestion worker (npm run ingest) only:
| Variable | Required | Default | Meaning |
|---|---|---|---|
COOP_ITEMS_URL |
yes | Coop's /api/v1/items/async endpoint. |
|
COOP_ITEMS_API_KEY |
yes | Coop org API key, sent as X-API-KEY. |
|
JETSTREAM_URL |
no | wss://jetstream2.us-east.bsky.network/subscribe |
Jetstream instance. |
JETSTREAM_COLLECTIONS |
no | app.bsky.feed.post,app.bsky.actor.profile |
Record types to stream. |
JETSTREAM_WANTED_DIDS |
no | (all) | Restrict to specific accounts. |
INGEST_BATCH_SIZE |
no | 50 |
Max items per POST. |
INGEST_BATCH_INTERVAL_MS |
no | 1000 |
Flush interval. |
See Ingesting content into Coop.
Endpoints #
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/health |
none | Liveness. Returns the labeler DID. |
POST |
/admin/labels |
bearer | Create and negate labels. |
POST |
/admin/enforce |
bearer | Take down or restore on your PDS. |
POST |
/coop/action |
bearer | Coop CUSTOM_ACTION shape: label and/or take down. |
POST |
/label |
bearer | Alias of /coop/action, matching the workshop relay path. |
GET |
/xrpc/com.atproto.label.queryLabels |
none | Query labels by URI pattern. |
GET (WS) |
/xrpc/com.atproto.label.subscribeLabels |
none | Label firehose. |
POST |
/xrpc/com.atproto.moderation.createReport |
service JWT | Accept a report. |
POST /admin/enforce #
Take down or restore a record or account on your own PDS. Bearer-authenticated with
ADMIN_TOKEN. Body: subject (a record { uri, cid } or account { did }), takedown
(true to take down, false to restore, default true), and optional ref. Requires
PDS_URL and PDS_ADMIN_PASSWORD; returns 501 otherwise. See Enforcement.
POST /coop/action #
Accepts Coop's CUSTOM_ACTION webhook body directly, so a Coop action uses troposphere with no
adapter. Bearer-authenticated with ADMIN_TOKEN. Body: item.id (the subject atproto URI or
DID) and custom with any of create / negate (label values) and takedown (boolean), plus
optional cid and ref. custom.labelVal is accepted as an alias for create, so a Coop action
wired for the old workshop relay (posting { item, custom: { labelVal } } to /label) works
unchanged. For a record without a cid, the current version is resolved from its own PDS; if that
cannot be resolved the label is still emitted, without a cid. See
Deploying with Coop.
POST /xrpc/com.atproto.moderation.createReport #
Standard atproto report intake, reached by PDS proxying (not called directly). Auth is the
inter-service JWT the reporter's PDS attaches. Input is reasonType, optional reason, and a
subject (a com.atproto.admin.defs#repoRef for an account or a com.atproto.repo.strongRef
for a record). Returns the created report with an integer id and the reportedBy DID. See
Receiving reports.
POST /admin/labels #
Request:
{
"subject": { "uri": "at://... or did:...", "cid": "optional" },
"create": ["label-value"],
"negate": ["label-value"]
}
Response:
{ "labels": [ { "seq": 1, "val": "label-value", "neg": false } ] }
Errors: 401 (missing or wrong token), 400 (missing subject.uri, or neither create nor
negate provided).
Scripts #
| Command | What it does |
|---|---|
npm run keygen |
Generate a signing key and print the public multibase. |
npm run db:init |
Apply the Postgres schema. |
npm run ingest |
Stream Jetstream into Coop's item intake. |
npm run dev |
Run with watch (type-stripped, no build step). |
npm run build |
Compile TypeScript to dist/. |
npm start |
Run the compiled build. |
npm run typecheck |
Type-check without emitting. |
npm run smoke |
End-to-end check against a running instance. |
Database #
Two tables. label: key columns seq (bigserial, the firehose cursor), src, uri, cid,
val, neg, cts, exp, sig. Timestamps are stored as text so a stored label reproduces
exactly the bytes that were signed. report: id (bigserial), reason_type, reason,
subject_type, subject_did, subject_uri, subject_cid, reported_by, created_at.
Enforcement is not stored; it is applied directly to the PDS.
Building the docs #
This book is built with mdBook:
mdbook serve docs # live preview
mdbook build docs # output to docs/book