| title | Develop and extend |
|---|---|
| audience | developer |
src/
index.ts worker entry: hostname -> object, POST /lease
edge.ts what the edge provides, with and without Cloudflare: the client's address, the lease limit, the hostname map
names.ts valid names, reserved names, lease names
pull.ts copy another relay in: NIP-77 as the initiator, one round per connection
jobs.ts the alarm's job list: pulls, backfills, rebroadcasts, once or standing
relay.ts the Durable Object: sockets, the alarm, members and bans, jobs, usage
gates.ts the write gate and the read gate
routes.ts the HTTP doors, in the order they are tried, each saying whether a blocked address may use it
nip11.ts the information document and the supported-NIP list
succession.ts an heir and a dead-man's switch
kinds.ts every event kind the relay treats specially, by NIP
store.ts SQLite schema and queries
settings.ts policy, members, bans, kind rules, retention, export/import
roles.ts who may do what: owner, moderator; one table for NIP-86 and NIP-29
groups.ts NIP-29, one group per relay: joins, leaves, moderation events
audit.ts the moderation log: one row per change from manage.ts or groups.ts
presets.ts the templates as one-click presets, some with a standing pull
config.ts the configuration document: parse, plan, apply, export
connections.ts the Connect fold's app shortcuts: the library's parser, the owner's list, /connect.json
manage.ts NIP-86 methods over NIP-98
domains.ts custom hostnames: the Cloudflare client and the KV mapping
dumps.ts scheduled JSONL dumps to R2
notify.ts NIP-17 messages from the relay to its owner
pages.ts notes and articles as pages, the Atom feed
sites.ts NIP-5A manifest validation, label parsing and site serving
site-mirror.ts proxy and alarm-driven copies of missing site blobs
site-auth.ts NIP-07 challenge and relay-signed site cookie
grasp.ts GRASP-01 Git Smart HTTP and atomic event visibility
git-sqlite.ts compressed Git objects, refs and retry receipts in SQLite
git-objects.ts bounded indexed object graphs for Git reconciliation
git-catalog.ts repository identities, quota totals and SQL work accounting
git-storage.ts owner-only Git storage diagnostics
git-backup.ts verified portable SQLite Git snapshots
grasp-state.ts NIP-34 authority events, purgatory and cleanup state
grasp-policy.ts GRASP-01 NIP-34 repository and ref policy
card.ts the status card, signed and as SVG
qr.ts a QR encoder for the card and the console
fuel.ts meters, prices, receipts, LNURL
identity.ts relay keypair and NIP-43 roster
nip66.ts the relay's NIP-66 discovery record about itself
blossom.ts BUD-01/02/04/06/08/09 on R2
nip96.ts NIP-96 door to the same bucket and table
bridge.ts HTTP bridge
invites.ts invite codes and the invite page
nip05.ts names
ratelimit.ts token buckets
negentropy.ts hll.ts event.ts filter.ts
dashboard.ts the console's page, from the three files in console/
console/ console.html, console.css, console.js: the console as written
gen/signer.ts generated: the NIP-46 client library the console loads from /signer.js
gen/templates.ts generated from relay-templates/
gen/connections.ts generated from connection-templates/
landing.ts the apex, rendered from config
ui.ts the shared look
test/
unit/ pure functions on node: the QR encoder, bolt11 amounts, the edge
object/ Durable Object tests in workerd, one file per feature or module
helpers/ the socket client, the management call, media tokens, the QR reader
conformance/ black-box suite for any relay URL, one file per NIP, files included
relay-templates/ one relay configuration per template, the presets the console offers
relay-config.schema.json the configuration file's schema, served at the apex
connection-templates/ one app shortcut per template, the library the Connect tab offers
connection-template.schema.json a connection template's schema, served at the apex
scripts/
build/ build-signer.mjs signer-entry.js bundle nostr-tools for the console's remote signing
build-console.mjs fold src/console into src/gen/console.ts
build-templates.mjs fold relay-templates/ into src/gen/templates.ts
build-connections.mjs fold connection-templates/ into src/gen/connections.ts
check/ check-console.mjs check-celld.mjs check-connections.mjs check-config.mjs run by npm run typecheck; check-config also checks any file
dev/ dev-signer.mjs seed.mjs stage.mjs junk.mjs shot.mjs zaptest.mjs npm run dev:signer, dev:seed, dev:stage, dev:junk, dev:shot, dev:zaptest
ops/ margin.mjs network.mjs relay.mjs npm run margin; npm run test:network; npm run relay check|plan|push|pull
Every script has an npm name (package.json), and the docs use those names: a tutorial never says node and a path.
wrangler.jsonc the Worker on Cloudflare
wrangler.celld.jsonc the same Worker on celld (docs/16)
npm test # unit and Durable Object tests
npm run typecheck
npm run test:conformance # against RELAY_URL, default ws://127.0.0.1:7447
npm run test:network -- plan # print the finite production topology; run and cleanup are manual
The conformance suite needs a claimed relay. Against a dev server:
CLAIM=1 RELAY_URL=ws://dev.localhost:8787 npm run test:conformance
CI runs typecheck and the object tests on every push; the conformance suite against the Worker on celld dev is a separate workflow run on demand (Hosting without Cloudflare). Work lands through pull requests in small commits that each typecheck on their own; the branch checks and review carry the change to main.
The production network exercise in Scripts and agents is an operator-run check of relay relationships. It creates five small, run-owned relays, uses finite one-shot jobs, samples only public events from the two named external relays and tears down its own relays. It is not a scheduled test, load test or capacity benchmark. Its private manifest supports manual cleanup after an interrupted run.
- Add an entry to
METHODSinsrc/manage.ts: the action it needs (roles.ts),reads: trueif it changes nothing, andrun. The handler takes what it uses from the call:str(i)andnum(i)for parameters,sfor settings,reply({ result })orreply({ error }, 400)to answer.supportedmethods, the permission check and the moderation log read the same entry. - A method a moderator may call names an action in the moderator's set in
roles.ts; anything else is the owner's. - If it changes state that other code caches, update
Settingsand its in-memory sets. - If the console should call it, add the control to
src/console/console.htmland a handler inconsole.js. - Cover it in the
test/objectfile for its feature, withrpcfromtest/helpers/relay.ts. If it can show a member, an event or a file, askSettings.mayRead(ormayListfor names and counts) with whatwhoAsksfound in the header, and add the path to the door walk intest/object/exposure.test.ts, which knocks on every path as a stranger and as a signed-in non-member and fails on anything of a member's that comes back.
Most NIPs land as their own module, wired in with one import and one row in routes.ts or one call from relay.ts. That is the pattern to follow:
| NIP | Module | Wired from |
|---|---|---|
| 29 groups | groups.ts |
acceptGroup in relay.ts, after the write gate (gates.ts) |
| 43 roster | identity.ts |
publishMembership in relay.ts |
| 66 discovery | nip66.ts |
publishDiscovery in relay.ts, from publishMembership and the alarm |
| 46 transport | one kind in the write gate and the read gate | gates.ts |
| 86 methods | manage.ts with roles.ts |
the RPC route |
| 96 files | nip96.ts over blossom.ts |
one row in routes.ts |
| 11 extras | settings.ts fields, nip11.ts |
the NIP-11 row in routes.ts |
A draft NIP implemented ahead of upstream also gets a docs/20-nip-<name>.md
file. Record the upstream commit and date it tracks, its user-facing usage,
the relay-specific additions and limits, and what to watch upstream. Draft
NIPs remain out of supported_nips unless the NIP explicitly defines relay
advertising for them.
A NIP that changes the query surface touches store.ts. Add the number to SUPPORTED_NIPS in nip11.ts only when the NIP says relays advertise it, with a word in the comment there for the less obvious ones. Add a file named after the NIP in test/conformance so the behavior is checked from outside, and a Durable Object test in test/object, named after the module it exercises; a NIP number names a test file only where the feature has no other name (nip05, nip11, nip66).
GRASP-01 follows the same split: grasp-policy.ts keeps NIP-34 parsing and
authorization pure, while the Git door owns Smart HTTP and the object-store
seam. The accepted kind 30618 state is the complete ref map the door may
serve. Keep the Git path separate from relay and NIP-5A routing, and record
any new GRASP capability in the information document and the numbered draft
specification. GRASP-02, GRASP-03, GRASP-05 and GRASP-06 are independently
gated extensions with dependency checks. GRASP-02/03/05 are bounded previews
and are not advertised until interoperability and conformance review; GRASP-08
remains outside this implementation.
Remote signing needs NIP-44 and secp256k1, which browsers do not ship. scripts/build/signer-entry.js imports the pieces of nostr-tools the console uses and npm run build:signer bundles them with esbuild into src/gen/signer.ts, a string the relay serves at /signer.js. The console loads it only when someone picks a remote signer. The generated file is committed; npm run typecheck fails when it is stale.
The console is three ordinary files in src/console: console.html (the body), console.css and console.js. npm run build:console folds them into src/gen/console.ts, the strings the relay serves; npm run dev runs it first, and npm run typecheck fails when the generated file is stale. It is a generated module rather than a text-module rule in wrangler.jsonc because celld deploy refuses a config with rules. scripts/check/check-console.mjs runs the script's synchronous start against a stub document, so a handler wired above the helper it calls fails typecheck instead of a live page. The shared shell in ui.ts provides fonts, colors, buttons, inputs and the sticker vocabulary; keep new pages inside it.
To look at what you changed: npm run dev, then npm run dev:signer, open http://<name>.localhost:8787/, paste the window.nostr snippet from that script into the devtools console, and sign in or claim. Playwright's CLI can drive the same loop: open the page, inject the snippet with eval, click, screenshot at a phone width and at a desktop width. scripts/dev/shot.mjs renders a page over the Chrome debugging protocol when Playwright is not around.
Rules that hold across pages: no middle dots, no purposeless subtext, tables scroll inside their card on narrow screens, decorative elements are not selectable, a multi-field form uses the labelled grid rather than one row of inputs, and the copy above a block is one sentence.
The Connect fold on the relay's page and the Connect tab in the owner's console are clients of /connect.json and the listconnectiontemplates, listconnections and setconnections methods. What they show is a library of files in connection-templates/, folded into src/gen/connections.ts by npm run build:connections the way the presets are; the file format, the placeholders, the owner's list and how to add a template are in Connection templates.
npm run dev:shot <url> <out.png> [width] [height] [--nostr] [--eval <js>] renders a page with phone or desktop emulation over the Chrome debugging protocol and prints the layout's scroll width, which should equal the viewport width. --nostr signs in as the dev signer's key (npm run dev:signer must be running), so the owner's console is in the picture; --eval runs a snippet after the page has loaded, such as opening a fold or a tab, before the shot. The pictures in docs/img/ come from a relay staged with npm run dev:stage.
Branch from main, push, open a pull request. CI must pass. Squash merge is the only merge mode; branches are deleted on merge.