1---2title: Develop and extend3audience: developer4---56# Develop and extend78## Layout910```11src/12 index.ts worker entry: hostname -> object, POST /lease13 edge.ts what the edge provides, with and without Cloudflare: the client's address, the lease limit, the hostname map14 names.ts valid names, reserved names, lease names15 pull.ts copy another relay in: NIP-77 as the initiator, one round per connection16 jobs.ts the alarm's job list: pulls, backfills, rebroadcasts, once or standing17 relay.ts the Durable Object: sockets, the alarm, members and bans, jobs, usage18 gates.ts the write gate and the read gate19 routes.ts the HTTP doors, in the order they are tried, each saying whether a blocked address may use it20 nip11.ts the information document and the supported-NIP list21 succession.ts an heir and a dead-man's switch22 kinds.ts every event kind the relay treats specially, by NIP23 store.ts SQLite schema and queries24 settings.ts policy, members, bans, kind rules, retention, export/import25 roles.ts who may do what: owner, moderator; one table for NIP-86 and NIP-2926 groups.ts NIP-29, one group per relay: joins, leaves, moderation events27 audit.ts the moderation log: one row per change from manage.ts or groups.ts28 presets.ts the templates as one-click presets, some with a standing pull29 config.ts the configuration document: parse, plan, apply, export30 connections.ts the Connect fold's app shortcuts: the library's parser, the owner's list, /connect.json31 manage.ts NIP-86 methods over NIP-9832 domains.ts custom hostnames: the Cloudflare client and the KV mapping33 dumps.ts scheduled JSONL dumps to R234 notify.ts NIP-17 messages from the relay to its owner35 pages.ts notes and articles as pages, the Atom feed36 sites.ts NIP-5A manifest validation, label parsing and site serving37 site-mirror.ts proxy and alarm-driven copies of missing site blobs38 site-auth.ts NIP-07 challenge and relay-signed site cookie39 grasp.ts GRASP-01 Git Smart HTTP and atomic event visibility40 git-sqlite.ts compressed Git objects, refs and retry receipts in SQLite41 git-objects.ts bounded indexed object graphs for Git reconciliation42 git-catalog.ts repository identities, quota totals and SQL work accounting43 git-storage.ts owner-only Git storage diagnostics44 git-backup.ts verified portable SQLite Git snapshots45 grasp-state.ts NIP-34 authority events, purgatory and cleanup state46 grasp-policy.ts GRASP-01 NIP-34 repository and ref policy47 card.ts the status card, signed and as SVG48 qr.ts a QR encoder for the card and the console49 fuel.ts meters, prices, receipts, LNURL50 identity.ts relay keypair and NIP-43 roster51 nip66.ts the relay's NIP-66 discovery record about itself52 blossom.ts BUD-01/02/04/06/08/09 on R253 nip96.ts NIP-96 door to the same bucket and table54 bridge.ts HTTP bridge55 invites.ts invite codes and the invite page56 nip05.ts names57 ratelimit.ts token buckets58 negentropy.ts hll.ts event.ts filter.ts59 dashboard.ts the console's page, from the three files in console/60 console/ console.html, console.css, console.js: the console as written61 gen/signer.ts generated: the NIP-46 client library the console loads from /signer.js62 gen/templates.ts generated from relay-templates/63 gen/connections.ts generated from connection-templates/64 landing.ts the apex, rendered from config65 ui.ts the shared look66test/67 unit/ pure functions on node: the QR encoder, bolt11 amounts, the edge68 object/ Durable Object tests in workerd, one file per feature or module69 helpers/ the socket client, the management call, media tokens, the QR reader70 conformance/ black-box suite for any relay URL, one file per NIP, files included71relay-templates/ one relay configuration per template, the presets the console offers72relay-config.schema.json the configuration file's schema, served at the apex73connection-templates/ one app shortcut per template, the library the Connect tab offers74connection-template.schema.json a connection template's schema, served at the apex75scripts/76 build/ build-signer.mjs signer-entry.js bundle nostr-tools for the console's remote signing77 build-console.mjs fold src/console into src/gen/console.ts78 build-templates.mjs fold relay-templates/ into src/gen/templates.ts79 build-connections.mjs fold connection-templates/ into src/gen/connections.ts80 check/ check-console.mjs check-celld.mjs check-connections.mjs check-config.mjs run by npm run typecheck; check-config also checks any file81 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:zaptest82 ops/ margin.mjs network.mjs relay.mjs npm run margin; npm run test:network; npm run relay check|plan|push|pull8384Every script has an npm name (package.json), and the docs use those names: a tutorial never says node and a path.85wrangler.jsonc the Worker on Cloudflare86wrangler.celld.jsonc the same Worker on celld (docs/16)87```8889## Run the tests9091```92npm test # unit and Durable Object tests93npm run typecheck94npm run test:conformance # against RELAY_URL, default ws://127.0.0.1:744795npm run test:network -- plan # print the finite production topology; run and cleanup are manual96```9798The conformance suite needs a claimed relay. Against a dev server:99100```101CLAIM=1 RELAY_URL=ws://dev.localhost:8787 npm run test:conformance102```103104CI 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](16-hosting-without-cloudflare.md)). Work lands through pull requests in small commits that each typecheck on their own; the branch checks and review carry the change to `main`.105106The production network exercise in [Scripts and agents](13-scripts-and-agents.md)107is an operator-run check of relay relationships. It creates five small,108run-owned relays, uses finite one-shot jobs, samples only public events from109the two named external relays and tears down its own relays. It is not a110scheduled test, load test or capacity benchmark. Its private manifest supports111manual cleanup after an interrupted run.112113## Add a management method1141151. Add an entry to `METHODS` in `src/manage.ts`: the action it needs (`roles.ts`), `reads: true` if it changes nothing, and `run`. The handler takes what it uses from the call: `str(i)` and `num(i)` for parameters, `s` for settings, `reply({ result })` or `reply({ error }, 400)` to answer. `supportedmethods`, the permission check and the moderation log read the same entry.1162. A method a moderator may call names an action in the moderator's set in `roles.ts`; anything else is the owner's.1173. If it changes state that other code caches, update `Settings` and its in-memory sets.1184. If the console should call it, add the control to `src/console/console.html` and a handler in `console.js`.1195. Cover it in the `test/object` file for its feature, with `rpc` from `test/helpers/relay.ts`. If it can show a member, an event or a file, ask `Settings.mayRead` (or `mayList` for names and counts) with what `whoAsks` found in the header, and add the path to the door walk in `test/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.120121## Add a NIP122123Most 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:124125| NIP | Module | Wired from |126|---|---|---|127| 29 groups | `groups.ts` | `acceptGroup` in `relay.ts`, after the write gate (`gates.ts`) |128| 43 roster | `identity.ts` | `publishMembership` in `relay.ts` |129| 66 discovery | `nip66.ts` | `publishDiscovery` in `relay.ts`, from `publishMembership` and the alarm |130| 46 transport | one kind in the write gate and the read gate | `gates.ts` |131| 86 methods | `manage.ts` with `roles.ts` | the RPC route |132| 96 files | `nip96.ts` over `blossom.ts` | one row in `routes.ts` |133| 11 extras | `settings.ts` fields, `nip11.ts` | the NIP-11 row in `routes.ts` |134135A draft NIP implemented ahead of upstream also gets a `docs/20-nip-<name>.md`136file. Record the upstream commit and date it tracks, its user-facing usage,137the relay-specific additions and limits, and what to watch upstream. Draft138NIPs remain out of `supported_nips` unless the NIP explicitly defines relay139advertising for them.140141A 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`).142143GRASP-01 follows the same split: `grasp-policy.ts` keeps NIP-34 parsing and144authorization pure, while the Git door owns Smart HTTP and the object-store145seam. The accepted kind 30618 state is the complete ref map the door may146serve. Keep the Git path separate from relay and NIP-5A routing, and record147any new GRASP capability in the information document and the numbered draft148specification. GRASP-02, GRASP-03, GRASP-05 and GRASP-06 are independently149gated extensions with dependency checks. GRASP-02/03/05 are bounded previews150and are not advertised until interoperability and conformance review; GRASP-08151remains outside this implementation.152153## The signer bundle154155Remote 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.156157## The console158159The 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.160161To 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.162163Rules 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.164165The 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](17-connection-templates.md).166167## Screenshots168169`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`.170171## Pull requests172173Branch from `main`, push, open a pull request. CI must pass. Squash merge is the only merge mode; branches are deleted on merge.