# Example config for geode. Section layout mirrors # nostr-rs-relay's config.toml so existing operators can port across. # # Run with: # ./gradlew :geode:run --args="--config /etc/geode.toml" # # CLI flags override individual values: e.g. `--port 8888` wins over # `[network].port`. [info] # The wss:// URL clients use to reach this relay (mandatory for NIP-42 # AUTH challenges). If not set, the relay synthesises one from the # [network] section. relay_url = "wss://relay.example.com/" name = "Example Geode" description = "A geode deployment." contact = "admin@example.com" # Operator pubkey (NIP-11). Optional. # pubkey = "..." # Override the supported NIPs advertised on the NIP-11 endpoint. If # omitted, the relay advertises the NIPs it actually implements. # supported_nips = [1, 9, 11, 40, 42, 45, 50, 62] [network] host = "0.0.0.0" port = 7447 path = "/" # Ktor CIO event-loop pool sizing. Leave commented-out for sensible # per-CPU defaults (typical for <2k concurrent connections). Lift on # big-VM deployments targeting 10k+ connections — over-threading at # low connection counts hurts L1/L2 cache locality, so always # benchmark before/after when tuning these. # # Operators targeting >1k concurrent WebSockets should also raise the # OS file-descriptor limit: `ulimit -n 65536` (or higher) before # launching, plus a matching `LimitNOFILE=` in any systemd unit. The # default of 1024 on most distros caps the relay well below 1k FDs # (one per WS plus DB and listening sockets). # connection_group_size = 4 # worker_group_size = 16 # call_group_size = 64 [database] # True keeps an in-memory SQLite db (events vanish on restart). Useful # for tests; set false + `file = "..."` for persistent storage. in_memory = false file = "/var/lib/geode/events.db" # NOTE: the four knobs below measured as pure noise in relayBench A/B # runs on 4-core container hardware at 50k events (see # geode/plans/2026-07-04-sqlite-knobs-ab.md) — their value is # hardware-dependent, so benchmark on YOUR box before enabling. # # Reader-connection pool size (file-backed stores only). Default 4. # readers = 4 # PRAGMA mmap_size in bytes — maps the db file into memory so reads # skip the pread syscall. Off by default (SQLite default). # mmap_size = 268435456 # PRAGMA temp_store = MEMORY — RAM instead of temp files for large # sorts. Off by default. # temp_store_memory = true # Refresh query-planner statistics (PRAGMA optimize) every N seconds. # Incremental and usually a no-op; keeps the planner from drifting # onto the wrong index as the corpus grows. Off by default. # optimize_interval_seconds = 3600 [options] # Drop events whose Schnorr signature does not verify. Strongly # recommended for any relay accepting traffic from real clients. # Verify Schnorr signatures on every EVENT. Default: true. Disable # only for trusted-input scenarios (test fixtures, mirror replays). verify_signatures = true # Run signature verification in parallel inside the IngestQueue # (across all CPU cores) instead of serially on each connection's # WebSocket pump. Default: true. Set false to fall back to the # legacy in-policy verify path. # parallel_verify = true # NIP-50 full-text search. Default: true. When false no FTS index is # built or maintained (inserts skip tokenization — a measurable share # of ingest cost), NIP-11 stops advertising NIP-50, and REQ filters # with a `search` term match nothing. CLI: --no-search. # full_text_search = true # Require clients to NIP-42 AUTH before REQ/EVENT/COUNT. require_auth = false # Advertise NIP-42 AUTH without requiring it: the relay sends the # challenge and records clients that authenticate (so downstream # policies can gate on identity), but REQ/EVENT/COUNT still work for # clients that never AUTH. Ignored when require_auth = true. # optional_auth = false # Reject events whose `created_at` is more than this many seconds in # the future. Enforced by RejectFutureEventsPolicy. # reject_future_seconds = 1800 [authorization] # Allow / deny lists. Allow is a permissive ceiling; deny still # removes specific entries inside it. Enforced by Pubkey/KindAllowDenyPolicy. # pubkey_whitelist = ["abcdef...64hex..."] # pubkey_blacklist = [] # kind_whitelist = [0, 1, 3, 7, 1059, 30023] # kind_blacklist = [4] # Mirror upstream relays (strfry-router style, "down" direction): the # relay dials each [[mirror]] url, subscribes to everything newer than # now - backfill_seconds, and ingests the stream alongside client # publishes. Reconnects and re-subscribes automatically. # # `trusted = true` is the relay-to-relay trust switch: events from that # upstream skip Schnorr signature verification (the upstream already # verified its own ingest; re-verifying burns ~8% of ingest CPU). The # trusted identity is the URL *this* relay dialed — TLS-authenticated # for wss:// — so an inbound client can never claim it. Default false: # mirror-but-verify. Only trust relays you operate or whose ingest # discipline you'd stake your own db on. # # `filter` (optional) scopes an upstream, as a NIP-01 filter JSON # object — same idea as strfry-router's per-stream filter. It shapes # the REQ sent upstream AND every delivered event is re-checked # against it before ingest, so even a trusted upstream can only # inject events inside the declared scope. `since`/`limit` inside it # are ignored (backfill_seconds owns the time window). Omit to mirror # everything; for several disjoint scopes, repeat [[mirror]] with the # same url. The keys are validated at boot (a typo like `kindss` or a # scalar where an array belongs fails startup) precisely because this # filter is the trust boundary for `trusted = true`. # # `dir` (strfry-router parity) sets the flow direction: "down" pulls # from the upstream (default), "up" pushes this relay's matching # events to it, "both" does both with echo suppression so the two # directions don't ping-pong the same event. # # [[mirror]] # url = "wss://upstream.example.com/" # dir = "both" # trusted = true # backfill_seconds = 3600 # filter = '{"kinds":[0,1,3,7],"#t":["nostr"]}' [admin] # NIP-86 relay management API. When `pubkeys` is non-empty, the relay # accepts HTTP POST application/nostr+json+rpc on the same URL, # authenticated with NIP-98 HTTP-Auth. Only events signed by one of # the listed pubkeys can run admin RPCs (banpubkey / banevent / # changerelayname / …). Empty (the default) disables the endpoint. # # NIP-98 binds admin tokens to the relay's HTTP URL, which is derived # from [info].relay_url with the scheme swapped (ws -> http, wss -> # https) per NIP-86. Make sure [info].relay_url is set to the # canonical public URL when behind TLS termination or a reverse proxy. # pubkeys = ["abcdef...64hex..."] # Path for the JSON snapshot that persists NIP-86 admin state (ban # lists + the live NIP-11 doc) across restarts. When unset, admin # state is in-memory only and forgotten on every restart. Convention # is to place this next to the SQLite event-store file. # state_file = "/var/lib/geode/events.db.admin.json"