Files
amethyst/geode/config.example.toml
T
Claude cc73639ae5 docs(geode): SQLite knobs A/B verdict — no winner, knobs stay off by default
Backlog items 4-5 close. Both-variants-in-one-run A/B at 50k, repeated
with relay order reversed: every delta flipped with the order (the
second-running relay won queries and ingest latency in BOTH runs), so
readers=8 / mmap_size=256MiB / temp_store=MEMORY / periodic PRAGMA
optimize are all noise-level on container-class hardware. The config
plumbing stays (hardware-dependent, operators should measure their own
box); the example config now says so explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtDNpayEYvJH7QuPswND3A
2026-07-04 00:50:37 +00:00

156 lines
6.4 KiB
TOML

# 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.
#
# [[mirror]]
# url = "wss://upstream.example.com/"
# 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"