A standalone, OpenAPI-driven static reference for Bluesky's HTTP/XRPC endpoints,
generated from the canonical on-network lexicon schemas. This replaces the old
docs/api reference that was embedded in (and slowed down) the Docusaurus build.
endpoints.config.ts (INCLUDE_PREFIXES + SCHEMA_AUTHORITIES)
│ scripts/seed-allowlist.ts (enumerate every query/procedure NSID)
▼
lexicons.json (allowlist, CID-pinned)
│ @atproto/lex (resolves NSIDs via DNS, fetches + transitive defs)
▼
lexicons/**/*.json (committed lexicon documents)
│ scripts/probe-implemented.ts (live-probe com.atproto queries)
▼
unimplemented.json (com.atproto endpoints the network 501s; build input)
│ src/build-openapi.ts (lexicon -> OpenAPI converter, one doc per view)
▼
openapi.<view>.json (Bluesky App / Bluesky DMs / Ozone Moderation)
│ src/render.ts (vendors Scalar's MIT standalone bundle)
▼
out/ (index.html + openapi.<view>.json + scalar.standalone.js — fully self-contained)
- Scope is by namespace, endpoints are auto-discovered.
INCLUDE_PREFIXESinendpoints.config.tsdefines which namespaces the reference covers (app.bsky.*,com.atproto.*,chat.bsky.*,tools.ozone.*).npm run seedenumerates every query/procedure NSID published in them (from the canonical accounts inSCHEMA_AUTHORITIES), skipping unspecced/temp/deprecated and anything inEXCLUDE, and writes the result tolexicons.json. - Only endpoints get cards. The converter emits a path (and sidebar tag) only
for query/procedure defs in the included namespaces. Referenced schema defs
(
*.defs, records, tokens) become OpenAPI components — needed for$refs — but never appear as empty endpoint cards. Deprecated schema defs are kept (so refs don't dangle) but flaggeddeprecated. - Unimplemented
com.atprotoendpoints are unlisted. The schema authorities publish every speccedcom.atprotoendpoint, but Bluesky's live services don't serve all of them (e.g.com.atproto.identity.resolveDid/resolveIdentitycurrently return501 MethodNotImplemented).npm run probe(scripts/probe-implemented.ts) calls eachcom.atprotoquery against the canonical hosts, records the ones nobody answers inunimplemented.json, and the converter drops those cards — same treatment as deprecated/unspecced. Routing is the trick: every host501s methods that aren't its own, so AppView/relay are used as positive ("served here") signals only and an authenticated PDS is the authority (the entryway auth-gates before method resolution, so unauthenticated every method returns401). Only queries are probed — procedures (writes) are never fired at the live network. See thePROBE_*note inendpoints.config.ts. - Split into views.
VIEWSinendpoints.config.tspartitions the namespaces into separate OpenAPI documents — Bluesky App (app.bsky.*+com.atproto.*), Bluesky DMs (chat.bsky.*), and Ozone Moderation (tools.ozone.*) — each written asopenapi.<slug>.json. The renderer surfaces them via Scalar's multi-source switcher (the dropdown in the top left). All views share the same Introduction. - Bluesky-first ordering: within each view,
NAMESPACE_ORDERbiasesapp.bsky.*andcom.atproto.*ahead of the rest (rendered via OpenAPIx-tagGroups). Shared auth/proxy guidance lives in the OpenAPIinfo.descriptionrendered as each view's Introduction (SHARED_DESCRIPTIONinbuild-openapi.ts).
npm install # one-time
npm run seed # enumerate endpoints in INCLUDE_PREFIXES -> lexicons.json + fetch
npm run probe # live-probe com.atproto queries -> unimplemented.json
npm run build # install-lexicons:ci -> build:openapi -> build:site
npm run build:openapi # lexicons/ -> openapi.<view>.json (one per VIEW)
npm run build:site # openapi.<view>.json -> out/
npx serve out # preview (use http; file:// won't fetch the JSON)Changing scope: edit INCLUDE_PREFIXES / EXCLUDE in endpoints.config.ts, run
npm run seed, then commit lexicons.json + lexicons/. The refresh CI runs
npm run seed weekly and opens a PR when the published endpoints change.
Refreshing the implementation boundary: npm run probe regenerates
unimplemented.json; commit it and rebuild. For the authoritative check, give it an
app password. Bluesky app passwords carry
full account access (there's no read-only kind), but the probe only ever sends GETs
— it never writes — so a throwaway/secondary account's app password is plenty:
BSKY_PROBE_IDENTIFIER=you.bsky.social \
BSKY_PROBE_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx \
npm run probe # BSKY_PROBE_PDS overrides the PDS host if self-hostedWithout credentials the probe is a safe no-op: it can't get past the PDS auth gate,
so it unlists nothing and reports the auth-gated endpoints as inconclusive. To keep
the boundary fresh automatically, run npm run probe in the weekly refresh workflow
with the app password stored as a CI secret.
.github/workflows/endpoints-build.yml— verifies pinned lexicons, builds the spec + site, uploads the artifact (deploy step deferred to the host)..github/workflows/endpoints-refresh-lexicons.yml— weeklylex install --update; opens a PR when canonical schemas change.
scripts/lex.mjswraps the@atproto/lexCLI to work around nodejs/node#62347 (Windows-only loopback-DNS bug that breakslex's_lexicon.*TXT lookups). It is a no-op on healthy systems, including Linux CI.openapi.*.jsonandout/are git-ignored; they are regenerated from the committedlexicons/+lexicons.json.