Lexicon authority
Serves io.atcr.* lexicons as a signed, static did:web authority on Cloudflare Workers, so the
namespace stops depending on the personal PDS at jarrett.app being online.
../lexicons stays the source of truth. Nothing here is edited by hand: dist/ is compiled
output, and the Worker is three lines over it. Built with Corpora.
What this changes
Today io.atcr.* resolves through did:plc:wfj5kyialpmcv2fzk6uqwsln on jarrett.app. When that
PDS returned HTTP 500, OAuth logins failed with invalid_scope: Failed to resolve requested permission set. After the cutover the same lexicons are served as precomputed bytes from a Worker
that holds no key, no state and no database, and there is no origin left to fail.
The signing key lives in a password manager and is used only by corpora build, offline. It is
never deployed, because nothing at serving time signs.
Two kinds of DNS, and they are not the same
Confusing these is the easiest way to break the namespace.
| Record | Purpose | When it changes |
|---|---|---|
lexicon.atcr.io |
the host the Worker serves on; is the DID under did:web |
created once, by wrangler |
_lexicon.atcr.io and five siblings |
the authority pointers that say which DID owns the NSIDs | flipped at cutover, step 4 |
Creating lexicon.atcr.io changes nothing about resolution. Until the six TXT records are flipped,
every resolver still follows the did:plc to jarrett.app, and the old authority stays fully
authoritative. That is what makes steps 1 to 3 zero-risk.
The six authority domains, because NSID authority is the NSID minus its final segment, reversed, and there is no hierarchical fallback:
_lexicon.atcr.io io.atcr.* 3 lexicons
_lexicon.hold.atcr.io io.atcr.hold.* 19 lexicons
_lexicon.image.hold.atcr.io io.atcr.hold.image.* 2 lexicons
_lexicon.repo.atcr.io io.atcr.repo.* 1 lexicon
_lexicon.sailor.atcr.io io.atcr.sailor.* 2 lexicons
_lexicon.stats.hold.atcr.io io.atcr.hold.stats.* 1 lexicon
All six exist. npm run domains re-derives this list from ../lexicons if a lexicon is ever added
a level deeper — which silently requires a new TXT record, and is the trap that left three of these
lexicons unresolvable until 2026-08-03.
First time
$ npm install
$ corpora keygen --out signing.key # then move it to a password manager and delete the file
The key is a build input and nothing else. You need it for every publish, because any record change moves the MST root and re-signs the whole repo — there is no incremental update. Losing it costs a rebuild; leaking it has no fix, because whoever holds it can sign a permission set that renders on an OAuth consent screen with text of their choosing.
Cutover
Order matters. Nothing here is destructive while the steps stay in sequence.
1. Deploy the authority, DNS pointers untouched.
$ export CORPORA_SIGNING_KEY="$(…from your password manager…)"
$ npm run build:cutover
$ npx wrangler deploy
build:cutover uses --dns-check cutover, which still fails if any of the six records is
missing but only warns that they name the old did:plc. That is the expected state here.
Reach for --no-dns-check and you lose the missing-record check too, at the moment it matters
most.
Do not create lexicon.atcr.io by hand; wrangler creates it when it attaches the Custom Domain,
and a pre-existing CNAME for that exact name makes the attach fail.
The zone has a wildcard *.atcr.io record (verified 2026-08-03: a name that cannot exist
resolves to the same Cloudflare IPs). Two things follow. A specific record beats a wildcard in
DNS, so the Custom Domain attach is expected to work regardless — but until it happens,
https://lexicon.atcr.io already answers, with HTTP 525 from the wildcard's origin rather than
NXDOMAIN. Do not read that 525 as the Worker being broken; it means the Custom Domain is not
attached yet. npm run verify:pre is only meaningful after step 1 succeeds.
2. Verify without touching DNS. The old authority is still live, so there is no risk yet.
$ npm run verify:pre
$ curl -sI https://lexicon.atcr.io/.well-known/did.json # 200, no redirect
3. Flip the six TXT records to did=did:web:lexicon.atcr.io.
4. Verify for real, now including DNS, and cross-check with an independent implementation:
$ npm run verify
$ goat lex resolve io.atcr.authFullApp
$ goat lex resolve io.atcr.hold.image.getConfig # the deeper authority
5. Leave the old records on the PDS. Do not delete them. Rollback is editing the six TXT records back, with a 300-second TTL, and the old authority is untouched and immediately authoritative again.
6. Retire the old records once nothing has resolved through them for a full expiration window. Worth actually doing rather than leaving forever: relay indexes hold copies of those records and will otherwise serve a frozen snapshot of the schemas indefinitely.
What the cutover changes in the records themselves
Checked 2026-08-03 by comparing record CIDs, which depend only on content:
- 22 of 24 shared lexicons are byte-identical to what
jarrett.appserves today. - 2 differ and both are purely additive —
io.atcr.hold.scangains optionalreasonandstatus,io.atcr.sailor.profilegains optionalociClientandregistryDomain. Zero removals, andgoat lex breakingpasses both. - 4 ship for the first time:
io.atcr.hold.image.config,io.atcr.hold.image.getConfig,io.atcr.hold.purgeManifest,io.atcr.hold.stats.daily. - 1 disappears:
io.atcr.sailor.webhook, live on the PDS and absent from../lexicons. Nothing references it, no permission set lists it, and no records of that$typeexist. Add the file whenever you want it back;_lexicon.sailor.atcr.ioalready covers it.
Because the resolved content is materially unchanged, Auth Servers holding a cached permission set never notice the cutover, and those resolving fresh get the same document under a new signature.
Publishing, afterwards
$ goat lex lint ../lexicons/**/*.json
$ goat lex breaking
$ export CORPORA_SIGNING_KEY="…"
$ npm run build && npm run verify:local # after `npm run serve` in another shell
$ npx wrangler deploy
$ npm run verify
Then record dist/manifest.json's didDocHash and mstRoot somewhere that is not this host — a
git tag or a release note. did:web has no operation log, so a fingerprint published elsewhere is
what makes tampering detectable, and corpora verify --expect-did-doc <hash> is the other half.
Optionally, tell the indexer, since a static authority never appears on a relay:
$ curl -X POST https://api.lexicon.store/requestIndex \
-H 'content-type: application/json' -d '{"did":"did:web:lexicon.atcr.io"}'
Deploy credentials
wrangler login requests OAuth scopes for every Workers product. A scoped API token is narrower:
Workers Scripts:Edit on the account and Workers Routes:Edit scoped to the atcr.io zone,
passed as CLOUDFLARE_API_TOKEN, with CLOUDFLARE_ACCOUNT_ID set so it does not also need
Account Settings:Read. Attaching a Custom Domain may additionally require Zone:DNS:Edit; start
without it and add it only if the deploy fails on permissions.
Reference
- Corpora README — the tool
- DEPLOYING — Cloudflare limits, the CDN query-string trap
- ROADMAP — the cutover sequence this runbook follows
- RELAYS — why this will never appear on a relay