First-party Nostr relay (write-restricted) for nbread.lol - #8
Merged
Conversation
wss://nbread.lol/relay — reads open to any client, writes restricted to claimed nbread handles via NIP-42 AUTH + the D1 users allowlist. Single hibernatable DO (no timers) registered ahead of securityHeaders so the 101 upgrade stays immutable; per-connection auth/challenge state in the socket attachment, ephemeral subscriptions in DO SQLite. Events flow through the existing mirrorEvent into the shared D1 events table (relay and blog never disagree); REQ reads translate to SQL over events.raw. Write ladder: auth-required -> pubkey match -> allowlist (5-min cache) -> created_at upper bound -> rate limit (fail-closed) -> mirror -> tombstone-guarded live fan-out. NIP-11 served by the Worker; accepted kinds 30023/5/0.
editorRelays prepends wss://nbread.lol/relay so every publish reaches it first; NIP-05 well-known adds it to each pubkey's relay hints; cron filters the self-relay out of its outbound dials (same-zone ws subrequest won't re-enter the Worker, and the store is shared). Docs page documents the relay and its write policy.
…ests smoke.sh asserts the NIP-11 document (supported_nips incl. 42, restricted_writes) and the info page; ops.md adds the /relay endpoint rows, the DO budget row (free-plan 100k req/day, hibernation = no duration billing, relay is additive), and the WAF ws-upgrade note; manual checklist gains a first-party relay section (nak readback, claimed vs unclaimed write, external-client read).
This was referenced Jul 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5.
What
nbread now runs its own Nostr relay at
wss://nbread.lol/relayso claimed blogs'kind 30023events have a durable, nbread-controlled home instead of depending entirely on third-party relays.usersallowlist (handle IS NOT NULL AND blocked = 0) +event.pubkey === authed pubkey. Accepted kinds:30023,5,0(own events only).How (design decisions)
idFromName("relay:v1")), registered on the outer Hono app beforesecurityHeaders— a 101 upgrade is an immutable response (headers.set()would throw), and guard/tenant/csrf never run per-upgrade. Free-plan requirement met vianew_sqlite_classes. No timers/alarms (they'd defeat hibernation → duration billing); per-connection auth+challenge state lives in the socket attachment, ephemeral subscriptions in DO SQLite.eventstable — writes go through the existingmirrorEvent(replaceable upsert, NIP-09 delete horizons, render-at-ingest, FTS, cache-gen bump), reads translate NIP-01 filters to SQL overevents.raw. The relay and the blog can never disagree, and "cron reads its own relay first" becomes a no-op by construction.auth-required→ pubkey-match → allowlist (5-min per-connection cache) →created_atupper bound → rate limit (D1, fail-closed:relay:ev:pk30/5min +relay:global:store500/day) →mirrorEvent→ tombstone-guarded live fan-out.idx_events_author_time (pubkey, created_at DESC)for the dominant authors+kinds query, and reserves therelayhandle.Budget (free plan)
DO requests count only ws upgrades + messages/20 against 100k/day; hibernating objects accrue no duration charge; the
relay:global:storecap bounds relay-driven KV gen-bumps to half the 1k/day budget. If DO budget is exhausted the relay 503s but the blog/editor//api/mirrorare unaffected — the relay is additive, never load-bearing.Security
Adversarially reviewed in three lenses (protocol/DO invariants, write-gate attack surface, budget/integration). Fixes applied: live fan-out re-checks the
deletedflag so tombstones are never pushed;#dlive-match aligned to the slotted-d-tag SQL semantics;created_atupper bound now enforced to match the NIP-11 advertisement; relay 404/429 responses carrynosniff/Referrer-Policy. Confirmed:securityHeaders/guard/tenantstill wrap every non-relay route;headers.spec.tspasses unchanged.Known v1 limitations (documented, low impact)
#t/#e) are JS-post-filtered after the SQLLIMIT, so such a REQ can under-deliver beyond the limit window (rare for long-form;#d/authors/kinds/ids are fully SQL)./api/mirrorand cron stores don't live-fan-out (only relay-received EVENTs do); the editor broadcasts to the relay by default, closing most of the gap.Testing
wrangler deploy --dry-runresolves theRELAY_DObinding; vendor-bundle drift clean.scripts/smoke.shasserts the NIP-11 document;docs/manual-signer-tests.mdgains a relay section (nakreadback, claimed-vs-unclaimed write, external-client read). Deploy is user-gated — needswrangler d1 migrations apply nbread --remotefor 0005.