clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / docs / architecture.md
architecture.md 7.9K source

Architecture

tiny assembles the combined relay host. The public tinyrelay package provides the standalone and embeddable Nostr relay, tinygit owns Git functionality, and tinyclient owns the frontend. These functional modules share one repository and Go module. See Module boundaries and protocol contracts for ownership rules, extension documentation and proposed follow-up seams.

Each tenant has a SQLite store, a policy snapshot and a set of services. The daemon constructs those services, connects their contracts and manages their lifetimes. HTTP, WebSocket and management adapters establish caller identity and enter the tenant's operation gate before calling services.

Find your starting point

cmd/tiny owns combined command dispatch, while cmd/tinyrelay starts the standalone relay through the shared runner. daemon describes tenant assembly and event flow. Package overviews and interface, constructor and transaction comments describe the contracts at their declarations. These comments are also available through go doc and editor symbol help.

Package layout

Executable entry points live in cmd/tiny, cmd/tinyrelay, cmd/tinygit and cmd/tinyclient. Host services and storage implementations live under internal/, where Go prevents outside projects from importing them.

tinyrelay, tinygit, tinyclient and protocol/ are public so other Go projects can embed them. Separate executables alone do not require public packages. These packages expose the contracts an embedder needs; host policy, database access and tenant assembly remain internal. All packages belong to the root Go module.

Responsibilities

Packages Responsibility
protocol/nostr Public event and filter values, canonical encoding, signatures and generic wire checks. No host access policy.
protocol/auth Public request-proof verification and replay state. No membership or permission decisions.
tinyrelay Standalone Nostr transport, durable event storage and generic relay metadata and policy. No tenant, Git or browser features.
event, auth Internal compatibility aliases and feature helpers; private-kind search filtering remains host behavior.
policy, gates Host policy, event admission and visibility checks.
storage, work Event persistence, queries, transaction hooks and durable queue claims.
community, communityread Membership, moderation, rooms, agents and the read models consumed by records.
records Relay identity, signed protocol records, projections and notifications.
blob, sites Content-addressed files, upload rules and sites backed by signed manifests.
tinygit Repository admission, object storage, synchronization, repair and Git HTTP endpoints.
replication, syncprotocol Relay synchronization plans and transports, count sketches and reconciliation sessions.
relay, relaycmd, mcp, tinyclient WebSocket sessions, standalone command lifecycle, MCP requests, HTML pages and browser interactions.
agentrunner The optional agent process: room mentions, durable queue recovery, ACP or command execution, and permission prompts.
views, wiki, webpush Fenced-block parsing, article rendering and encrypted browser push delivery.
seedmark Deterministic decorative SVG avatars for people and rooms without a profile picture.
catalog, domains Tenant lifecycle records, filesystem paths and host mappings.
templates, configport Built-in policy templates and configuration import, export and application.
telemetry, daemon Observability, service assembly, request routing and worker ownership.

Ownership and contracts

App owns the catalog, telemetry and active tenants. Each Tenant owns its store and service lifetimes. Services borrow the store and policy reader supplied at construction. Policy reads return a shared snapshot; management writes persist and replace that snapshot. Shutdown stops admissions, joins background work and drains active operations before closing storage.

Services own the schema and data access for their features. storage.Save and storage.SaveTx accept transaction hooks so event data, projections and work intents can commit together. APIs that receive *sql.Tx use the caller's transaction. Community event handlers that accept a persistence callback open the transaction and pass it into the callback.

Consumers declare interfaces around the operations they need. records.CommunityReader reads membership and moderation projections through communityread values. relay.Backend supplies event operations to the WebSocket router. tinyrelay.Backend is the embedding contract for a host that owns persistence, while tinyrelay.OpenServer supplies a durable standalone implementation. tinyclient.Backend supplies management results, and tinyclient.RoomsReader supplies typed room pages. The daemon implements these adapters and assembles the feature handlers used by work.Worker.

Event acceptance

protocol/nostr.Validate checks wire shape, event ID and signature. tinygit.ParseMetadata separately parses repository claims without establishing signer validity, repository authority or object availability. Git admission entry points perform signature and metadata checks before host admission callbacks. Generic nostr.Matches is not a visibility gate; callers must apply host access policy.

Client publication and imported events use the shared event persistence and projection path. Each entry point applies its admission rules and protocol actions. Signed repository state stays pending until its referenced Git objects are available; promotion makes it visible and plans its optional follow-up work.

Callbacks and custom views are optional. A planning intent commits with an accepted event and captures its eligible registrations or failed local lookups. Recovery rechecks the source and current registrations, with at most three worker attempts and a 15-minute age limit. Stable child intent IDs preserve completed work. Remote delivery and transforms have their own retry policies. See callbacks and custom views.

Browser boundaries

Pages render without JavaScript. page.html owns the shell and panel dispatch; each page template owns one content section. Markup uses elements and IDs for styling in style.css. Shared form and data elements live in components.js, room interactions live in rooms.js, and signed publication uses tiny.signing. The signing contract verifies returned events before publication. Browser modules describe their entry points and shared services in their source headers.

Dependencies and checks

Go module versions are declared in go.mod, with dependency checksums in go.sum. Browser dependencies are locked in package-lock.json. make verify checks the Go and JavaScript code and verifies generated browser bundles against their sources. See Testing for local and Linux checks.