A personal Nostr relay running on Cloudflare Workers + D1 (SQLite). Free tier, no wrangler, no build step.
EVENT gets an explicit result (stored, duplicate, invalid, error).#e, #p, #t, etc., via an indexed event_tags table.ids/authors filters accept partial prefixes (8+ hex chars).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).
relay).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.
worker.js, then add worker.js and nostr-tools.js from v5/worker/ as separate files.worker.single.js into worker.js.DB.https://d1relay.nostrweb.workers.dev — you should see relay info (NIP-11) JSON.node v5/tests/run.js wss://d1relay.nostrweb.workers.devNo 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
events — one row per stored event:
id — 64-hex event id (primary key, so duplicates are rejected)pubkey, created_at, kind, contentraw_event — the full original JSON, returned verbatim
event_tags — a row per tag, for fast #tag
filters: event_id, name, value,
position.
| Message | Response |
|---|---|
["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"] |
| Version | What changed |
|---|---|
| v1 | Minimal relay (EVENT, REQ, EOSE). |
| v2 | NIP-11 info, shape validation, id check, multi-filter REQ, limit cap. |
| v2.5 | Inlined crypto library, real signature verification, rate limiting. |
| v3 | Stored full raw event JSON; ids filter. |
| v5 | NIP-20 OK codes, tag filters, NIP-45 COUNT, prefix matching, size caps, new event_tags schema, offline tests, updated tester. |