A serverless AT Protocol lexicon authority
atproto lexicon
README.md

Cloudflare Worker #

A lexicon authority as a single Worker script. No bindings, no secrets, no origin.

corpora build   (your machine, with the key)  ->  dist/
wrangler deploy (any machine, no key)         ->  330+ cities

Deploy #

$ npm install
$ CORPORA_SIGNING_KEY=$(pass corpora/signing-key) \
    npx corpora build --lexicons ./lexicons --out ./dist --did did:web:lexicon.example.com
$ npx wrangler deploy

Two commands, and only the first one touches the key. wrangler deploy uploads a script whose only interesting content is the dist/ you just built. Nothing in the deploy step needs the signing key, and nothing in the deployed Worker holds it.

Before the first deploy, edit wrangler.jsonc: set name, set routes[0].pattern to your hostname, and set compatibility_date to today's date.

What the entry point is #

import { createWorker } from 'corpora/worker'
import data from './dist/data.js'

export default createWorker(data)

dist/data.js is a generated ESM module of base64 payloads. corpora/worker wraps corpora/serve, which is a switch statement over those payloads. Neither imports anything, so wrangler bundles them without a single polyfill or module-resolution flag. That is the payoff of keeping @atproto/repo in the offline build — see PRIOR_ART for what the alternative costs.

Do not use Workers static assets #

wrangler.jsonc has no assets block on purpose.

Static assets are lazily populated: fetched from central storage on first request and cached at the nearest location, with tiered caching on a miss. That is caching with fallback to an origin, which is the exact topology this project exists to remove. Worker scripts replicate instead, propagating globally in under a second. Static assets also fix Content-Type from the file extension at upload time and _headers cannot override it, so .car files would be served with a MIME type you cannot correct. See ARCHITECTURE.

The payload is a fraction of the bundle limit, so embedding it in the script costs nothing.

Check the bundle size #

$ npx wrangler deploy --dry-run --outdir .wrangler/bundled
Total Upload: 259.61 KiB / gzip: 47.23 KiB

The gzip: figure is the one that counts against the limit: 3 MB on the Workers Free plan, 10 MB on Paid, plus a 64 MB uncompressed ceiling on both. A real namespace lands two orders of magnitude below that. Watch the reported startup_time_ms too — the whole script's global scope, including the base64 decode in data.js, must execute within 1 second.

Routing and DNS #

routes with custom_domain: true makes wrangler create the DNS record and issue the certificate, so there is no DNS step. It requires the zone to be in the same Cloudflare account and the hostname to have no existing CNAME record.

did:web resolution fetches https://<host>/.well-known/did.json with redirects treated as errors, so the Worker must own the hostname the DID names — not a name that redirects to it. Serve the authority from a dedicated subdomain and this is automatic.

Verify #

$ npx corpora verify --origin https://lexicon.example.com

A deploy that succeeds is not a deploy that resolves. See DEPLOYING.

Cost #

Every request is a Worker invocation returning bytes already in memory: no subrequests, no storage reads, single-digit milliseconds of CPU. A lexicon authority's traffic is Auth Servers resolving on a cold cache, which is not much traffic. Uncommenting the cache block in wrangler.jsonc removes even the invocation for repeat requests.