D1 Relay — Documentation

A personal Nostr relay running on Cloudflare Workers + D1 (SQLite). Free tier, no wrangler, no build step.

Features

Files

v5/
├── schema.sql            D1 schema (commented; use with wrangler/tools)
├── schema-console.sql    Same schema, NO comments — paste this into the
│                         D1 dashboard Console (it rejects comments)
├── worker/
│   ├── nostr-tools.js    crypto library (self-contained)
│   ├── worker.js         the relay (imports nostr-tools.js)
│   └── worker.single.js  optional single-file bundle of the two
├── tools/
│   ├── build-single.mjs  regenerates worker.single.js
│   └── build-site.mjs    regenerates the site assets
├── tests/
│   ├── local.mjs         offline test harness (56 checks, no installs)
│   └── run.js            live test of a deployed relay
└── tester/               standalone browser tester (works offline)

worker.single.js is optional — it exists as a paste-in fallback. You only ever need one of: two files (worker.js + nostr-tools.js) or one file (worker.single.js).

Deploy (Cloudflare dashboard, no wrangler)

1. Create the database

  1. Cloudflare dashboard → Workers & Pages → D1 → Create database.
  2. Name it (e.g. relay).
  3. Open the database → Console → paste the contents of v5/schema-console.sql → run.

⚠️ Use schema-console.sql, not schema.sql. The D1 Console fails with "Requests without any query are not supported" when the SQL contains -- comment lines (a known D1 parser bug). The console version is the same schema with no comments.

2. Create the Worker

  1. Workers & Pages → Create → Worker → Edit code.
  2. Multi-file: delete the default worker.js, then add worker.js and nostr-tools.js from v5/worker/ as separate files.
  3. Or single-file: paste worker.single.js into worker.js.
  4. Save and Deploy.

3. Bind the database

  1. Worker → Settings → Variables → D1 database bindings → Add binding.
  2. Variable name must be exactly DB.
  3. Select the database you created.

4. Test it

Run the tests locally

No npm install, no internet — just Node 22.5+ (built-in node:sqlite).

node v5/tests/local.mjs

Runs the relay (both modular and single-file) against an in-memory database and checks 56 things: storing, duplicates, bad signatures, malformed messages, prefix lookups, tag filters, COUNT, and limits.

To test a deployed relay over the network:

node v5/tests/run.js https://your-worker.workers.dev

Schema

events — one row per stored event:

event_tags — a row per tag, for fast #tag filters: event_id, name, value, position.

Relay behavior

MessageResponse
["EVENT", {...}] valid["OK", id, true, "stored"]
duplicate["OK", id, false, "duplicate: event already stored"]
invalid (shape/sig/id/too big/future)["OK", id, false, "invalid: ..."]
db write fails["OK", id, false, "error: db write failed"]
["REQ", sub, filter]["EVENT", ...] matches, then ["EOSE", sub]
["COUNT", sub, filter]["COUNT", sub, {count: N}]
malformed / too big["NOTICE", "..."]
too many events/reqs in 10s["NOTICE", "rate limited"]

Limits

Version log

VersionWhat changed
v1Minimal relay (EVENT, REQ, EOSE).
v2NIP-11 info, shape validation, id check, multi-filter REQ, limit cap.
v2.5Inlined crypto library, real signature verification, rate limiting.
v3Stored full raw event JSON; ids filter.
v5NIP-20 OK codes, tag filters, NIP-45 COUNT, prefix matching, size caps, new event_tags schema, offline tests, updated tester.