Relays architecture
This document describes current code. The approved Fetch / Coverage / Sync design and active implementation milestones describe the next revision; NIP-77, range history, 100-event incremental paging, and removal of presence expiry are not implemented yet.
Entrypoints
init.tsinitializes plugin SQLite and exposes command metadata.adapter.tsparses command invocations and routes to account commands orlist.commands/manager.tsdefines and executes update/check/fetch and account add/delete.commands/list/adapter.tsresolves the selected account, archive groups, relay scopes, and persisted coverage/missing groups.commands/list/renderers/web.tsbuilds generic WebNodes with scoped styles. All forms/actions operate in the timeline; no dock registration or core UI branches. Add-account uses the reusableselectsourcenostrAccountsto submit only the browser account or saved bunker account owner's public key. Manual npub entry submits through a separate form to the sameaddcommand. Each form puts the input and submit button on one row. Relay header/body cells use element nodes, including connection times, to maintain identical grid tracks (adjacent raw text nodes otherwise share an anonymous grid item).store.tsvalidates configuration/public keys and implements account/archive reads.coverage.tsowns source/destination derivation, additive presence-table migration, monotonic presence writes, and per-relay coverage/missing-kind/date aggregation.network.tsperforms bounded kind-0/NIP-65 discovery and WebSocket checks.operations.tsowns durable backup progress/checkpoints, raw NIP-01 queries distinguishing real EOSE from failures, signature-verified archival, and resume.publication.tsvalidates the browser-signed NIP-65 event against current account configuration and publishes to new/old/bootstrap relays with per-relay outcomes.automatic-refresh.tscoalesces background refreshes per database/account, caches completion for five minutes, and invalidates on source URL changes. Webliststarts refresh without waiting and opts into two-second polling only while pending. DM/CLI list does not start network work. Errors are displayed in Relays without discarding previously fetched metadata or local relay settings.db.tsopens SQLite and retains legacy scaffold CRUD alongside new table initialization.
Persistence
accounts: public-key identity, validated JSON relay configuration, profile JSON, observed published relay snapshot, and update timestamp.relay_checks: URL, last connection outcome, handshake duration, checked timestamp.events: original signed JSON and indexed ID/author/kind/time;operations.tsinserts verified public-event bodies idempotently during backup.relay_events: event/relay observations, independent of UI grouping.relay_event_presence:(relay_url, event_id)key, present/missing/unknown state, checked-at timestamp. Existing positive relay observations are copied withINSERT OR IGNORE; repeat initialization does not overwrite newer presence. Writes ignore out-of-order older observations. Unobserved pairs need no row.operations: durable backup/publication status and JSON progress. Backup data includes per-source pending time windows, page/received counts, errors, and newly archived count. Init marks previous running operations interrupted. Partial/ interrupted backups resume when source scope matches. No private signer data.relay_list_versions: newest published/discovered NIP-65 timestamp/event ID, preventing slower metadata refresh from restoring an older published snapshot.- Legacy
relayss: preserved scaffold records, excluded from the manager.
Tables are additive (CREATE TABLE IF NOT EXISTS). Future schema changes should
use explicit migrations. Account removal leaves archive facts intact. Tree
kind/year/month grouping is derived in UTC; it is not stored as a UI hierarchy.
Event operations boundary
list renders persisted accounts, relay observations, and archive groups.
Design-preview mode and sample data have been removed. Backup/publication are
active; network existence queries, sync, export, and event selection remain pending.
The header owns account removal and JSON editing. Relays owns NIP-65 read/write
configuration, connection status, and published-list application. Extra fetch
sources is independent. Fetch browses the archive, Coverage summarizes per-write-
relay presence, and Sync groups missing deliveries by relay/kind/date.
accountRelayScope derives fetch sources from configured write relays plus extras;
sync destinations are configured write relays only. A URL may exist in both lists;
editing either list does not silently edit the other. The source union is deduplicated.
Coverage joins an account's archived events to presence facts per destination relay. Absent, explicitly unknown, future-dated, and older-than-one-hour observations count as unknown. Only fresh missing facts populate the sync tree. Counts represent event/relay pairs; account event kinds/date groups remain unique event counts. Socket checks never imply event presence. Future network existence queries must complete before recording missing, and future delivery runners must recheck stale facts and read back accepted publications.
Network boundary
fetch queries configured and environment bootstrap relays using nostr-tools
signature-verified event subscriptions and selects the newest replaceable events.
The published NIP-65 snapshot is separate from local configuration. check
measures socket establishment only; NIP-42 authentication/relay permissions are
future work. No account private keys or bunker secrets are persisted.
backup uses configured write relays plus extra sources, not discovery bootstrap
relays. It queries each source sequentially with a 500-event limit, eight-second
timeout, and 100-page per-source run budget. Saturated windows split into disjoint
inclusive timestamp ranges. Dense single-second windows remain pending/partial;
other windows can still be explored. EOSE indicates completion of that bounded
query, not a global guarantee of account-history completeness. Verified public
events are inserted idempotently and presence observations refreshed atomically.
Private messaging and ephemeral kinds are excluded.
Publishing uses generic nostr.signEvent immediate signing with allowedPubkeys.
The selected account's author/signature and exact current tags are revalidated by
the plugin before publication. Client signing alone never counts as publishing.
The renderer polls only while metadata refresh or an operation is running.
Progress: implementation plan. Core contracts: plugin system and Web renderer.