Basics
The configuration is a TOML file, by
default named nostrfy.toml. Create it with init:
nostrfy --config nostrfy.toml init
Validate it (recommended before every start):
nostrfy --config nostrfy.toml check
Every command takes --config <path> (default nostrfy.toml).
General syntax:
[section]
key = "string"
key = 8080
key = [1, 2]
key = true
Configuration sections
| Section | Purpose |
|---|
[relay] | Identity, URLs and NIP toggles |
[server] | Network binding, API split, metrics |
[rpc] | NIP-86 management RPC (auth, body limit) |
[limits] | All limits and overload protections |
[database] | LMDB storage, search index, queue caps |
[daemon] | PID, log and stats files and rotation |
[access] | Initial access control lists (changeable at runtime) |
[blossom] | Blossom file server (media hosting) |
Every key is optional; a missing key uses its default.
Section [relay] — relay identity
| Key | Type | Default | Description |
|---|
name | string | "nostrfy" | Relay name shown to clients via NIP-11 |
description | string | "A minimal and stable Nostr relay" | Relay description (NIP-11) |
pubkey | string (64 hex) | "" | Administrator public key (NIP-11 pubkey field) |
contact | string | "" | Administrator contact URI (mailto: or https://) |
icon | string | "" | Relay icon image URL |
post_policy | string | "" | URL pointing to the relay's posting policy |
private_key | string (64 hex) | "" | The relay's own secret key; required for NIP-29 groups |
public_url | string | "" | Public URL, e.g. wss://relay.example.com |
livekit_url | string | "" | LiveKit server URL for NIP-29 audio/video rooms |
livekit_api_key | string | "" | LiveKit API key |
livekit_api_secret | string | "" | LiveKit API secret (used to sign JWTs) |
enabled_nips | array of integers | [] | Explicit NIP allowlist |
disabled_nips | array of integers | [] | NIPs to disable (ignored when enabled_nips is non-empty) |
reject_ephemeral | boolean | false | Reject NIP-01 ephemeral events (kinds 20000-29999) |
enabled_git | boolean | false | Accept NIP-34 git events (kinds 1617-1633, 30617/30618) |
require_pow | integer | 0 | Required proof-of-work in leading zero bits |
new_pubkey_min_age_secs | integer | 0 | Refuse events from pubkeys younger than this (seconds; 0 = off) |
max_events_per_min_per_pubkey | integer | 0 | Publish rate limit per pubkey (per minute; 0 = no limit) |
max_groups | integer | 1000 | Cap on the in-memory NIP-29 group store |
require_auth | boolean | false | Require NIP-42 authentication for REQ/EVENT/COUNT/NEG |
send_auth_challenge | boolean | true | Send the AUTH challenge on connect |
enabled_nip78_auth | boolean | true | Require NIP-42 AUTH before accepting kind 78/30078 events |
enabled_command_events | boolean | false | Execute kind:1 operator commands authored by the admin pubkey |
Key details
- private_key — the relay's own secret key, used to sign relay-generated events: NIP-29 group
metadata (39000-39005) and NIP-43 role/membership events. Generate with
nostrfy genkey; keep it
secret. It is read once at startup, so changing it requires a restart. - public_url — used to validate URL-bearing tags from clients: NIP-42 AUTH, NIP-62 vanish and
NIP-98 admin auth. When empty, the relay falls back to
host:port, which never matches a real
client URL when binding 0.0.0.0 or 127.0.0.1 (a warning is logged). Always set it. - enabled_nips vs disabled_nips — the allowlist wins: when
enabled_nips is
non-empty, only its NIPs are advertised and disabled_nips is ignored. Both require a restart. - reject_ephemeral — kinds 20000-29999 are rejected, but the exempt kinds NIPs require to be
relayed are still forwarded: 22242, 27235, 28934/28935/28936, 24133, 23194/23195, 24242 and 21059. Applies on
SIGHUP.
- enabled_git — opt-in NIP-34: accepts kinds 1617-1633 and 30617/30618 and advertises NIP-34.
Off by default because patch payloads can be large. Applies on SIGHUP.
Section [server] — server settings
| Key | Type | Default | Description |
|---|
host | string | "127.0.0.1" | Bind address; 0.0.0.0 accepts connections from anywhere |
port | integer | 8080 | Port (1-65535); port 80 requires root |
api_host | string | "" | Hostname dedicated to the REST API |
metrics_enabled | boolean | true | Serve Prometheus metrics at /metrics |
ws_paths | string | "root" | WebSocket endpoint paths: root, inbox-outbox or all |
inbox_write_policy | string | "any" | Who may write to /inbox: "any" or "relay" (events must still carry a p tag) |
outbox_write_policy | string | "any" | Who may write to /outbox: "any" (the NIP-42-authenticated pubkey's own events) or "relay" |
Key details
- host —
0.0.0.0 binds all IPv4 interfaces; 127.0.0.1 is local only. - port — 1-65535; port 80 requires root. This one port serves the WebSocket relay, the NIP-11 document, the REST API and the NIP-86 RPC together.
- api_host — dedicates the REST API to a single hostname so the API and the relay can share one
port behind a reverse proxy. Fixed at startup — requires a restart.
- ws_paths —
root serves / only, inbox-outbox serves /inbox and
/outbox only, all serves both. Fixed at startup — requires a restart.
Section [rpc] — NIP-86 management
| Key | Type | Default | Description |
|---|
management_token | string | "" | Bearer token for the management APIs |
admin_pubkey | string (64 hex) | "" | Administrator pubkey for NIP-98 management auth |
max_admin_body_bytes | integer | 65536 | Body limit for the NIP-86 management RPC |
Section [limits] — limits and protections
Connections and messages
| Key | Type | Default | Description |
|---|
max_connections | integer | 10000 | Maximum concurrent connections |
max_connections_per_ip | integer | 64 | Max connections per source IP |
max_ws_message_bytes | integer | 1048576 | Max bytes per WebSocket message/frame |
socket_recv_buffer_kb | integer | 64 | Per-connection kernel receive buffer (KiB) |
max_out_queue_bytes | integer | 262144 | Per-connection outgoing queue cap (bytes) |
ws_idle_timeout_secs | integer | 300 | Close idle connections after this long |
http_read_timeout_secs | integer | 30 | HTTP request-head timeout (slow-loris defense) |
max_connections_per_sec_per_ip | integer | 0 | Max new connections per second per source IP |
Subscriptions and queries
| Key | Type | Default | Description |
|---|
max_filters | integer | 20 | Max filters per REQ |
max_subscriptions | integer | 20 | Max subscriptions per connection |
max_limit | integer | 500 | Ceiling for the REQ limit |
max_count | integer | 2000 | Ceiling for COUNT results |
max_sub_id_len | integer | 64 | Max subscription id length |
max_sub_bytes | integer | 1048576 | Total subscription filter bytes per connection |
max_req_response_bytes | integer | 33554432 (32 MB) | Ceiling for the total bytes a single REQ response may send |
Events
| Key | Type | Default | Description |
|---|
max_content_bytes | integer | 65536 | Max event content length in characters |
max_tags | integer | 2000 | Max tags per event |
max_tag_value_bytes | integer | 1024 | Max bytes per tag value |
max_created_at_future_secs | integer | 3600 | Tolerated future skew of created_at |
group_late_publish_secs | integer | 3600 | Tolerated lag for NIP-29 group admin events (seconds) |
max_neg_items | integer | 100000 | Max records per NIP-77 negentropy sync |
REST API
| Key | Type | Default | Description |
|---|
max_api_concurrent | integer | 8 | Max concurrent /api/v1 requests |
max_api_limit | integer | 5000 | Ceiling for the API limit parameter |
max_api_offset | integer | 50000 | Ceiling for the API offset parameter |
max_api_search_bytes | integer | 2048 | Max bytes of the API search parameter |
Live fan-out
| Key | Type | Default | Description |
|---|
live_batch_interval_ms | integer | 20 | How often live events are flushed (ms) |
live_batch_size | integer | 32 | Max events per live batch |
live_buffer | integer | 65536 | Live fan-out queue size |
Section [database] — database
| Key | Type | Default | Description |
|---|
path | string | "./data" | Database directory (LMDB) |
max_dbs | integer | 32 | LMDB max named databases |
max_readers | integer | 128 | LMDB max concurrent readers |
map_size | integer | 1073741824 (1 GB) | Floor for the memory map size (bytes) |
max_map_size | integer | 1099511627776 (1 TB) | Memory-map ceiling (bytes) |
purge_interval_secs | integer | 300 | NIP-40 purge interval (seconds) |
search_index | boolean | true | Enable the NIP-50 word index |
reader_threads | integer | 2 | Dedicated scan threads |
max_indexed_words | integer | 32 | Words of each event's content indexed for search |
meta_index | boolean | true | Write the per-event metadata header used by the scan prefilter |
disabled_fsync | boolean | false | Skip the synchronous disk flush after every write batch |
db_buffer_size | integer | 2048 | Initial per-connection WebSocket buffer (bytes) |
db_request_timeout_secs | integer | 30 | How long a database request may wait before failing |
max_db_queue_msgs | integer | 4096 | Max queued pending messages before failing fast |
max_db_queue_events | integer | 262144 | Max events inside queued batches before failing fast |
Key details
- map_size — the floor for the memory map: the map is always opened at least this large.
- max_map_size — the ceiling, opened as a sparse virtual reservation: physical disk grows only
with the data actually written. Raise it when you hit
database map is full. - search_index = false — search still works (whole-word matching against content) but scans
get slower; on a tiny VPS it halves the database. Recommended on small instances.
- disabled_fsync — trades durability for throughput: writes commit into the OS page cache and
a power loss may lose the most recent writes.
Section [daemon] — daemon
| Key | Type | Default | Description |
|---|
pid_file | string | "./nostrfy.pid" | PID file path |
log_file | string | "./nostrfy.log" | Log file path |
stats_file | string | "./nostrfy.stats.json" | Statistics file path |
stats_interval_secs | integer | 5 | Statistics write interval (seconds) |
max_log_size_bytes | integer | 52428800 (50 MB) | Log rotation size (0 = no rotation) |
max_log_files | integer | 5 | Rotated log generations to keep |
Paths are resolved against the config file's directory, so they stay valid after the daemon changes its working directory.
Section [access] — access control
| Key | Type | Default | Description |
|---|
restrict_relay | boolean | false | Only allow-listed pubkeys may publish |
blocked_kinds | array of integers | [] | Kinds to reject |
allowed_kinds | array of integers | [] | Kind allowlist; only these kinds are accepted when non-empty |
blocked_ips | array of strings | [] | IP addresses refused at connection time |
The pubkey allow/deny lists are not config keys — they live in the relay database (LMDB) and are
managed at runtime:
nostrfy relay allow npub1...
nostrfy relay deny npub1...
nostrfy relay list
- restrict_relay = true — only the allow-listed pubkeys may publish, while
reading stays open to everyone (any client can still subscribe and fetch).
- A denied pubkey is always rejected when publishing and never served when reading.
Section [blossom] — Blossom file server
| Key | Type | Default | Description |
|---|
host | string | "" | Hostname for the Blossom server (empty = disabled) |
storage | string | "local" | Backend: "local" (local_path) or "s3" (S3-compatible bucket) |
local_path | string | "/var/lib/nostrfy/images" | Local storage root for media files |
max_upload_bytes | integer | 20971520 (20 MB) | Max media file size |
min_free_bytes | integer | 33554432 (32 MB) | Disk space below which uploads are refused |
s3_endpoint | string | "" | S3-compatible endpoint (e.g. R2) |
s3_region | string | "" | S3 region (R2 uses "auto") |
s3_bucket | string | "" | S3 bucket name |
s3_access_key | string | "" | S3 access key |
s3_secret_key | string | "" | S3 secret key |
restrict_uploads | boolean | false | Only allow-listed pubkeys may upload |
Reloading at runtime (SIGHUP)
Editing the file and sending kill -HUP $(cat nostrfy.pid) reloads the config without a
restart. Most settings take effect immediately; a few are fixed at startup:
| Applies on SIGHUP | Requires restart |
|---|
| relay.name, description, pubkey, contact, icon, post_policy, public_url | relay.private_key |
| reject_ephemeral, enabled_git, enabled_nip78_auth | relay.livekit_*, enabled_nips / disabled_nips |
| most of [limits] | api_host, metrics_enabled, ws_paths, database.*, daemon sizes, limit caps, blossom.* |
[access] is not applied by a reload — the lists are seeded once at startup and then managed at runtime via NIP-86.
The log warns when a restart-required setting changed, and a few startup-captured settings are not checked by the
reload.
| Mistake | Fix |
|---|
| public_url unset | set wss://... |
| host left 127.0.0.1 | external clients can't connect |
| private_key unset with NIP-29 | run nostrfy genkey + restart |
| restrict_relay true with empty allowlist | everyone locked out |
| changing restart-only keys and only SIGHUPing | use nostrfy restart |