Configuration

April 26, 2026 · View on GitHub

Environment variables

The following environment variables can be set:

NameDescriptionDefault
SECRETLong random secret.changeme
RELAY_PORTRelay's server port8008
RELAY_PRIVATE_KEYRelay's private key in hex(auto-generated)
WORKER_COUNTNumber of workers overrideNo. of available CPUs
DB_URIPostgreSQL URI (overrides DB_HOST, DB_PORT, etc.)
DB_HOSTPostgresSQL Hostname
DB_PORTPostgreSQL Port5432
DB_USERPostgreSQL Usernamenostr_ts_relay
DB_PASSWORDPostgreSQL Passwordnostr_ts_relay
DB_NAMEPostgreSQL Database namenostr_ts_relay
DB_MIN_POOL_SIZEMin. connections per worker16
DB_MAX_POOL_SIZEMax. connections per worker32
DB_ACQUIRE_CONNECTION_TIMEOUTNew connection timeout (ms)60000
READ_REPLICA_ENABLEDRead Replica (RR) Toggle'false'
READ_REPLICASNumber of read replicas (RR0, RR1, ..., RRn)2
RR0_DB_HOSTPostgresSQL Hostname (RR)
RR0_DB_PORTPostgreSQL Port (RR)5432
RR0_DB_USERPostgreSQL Username (RR)nostr_ts_relay
RR0_DB_PASSWORDPostgreSQL Password (RR)nostr_ts_relay
RR0_DB_NAMEPostgreSQL Database name (RR)nostr_ts_relay
RR0_DB_MIN_POOL_SIZEMin. connections per worker (RR)16
RR0_DB_MAX_POOL_SIZEMax. connections per worker (RR)32
RR0_DB_ACQUIRE_CONNECTION_TIMEOUTNew connection timeout (ms) (RR)60000
RR1_DB_HOSTPostgresSQL Hostname (RR)
RR1_DB_PORTPostgreSQL Port (RR)5432
RR1_DB_USERPostgreSQL Username (RR)nostr_ts_relay
RR1_DB_PASSWORDPostgreSQL Password (RR)nostr_ts_relay
RR1_DB_NAMEPostgreSQL Database name (RR)nostr_ts_relay
RR1_DB_MIN_POOL_SIZEMin. connections per worker (RR)16
RR1_DB_MAX_POOL_SIZEMax. connections per worker (RR)32
RR1_DB_ACQUIRE_CONNECTION_TIMEOUTNew connection timeout (ms) (RR)60000
RRn_DB_HOSTPostgresSQL Hostname (RR)
RRn_DB_PORTPostgreSQL Port (RR)5432
RRn_DB_USERPostgreSQL Username (RR)nostr_ts_relay
RRn_DB_PASSWORDPostgreSQL Password (RR)nostr_ts_relay
RRn_DB_NAMEPostgreSQL Database name (RR)nostr_ts_relay
RRn_DB_MIN_POOL_SIZEMin. connections per worker (RR)16
RRn_DB_MAX_POOL_SIZEMax. connections per worker (RR)32
RRn_DB_ACQUIRE_CONNECTION_TIMEOUTNew connection timeout (ms) (RR)60000
TOR_HOSTTor Hostname
TOR_CONTROL_PORTTor control Port9051
TOR_PASSWORDTor control passwordnostr_ts_relay
HIDDEN_SERVICE_PORTTor hidden service port80
REDIS_URIRedis URI (overrides REDIS_HOST, REDIS_PORT, etc.)
REDIS_HOST
REDIS_PORTRedis Port6379
REDIS_USERRedis Userdefault
REDIS_PASSWORDRedis Passwordnostr_ts_relay
NOSTR_CONFIG_DIRConfiguration directory<project_root>/.nostr/
DEBUGDebugging filter
ZEBEDEE_API_KEYZebedee Project API Key
NWC_URLNWC connection URL (nostr+walletconnect://...)

I2P

I2P support is provided as a sidecar container (i2pd) via docker-compose.i2p.yml, mirroring the Tor setup. No application-level environment variables are needed — the i2pd container creates an I2P server tunnel that forwards traffic to nostream's WebSocket port.

Configuration files live in the i2p/ directory:

FileDescription
i2p/tunnels.confDefines the I2P server tunnel pointing at nostream (port 8008).
i2p/i2pd.confMinimal i2pd daemon configuration.

Tunnel keys are persisted at .nostr/i2p/data/ so the .b32.i2p address survives container restarts.

The i2pd web console (tunnel status, .b32.i2p destinations) is published to the host on 127.0.0.1:7070 only. Remove the ports: mapping in docker-compose.i2p.yml to disable host-side access.

  • Start with I2P: nostream start --i2p

If you've set READ_REPLICAS to 4, you should configure RR0_ through RR3_.

Database indexes and benchmarking

The schema ships with a small, query-driven set of indexes. The most important ones for relay hot paths are:

IndexCovers
events_active_pubkey_kind_created_at_idxREQ with authors+kinds ordered by created_at DESC, event_id ASC; hasActiveRequestToVanish; by-pubkey deletes. Composite key (event_pubkey, event_kind, event_created_at DESC, event_id) so the ORDER BY tie-breaker is satisfied from the index without a sort step.
events_deleted_at_partial_idxRetention purge over soft-deleted rows. Partial on deleted_at IS NOT NULL.
invoices_pending_created_at_idxfindPendingInvoices poll (ORDER BY created_at ASC). Partial on status = 'pending'.
event_tags (tag_name, tag_value)NIP-01 generic tag filters (#e, #p, #K, #I, …) via the normalized event_tags table. Both lowercase and uppercase single-letter tag filters are supported.
events_event_created_at_indexTime-range scans (since / until).
events_event_kind_indexKind-only filters and purge kind-whitelist logic.

Run the read-only benchmark against your own database to confirm the planner is using the expected indexes and to record baseline latencies:

pnpm db:benchmark
pnpm db:benchmark --runs 5 --kind 1 --limit 500

The db:benchmark script loads the local .env file automatically (via node --env-file-if-exists=.env), using the same DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME variables as the relay. The benchmark issues only EXPLAIN (ANALYZE, BUFFERS) and SELECT statements — it never writes. Flags: --runs <n> (default 3), --kind <n> (default 1 / TEXT_NOTE; pass 0 for SET_METADATA), --limit <n> (default 500), --horizon-days <n> (default 7), --help.

For a full before/after proof of the index impact (seeds a throwaway dataset, drops and recreates the indexes, and prints a BEFORE/AFTER table), use:

pnpm db:verify-index-impact

The hot-path index migration (20260420_120000_add_hot_path_indexes.js) uses CREATE INDEX CONCURRENTLY, so it can be applied to a running relay without taking ACCESS EXCLUSIVE locks on the events or invoices tables.

Tag filter scope

Subscription filters support single-letter tag filters using the #<letter> key syntax (NIP-01). Both lowercase (#a#z) and uppercase (#A#Z) variants are accepted.

ScopeExamplesUsage
Lowercase (#a#z)#e, #p, #a, #kStandard NIP-01 tag queries; parent-level references in NIP-22 comment threading
Uppercase (#A#Z)#E, #P, #A, #K, #IRoot-level references in NIP-22 comment threading and other NIPs that use uppercase to distinguish root vs. parent scope

NIP-22 comment threading (kind 1111): NIP-22 comment events use lowercase tags (#e, #a, #i, #k) to reference the immediate parent and uppercase tags (#E, #A, #I, #K) to reference the root item. Filters must therefore accept both cases to allow clients to query the full comment thread hierarchy. For example, to find all comments on a root event: {"kinds":[1111],"#E":["<root-event-id>"]}, or to find comments of a specific root kind: {"kinds":[1111],"#K":["1"]}.

Settings

Running nostream for the first time creates the settings file in <project_root>/.nostr/settings.yaml. If the file is not created and an error is thrown ensure that the <project_root>/.nostr folder exists. The configuration directory can be changed by setting the NOSTR_CONFIG_DIR environment variable. nostream will pick up any changes to this settings file without needing to restart.

The settings below are listed in alphabetical order by name. Please keep this table sorted when adding new entries.

NameDescription
info.bannerPublic banner image URL for the relay information document.
info.contactRelay operator's contact. (e.g. mailto:operator@relay-your-domain.com)
info.descriptionPublic description of your relay. (e.g. Toronto Bitcoin Group Public Relay)
info.iconPublic icon image URL for the relay information document.
info.namePublic name of your relay. (e.g. TBG's Public Relay)
info.pubkeyRelay operator's Nostr pubkey in hex format.
info.relay_urlPublic-facing URL of your relay. (e.g. wss://relay.your-domain.com)
info.selfRelay pubkey in hex format for the relay information document self field.
info.terms_of_servicePublic URL to relay terms of service.
limits.admissionCheck.ipWhitelistList of IPs (IPv4 or IPv6) to ignore rate limits.
limits.admissionCheck.rateLimits[].periodRate limit period in milliseconds.
limits.admissionCheck.rateLimits[].rateMaximum number of admission checks during period.
limits.client.subscription.maxFiltersMaximum number of filters per subscription. Defaults to 10. Disabled when set to zero.
limits.client.subscription.maxSubscriptionsMaximum number of subscriptions per connected client. Defaults to 10. Disabled when set to zero.
limits.event.content[].kindsList of event kinds to apply limit. Use [min, max] for ranges. Optional.
limits.event.content[].maxLengthMaximum length of content. Defaults to 1 MB. Disabled when set to zero.
limits.event.createdAt.maxPositiveDeltaMaximum number of seconds an event's created_at can be in the future. Defaults to 900 (15 minutes). Disabled when set to zero.
limits.event.createdAt.minNegativeDeltaMaximum number of seconds an event's created_at can be in the past. Defaults to zero. Disabled when set to zero.
limits.event.eventId.minLeadingZeroBitsLeading zero bits required on every incoming event for proof of work. Defaults to zero. Disabled when set to zero.
limits.event.kind.blacklistList of event kinds to always reject. Leave empty to allow any.
limits.event.kind.whitelistList of event kinds to always allow. Leave empty to allow any.
limits.event.pubkey.blacklistList of public keys to always reject. Public keys in this list will not be able to post to this relay.
limits.event.pubkey.minLeadingZeroBitsLeading zero bits required on the public key of incoming events for proof of work. Defaults to zero. Disabled when set to zero.
limits.event.pubkey.whitelistList of public keys to always allow. Only public keys in this list will be able to post to this relay. Use for private relays.
limits.event.rateLimits[].kindsList of event kinds rate limited. Use [min, max] for ranges. Optional.
limits.event.rateLimits[].periodRate limiting period in milliseconds. For sliding_window: the time window during which requests are counted. For ewma: the half-life of the exponential decay — shorter values forget bursts faster, longer values are stricter on bursty clients.
limits.event.rateLimits[].rateMaximum number of events during period.
limits.event.retention.kind.whitelistEvent kinds excluded from retention purge. NIP-62 REQUEST_TO_VANISH is always excluded from retention purge, even if not listed here.
limits.event.retention.maxDaysMaximum number of days to retain events. Purge deletes events that are expired (expires_at), soft-deleted (deleted_at), or older than this window (created_at). Any non-positive value disables retention purge.
limits.event.retention.pubkey.whitelistPublic keys excluded from retention purge.
limits.event.whitelists.ipAddressesList of IPs (IPv4 or IPv6) to ignore rate limits.
limits.event.whitelists.pubkeysList of public keys to ignore rate limits.
limits.message.ipWhitelistList of IPs (IPv4 or IPv6) to ignore rate limits.
limits.message.rateLimits[].periodRate limit period in milliseconds.
limits.client.subscription.maxSubscriptionsMaximum number of subscriptions per connected client. Defaults to 10. Disabled when set to zero.
limits.client.subscription.maxFiltersMaximum number of filters per subscription. Defaults to 10. Disabled when set to zero.
limits.message.rateLimits[].periodRate limiting period in milliseconds. For sliding_window: the time window. For ewma: the half-life of the decay function.
limits.message.rateLimits[].rateMaximum number of messages during period.
mirroring.static[].addressAddress of mirrored relay. (e.g. ws://100.100.100.100:8008)
mirroring.static[].filtersSubscription filters used to mirror.
mirroring.static[].limits.eventEvent limit overrides for this mirror. See configurations under limits.event.
mirroring.static[].secretSecret to pass to relays. Nostream relays only. Optional.
mirroring.static[].skipAdmissionCheckDisable the admission fee check for events coming from this mirror.
network.maxPayloadSizeMaximum number of bytes accepted per WebSocket frame
network.remoteIpHeaderHTTP header from proxy containing IP address from client.
nip05.domainBlacklistList of domains blocked from NIP-05 verification. Authors with NIP-05 at these domains will be rejected.
nip05.domainWhitelistList of domains allowed for NIP-05 verification. If set, only authors verified at these domains can publish.
nip05.maxConsecutiveFailuresNumber of consecutive verification failures before giving up on an author. Defaults to 20.
nip05.modeNIP-05 verification mode: enabled requires verification, passive verifies without blocking, disabled does nothing. Defaults to disabled.
nip05.verifyExpirationTime in milliseconds before a successful NIP-05 verification expires and needs re-checking. Defaults to 604800000 (1 week).
nip05.verifyUpdateFrequencyMinimum interval in milliseconds between re-verification attempts for a given author. Defaults to 86400000 (24 hours).
nip45.enabledEnable or disable NIP-45 COUNT handling. Defaults to true.
paymentProcessors.lnbits.baseURLBase URL of your Lnbits instance.
paymentProcessors.lnbits.callbackBaseURLPublic-facing Nostream's Lnbits Callback URL. (e.g. https://relay.your-domain.com/callbacks/lnbits)
paymentProcessors.lnurl.invoiceURLLUD-06 Pay Request provider URL. (e.g. https://getalby.com/lnurlp/your-username)
paymentProcessors.zebedee.baseURLZebedee's API base URL.
paymentProcessors.zebedee.callbackBaseURLPublic-facing Nostream's Zebedee Callback URL (e.g. https://relay.your-domain.com/callbacks/zebedee)
paymentProcessors.zebedee.ipWhitelistList with Zebedee's API Production IPs. See ZBD API Documentation for more info.
payments.enabledEnabled payments. Defaults to false.
payments.feeSchedules.admission[].amountAdmission fee amount in msats.
payments.feeSchedules.admission[].enabledEnables admission fee. Defaults to false.
payments.feeSchedules.admission[].whitelists.event_kindsList of event kinds to waive admission fee. Use [min, max] for ranges.
payments.feeSchedules.admission[].whitelists.pubkeysList of pubkeys to waive admission fee.
payments.processorEither zebedee, lnbits, lnurl.
workers.countNumber of workers to spin up to handle incoming connections.
Spin workers as many CPUs are available when set to zero. Defaults to zero.
limits.message.ipWhitelistList of IPs (IPv4 or IPv6) to ignore rate limits.
limits.admissionCheck.rateLimits[].periodRate limiting period in milliseconds. For sliding_window: the time window. For ewma: the half-life of the decay function.
limits.admissionCheck.rateLimits[].rateMaximum number of admission checks during period.
limits.admissionCheck.ipWhitelistList of IPs (IPv4 or IPv6) to ignore rate limits.
limits.rateLimiter.strategyRate limiting strategy. Either ewma or sliding_window. Defaults to ewma. When using ewma, the period field in each rate limit serves as the half-life for the exponential decay function. Note: when switching from sliding_window to ewma, consider increasing rate values slightly as EWMA penalizes bursty behavior more aggressively.
mirroring.static[].addressAddress of mirrored relay. (e.g. ws://100.100.100.100:8008)
mirroring.static[].filtersSubscription filters used to mirror.
mirroring.static[].limits.eventEvent limit overrides for this mirror. See configurations under limits.event.
mirroring.static[].secretSecret to pass to relays. Nostream relays only. Optional.
mirroring.static[].skipAdmissionCheckDisable the admission fee check for events coming from this mirror.
network.maxPayloadSizeMaximum number of bytes accepted per WebSocket frame
network.remoteIpHeaderHTTP header from proxy containing IP address from client.
network.trustedProxiesOptional allow-list of proxy IPs allowed to set network.remoteIpHeader; otherwise socket remote IP is used.
nip05.domainBlacklistList of domains blocked from NIP-05 verification. Authors with NIP-05 at these domains will be rejected.
nip05.domainWhitelistList of domains allowed for NIP-05 verification. If set, only authors verified at these domains can publish.
nip05.maxConsecutiveFailuresNumber of consecutive verification failures before giving up on an author. Defaults to 20.
nip05.modeNIP-05 verification mode: enabled requires verification, passive verifies without blocking, disabled does nothing. Defaults to disabled.
nip05.verifyExpirationTime in milliseconds before a successful NIP-05 verification expires and needs re-checking. Defaults to 604800000 (1 week).
nip05.verifyUpdateFrequencyMinimum interval in milliseconds between re-verification attempts for a given author. Defaults to 86400000 (24 hours).
paymentProcessors.lnbits.baseURLBase URL of your Lnbits instance.
paymentProcessors.lnbits.callbackBaseURLPublic-facing Nostream's Lnbits Callback URL. (e.g. https://relay.your-domain.com/callbacks/lnbits)
paymentProcessors.lnurl.invoiceURLLUD-06 Pay Request provider URL. (e.g. https://getalby.com/lnurlp/your-username)
paymentProcessors.zebedee.baseURLZebedee's API base URL.
paymentProcessors.zebedee.callbackBaseURLPublic-facing Nostream's Zebedee Callback URL (e.g. https://relay.your-domain.com/callbacks/zebedee)
paymentProcessors.zebedee.ipWhitelistList with Zebedee's API Production IPs. See ZBD API Documentation for more info.
payments.enabledEnabled payments. Defaults to false.
payments.feeSchedules.admission[].amountAdmission fee amount in msats.
payments.feeSchedules.admission[].enabledEnables admission fee. Defaults to false.
payments.feeSchedules.admission[].whitelists.event_kindsList of event kinds to waive admission fee. Use [min, max] for ranges.
payments.feeSchedules.admission[].whitelists.pubkeysList of pubkeys to waive admission fee.
payments.processorEither zebedee, lnbits, lnurl, nodeless, opennode, nwc.
workers.countNumber of workers to spin up to handle incoming connections. Spin workers as many CPUs are available when set to zero. Defaults to zero.