Lexicons
Contrail emits lexicon JSON for every XRPC method it exposes. Your app needs those files for two things: generating typed TypeScript clients and publishing to a PDS so other apps can discover your schemas. The contrail-lex CLI handles both.
pnpm add -D @atmo-dev/contrail-lexicons @atcute/lex-cli
@atcute/lex-cli is a peer dep — you pin the version.
CLI
contrail-lex generate # emit lexicon JSON from your Contrail config
contrail-lex pull # wraps `lex-cli pull` (fetch external lexicons)
contrail-lex types # wraps `lex-cli generate` (JSON → TS types)
contrail-lex all # generate → pull → generate → pull → types
contrail-lex all --no-types # same, skip the type step
contrail-lex publish # publish lexicons to your PDS (add --dry-run to preview)
contrail-lex pull-service <url> # consume a deployed contrail's /lexicons endpoint
Config is auto-detected at contrail.config.ts, src/contrail.config.ts, src/lib/contrail.config.ts, or app/contrail.config.ts. Override with --config <path>.
What lands where
Running contrail-lex all organises everything under one lexicons/ directory:
lexicons/
custom/ # hand-authored lexicons you write (optional)
generated/ # JSON emitted from your Contrail config
pulled/ # external NSIDs fetched by lex-cli pull
lex.config.js # regenerated each run (gitignore)
src/lexicon-types/ # TS types from lex-cli generate
lexicons/custom/, lexicons/generated/, and lexicons/pulled/ should be committed — that way CI doesn’t need network access. lex.config.js and src/lexicon-types/ are regenerated on demand and safe to gitignore.
lexicons/generated/index.ts is also emitted on every run — a barrel that imports every lexicon the deployment speaks (generated + pulled + custom). Pass it to createWorker(config, { lexicons }) to expose them at /xrpc/<namespace>.lexicons on your deployed service; consumer apps can then pull-service against it.
Consuming a deployed contrail
If you’re building a frontend that talks to someone’s (or your own) deployed contrail, you don’t need the backend’s source code to get typed XRPC calls. Have the operator pass { lexicons } to createWorker, then:
contrail-lex pull-service https://my-contrail.dev/xrpc/com.example.lexicons
# or: contrail-lex pull-service https://my-contrail.dev --namespace com.example
npx lex-cli generate
pull-service hits the manifest endpoint, writes each lexicon under lexicons/pulled/<nsid>.json, and lex-cli generate emits TypeScript types for @atcute/client. Override the output dir with --out <path>.
The manifest includes the service’s generated lexicons plus any external NSIDs the generator $refs (e.g., app.bsky.actor.profile, community.lexicon.calendar.event) — so typegen resolves cleanly with no additional fetching from bsky / atproto registries.
No PDS setup required, no DNS, no published lexicon records — just an HTTP endpoint and typegen.
Publishing
Once your lexicons are generated and committed, publish them as com.atproto.lexicon.schema records on your PDS so other apps can resolve them:
contrail-lex publish <handle-or-did> <app-password>
# or via env vars (nicer for CI):
LEXICON_ACCOUNT_IDENTIFIER=you.bsky.social \
LEXICON_ACCOUNT_PASSWORD=xxxx-xxxx-xxxx-xxxx \
contrail-lex publish
Writes each lexicon to at://<did>/com.atproto.lexicon.schema/<nsid>. You’ll also need DNS TXT records for your NSID authorities — the command prints the exact records you need and won’t proceed until you confirm (override with --skip-confirm in CI).
Preview first with --dry-run:
contrail-lex publish --dry-run
Walks your lexicons/generated/ dir, prints the NSIDs it would publish and the exact TXT records you’d need. Doesn’t log in or write anything. Credentials aren’t required for dry-run, so it’s safe to run without secrets configured — useful for sanity-checking what a release would push.
One-off flow: generate once, commit, contrail-lex publish --dry-run to preview, contrail-lex publish once per version bump. You don’t need to re-publish unless your lexicon JSON changes.
Programmatic API
If you need to call the generator from code — e.g. to derive the XRPC method list for an OAuth permission set:
import { generateLexicons, extractXrpcMethods } from "@atmo-dev/contrail-lexicons";
const generated = generateLexicons({
config,
rootDir: process.cwd(),
outputDir: "lexicons/generated",
});
const methods = extractXrpcMethods(generated); // every query + procedure NSID
publishLexicons is also exported for custom flows, but the CLI covers the common case.