Publishing #
What changes #
Publishing a lexicon today means calling a live server. Under Corpora it means compiling and deploying. The model inverts, and that inversion is the point.
goat lex publish |
corpora build + deploy |
|
|---|---|---|
| Mechanism | Authenticated write to your PDS | Offline compile, then a deploy |
| Auth | goat account login, app password |
None. Deploy credentials only. |
| Granularity | One record at a time | Full rebuild of every record |
| Signing key | Held by the PDS | Held by you, offline |
| Source of truth | The PDS repo | Your lexicons/*.json |
Incremental becomes wholesale #
goat publishes one record and the PDS updates the MST incrementally. Corpora rebuilds the
entire repo and regenerates every proof CAR, because any record change moves the MST root and
therefore the commit. There is no "publish just this one lexicon."
At this scale that is sub-second, but do not design for incremental updates.
The source of truth flips, which is the real upside #
With goat, the PDS repo is authoritative and your JSON files are a copy you push from, so the
two can drift. atcr.io's already have: 25 records published against 28 files, with
io.atcr.sailor.webhook live but absent from the repo and four files never published.
A build-from-source model makes that class of bug impossible. Delete a file and it is gone from the next build. Add one and it ships. The repo is derived output, never edited directly.
You hold the key #
Today your PDS holds the signing key. Under did:web plus Corpora you hold it. That is the
trade for not depending on the PDS, and it makes key handling an operational concern rather than
somebody else's problem.
goat does not go away #
Everything except lex publish is reads over standard XRPC and keeps working against the new
authority once DNS points at it. goat becomes the verification tool rather than the publishing
tool, which is a reasonable split.
| Command | Still useful for |
|---|---|
goat lex check-dns |
Checking DNS before publishing into new namespaces. Catches the depth trap. |
goat lex status |
Diffing local files against what is live. The authoritative drift report. |
goat lex breaking |
Schema evolution rules against the published version. Belongs in CI. |
goat lex lint |
Validating local lexicons |
goat get, goat repo export |
Inspecting the deployed authority |
goat lex publish |
Replaced by corpora build and a deploy |
Two of these are worth running before any of this is built, since they will tell you the true
state of the namespace better than manual inspection: goat lex check-dns and goat lex status.
The publish loop #
- Edit
lexicons/**/*.jsonin the project that owns them. goat lex lintandgoat lex breakingto catch schema problems early.corpora build, which validates authority-domain DNS, signs, and writesdist/.- Verify: run the official resolver against the built artifacts before deploying.
- Deploy the shell (
wrangler deploy, or push a container image). - Verify again against the live URL.
- Record
manifest.json'sdidDocHashandmstRootsomewhere that is not the serving host. - Optionally, tell the indexer (below).
Steps 4 and 6 are not optional ceremony. A broken authority does not fail loudly on your side; it
fails as invalid_scope on a stranger's login, hours later, on a cold cache.
Step 7 takes a second and is the whole of did:web's tamper-evidence story. did:web has no
operation log, so a fingerprint published away from the host is what lets anyone — including you,
later — detect that the served document changed. corpora verify --expect-did-doc <hash> is the
other half. See ARCHITECTURE.
Telling the indexer #
A Corpora authority never appears on a relay, because relays learn about repositories from a firehose and a static authority does not have one. RELAYS explains why that is architectural rather than an omission, and why it costs less than it sounds like.
The one known consumer that reads lexicon schemas off the firehose, lexicon.store, also accepts a direct submission, which does the same job without any of the machinery:
$ curl -X POST https://api.lexicon.store/requestIndex \
-H 'content-type: application/json' \
-d '{"did":"did:web:lexicon.example.com"}'
Optional, and only worth doing if you want your schemas discoverable by people who do not already know the NSID. Resolution does not depend on it in any way.
Key handling #
The signing key is a secp256k1 private key. It is an input to the build and nothing else.
- Never in git, in either repository.
- Injected as an environment variable at build time, from a password manager for manual publishing or a CI secret for automated publishing.
- Never deployed. No serving shell holds it, because nothing at serving time signs.
Rotation is cheap under did:web: generate a new key, rebuild (which re-signs everything), and
deploy. The new did.json and the new signatures ship in the same atomic bundle, so there is no
window where they disagree. Serve did.json with a short cache-control so a stale copy does
not outlive the rotation.
Losing the key is therefore recoverable, which is a genuine advantage of did:web over
did:plc. What did:web does not protect against is host compromise; see
ARCHITECTURE.
Automation #
Manual publishing with the key in a password manager is defensible at a few publishes per year, and it keeps the key out of CI entirely.
If you automate, the trade is convenience against putting a signing key in CI secrets. A reasonable middle path is CI that runs the build and verification on every change to the lexicon directory but only deploys on a tagged release, so the key is used rarely and deliberately.