Configuration Reference
The complete schema of ferrixd.toml. Every table and field, with types, defaults, and validation rules. For a guided walkthrough, see the configuration guide.
Unknown keys are errors
Configuration is fail-closed: every table rejects unknown keys. A typo prevents startup instead of being silently ignored. Validate with ferrixd check after every edit.
Conventions below: required fields have no default; everything else may be omitted. Durations are integer seconds; sizes are bytes.
[server] — required
Identity and listeners.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | advertised server name, used in numerics and message prefixes |
network | string | "ferrixnet" | network name (ISUPPORT NETWORK=) |
icon | string | unset | network icon URL (IRCv3 draft/network-icon), advertised as ISUPPORT draft/ICON=; use an HTTPS URL to a (square) image, optionally with a {size} template |
casemapping | "ascii" | "rfc1459" | "ascii" | nick/channel case folding; must be identical network-wide |
motd | array of string | [] | message of the day, one entry per line; REHASH-reloadable |
cloak_key | string | unset | HMAC key enabling host cloaking; omit to disable; keep secret and identical on linked servers |
sid | string | "42F" | server ID for S2S; unique per network |
link_bind | socket address | unset | inbound S2S listener, e.g. "0.0.0.0:6666" |
tls_bind | socket address | required | primary TLS listener, e.g. "0.0.0.0:6697" |
plain_bind | socket address | unset | plaintext listener; loopback-only unless allow_plain_nonlocal |
allow_plain_nonlocal | bool | false | permit a non-loopback plain_bind or ws_bind (e.g. behind a local TLS-terminating proxy) |
wss_bind | socket address | unset | secure WebSocket (wss://) listener; terminates TLS with the [tls] certificate, then negotiates the text.ircv3.net/binary.ircv3.net subprotocols |
ws_bind | socket address | unset | plaintext WebSocket (ws://) listener; loopback-only unless allow_plain_nonlocal (prefer wss_bind) |
password | string | unset | connection password: clients must send a matching PASS before registration (464 otherwise); REHASH-reloadable |
sts | table | unset | IRCv3 strict transport security policy: { port = 6697, duration = 2592000, preload = false }; plaintext connections are told the TLS port, TLS connections the duration (seconds; 0 clears the policy) |
Validation: a non-loopback plain_bind or ws_bind with allow_plain_nonlocal = false is a configuration error.
[tls] — required
| Field | Type | Default | Description |
|---|---|---|---|
cert | path | unset | PEM certificate chain (leaf first) |
key | path | unset | PEM private key |
self_signed_dev | bool | false | generate an ephemeral self-signed cert at startup — development only; ignored when cert/key are set |
dev_hostnames | array of string | ["localhost"] | SANs for the dev certificate |
Validation: cert and key must be set together; if neither is set, self_signed_dev must be true.
[limits] — optional
All fields optional; defaults shown. See also Limits & Defaults for the hardcoded constants.
| Field | Type | Default | Description |
|---|---|---|---|
max_tag_bytes | int | 8191 | wire budget for the message-tags section (IRCv3) |
max_body_bytes | int | 512 | wire budget for the message body (RFC 1459) |
max_line_bytes | int | 8704 | fatal frame length — longer frames drop the connection; must be ≥ max_tag_bytes + max_body_bytes |
registration_timeout_secs | int | 30 | seconds a connection may stay unregistered |
handshake_timeout_secs | int | 15 | TLS handshake budget |
ping_interval_secs | int | 120 | idle seconds before a server PING; a second missed interval disconnects |
max_clients_per_ip | int | 10 | simultaneous connections per source IP |
max_channels | int | 50 | channels per client (CHANLIMIT); opers exempt |
sendq_lines | int | 2048 | outbound queue depth in lines; overflow disconnects (SendQ exceeded) |
recv_burst | int | 20 | inbound token-bucket burst allowance |
recv_rate | int | 10 | sustained inbound commands/second; exhaustion disconnects (Excess Flood) |
history_len | int | 500 | retained messages per chathistory target; 0 = current-run only |
history_max_targets | int | 50000 | cap on distinct in-memory history targets; LRU-evicted beyond |
[[accounts]] — optional, repeatable
SASL seed accounts. See Accounts & SASL.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | account name |
password | string | unset | plaintext password, Argon2id-hashed at startup — development convenience |
password_hash | string | unset | precomputed Argon2 PHC string (ferrixd hash-password) — production |
scram | string | unset | precomputed SCRAM-SHA-256 credential (<iterations>:<b64 salt>:<b64 stored_key>:<b64 server_key>). Required for SCRAM logins on a password_hash account — the server never sees that account's plaintext and cannot derive them. ferrixd hash-password --toml prints password_hash and scram together; an account seeded with plaintext password gets SCRAM credentials automatically |
fingerprints | array of string | [] | permitted TLS client-cert SHA-256 fingerprints (lowercase hex) for SASL EXTERNAL |
REHASH-reloadable. Self-registered accounts (via REGISTER) are stored in [persistence] and merged on top.
[[operators]] — optional, repeatable
IRC operator credentials for OPER. See Operators & Moderation.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | operator name |
password | string | unset | plaintext (dev only) |
password_hash | string | unset | Argon2 PHC string (production) |
hosts | array of string | [] | hostmask globs (nick!user@host or bare IP) allowed to use this block; anywhere else gets 491 ERR_NOOPERHOST; empty = any host |
Validation: each operator needs password or password_hash. REHASH-reloadable.
[[bans]] — optional, repeatable
Startup K-lines, enforced at registration.
| Field | Type | Default | Description |
|---|---|---|---|
mask | string | required | nick!user@host glob (*, ?) |
reason | string | "Banned" | shown to the banned client |
REHASH-reloadable. Runtime additions: KLINE/DLINE (moderation guide).
[[webirc]] — optional, repeatable
Trusted WEBIRC gateways (IRCv3 WEBIRC). A web/IRC gateway may rewrite a client's apparent host and IP so users behind it are seen — and moderated — by their real address. Empty (the default) disables the WEBIRC command.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | gateway identifier, matched against the WEBIRC <gateway> parameter |
password | string | required | shared secret the gateway sends as WEBIRC <password>; compared in constant time — use a long random value |
hosts | array of string | required | source-address globs (e.g. "127.0.0.1", "10.0.0.*") the gateway may connect from |
A WEBIRC is accepted only when it is the connection's first command (before CAP/NICK/USER/PASS), the real peer address matches one of hosts, and the password matches; the rewritten IP is then re-checked against D-lines. Any failure closes the connection. REHASH-reloadable.
[persistence] — optional
SQLite durability for history, registered channels, and self-registered accounts. Omit for in-memory-only operation. See Message History.
| Field | Type | Default | Description |
|---|---|---|---|
path | path | required | SQLite database file (created if missing; WAL mode) |
load_limit | int | 5000 | most-recent history rows loaded into RAM at startup |
[metrics] — optional
| Field | Type | Default | Description |
|---|---|---|---|
bind | socket address | required | HTTP listener for /metrics; bind to loopback — the endpoint has no auth |
Catalogue: Metrics reference.
[plugins] — optional
WASM plugin host. See WASM Plugins.
| Field | Type | Default | Description |
|---|---|---|---|
dir | path | required | directory scanned for *.wasm at startup (sorted filename order) |
fuel | int | 5000000 | per-hook-call instruction budget |
max_memory | int | 16777216 | per-instance linear-memory cap, bytes |
expose_private_messages | bool | false | feed user-to-user DMs to the ferrix_on_private_message hook (a privacy decision — off unless you opt in) |
state_dir | path | unset | directory for host-managed per-plugin KV state files; unset → in-memory only |
grants | table | {} | per-plugin capability grants, plugin name → list of capability names (currently "send_notice"); deny-by-default |
[[links]] — optional, repeatable
S2S peer definitions. See Federation.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | peer's advertised server.name; must match at handshake |
connect | socket address | unset | peer's link_bind; omit for accept-only links |
fingerprint | string | required | peer TLS cert SHA-256, lowercase hex (ferrixd fingerprint) — pinned |
password | string | required | shared link secret, compared in constant time |
protocol | string | "ferrix" | wire protocol on this link: "ferrix" (native) or "ts6" (charybdis-family bridge, e.g. solanum) |
What REHASH reloads
| Reloadable at runtime | Restart required |
|---|---|
[[accounts]], [[operators]], [[bans]], [[webirc]], server.motd, server.password, [tls] certificate/key, [[links]] definitions | listener bind addresses, [limits], [persistence], [metrics], [plugins], sid, cloak_key, casemapping |
[tls] certificate/key reload is live for every TLS listener (no dropped connections; a bad PEM leaves the previous material armed). REHASH refreshes the [[links]] definitions so operator CONNECT sees edits, but it does not start or stop the boot-time auto-dial loops — use CONNECT/SQUIT to bring newly added or removed links up or down.
Complete annotated example
The exact file ferrixd gen-config writes:
# ferrixd example configuration.
#
# Copy to `ferrixd.toml` and adjust (or run `ferrixd gen-config` to write this
# file for you). Validate any changes with `ferrixd check` before restarting.
# The parser rejects unknown keys (fail-closed configuration), so a
# typo is an error, not a silently-ignored setting.
#
# Helper subcommands for the values below:
# ferrixd gen-cert -> a self-signed [tls] cert + key (dev)
# ferrixd hash-password -> an Argon2id `password_hash` for accounts/operators
# ferrixd fingerprint -> a cert fingerprint for [[links]] / SASL EXTERNAL
[server]
# Advertised server name (used in numerics / source prefixes).
name = "irc.example.test"
# Advertised network name (ISUPPORT NETWORK).
network = "ferrixnet"
# Optional network icon URL (IRCv3 draft/network-icon), advertised as the
# `draft/ICON` ISUPPORT token. Use an HTTPS URL to a (preferably square) image;
# a literal `{size}` in the URL is passed through for clients to substitute.
# icon = "https://example.org/icon.svg"
# Network-wide case folding for nicks/channels: "ascii" (default) or "rfc1459".
casemapping = "ascii"
# HMAC key enabling host cloaking (omit to disable). Keep this secret — it makes
# cloaks unforgeable. Authenticated users are cloaked as account.<network>.
# cloak_key = "change-this-to-a-long-random-secret"
# Server id (SID) and optional inbound S2S link listener.
sid = "42F"
# link_bind = "0.0.0.0:6666"
# Message of the day, one entry per line (optional).
motd = [
"Welcome to ferrixd.",
"Be excellent to each other.",
]
# TLS is the primary, mandatory transport.
tls_bind = "0.0.0.0:6697"
# Plaintext is disabled by default. Uncomment ONLY for local testing;
# binding to a non-loopback address will be rejected unless you also set
# `allow_plain_nonlocal = true` below.
# plain_bind = "127.0.0.1:6667"
# allow_plain_nonlocal = false
# IRC over WebSockets (IRCv3). `wss_bind` reuses the [tls] certificate and is the
# recommended form for browser clients. `ws_bind` is plaintext and — like
# `plain_bind` — loopback-only unless `allow_plain_nonlocal = true`.
# wss_bind = "0.0.0.0:443"
# ws_bind = "127.0.0.1:8080"
# Optional connection password: clients must send `PASS <password>` before
# completing registration (rejected with 464 otherwise).
# password = "change-me"
# Optional IRCv3 strict transport security (`sts`) policy. Plaintext clients
# are told the TLS port to reconnect to; TLS clients are told to remember the
# policy for `duration` seconds (0 clears it).
# sts = { port = 6697, duration = 2592000, preload = false }
[tls]
# Option A — provide a real certificate chain and private key (PEM):
# cert = "/etc/ferrixd/fullchain.pem"
# key = "/etc/ferrixd/privkey.pem"
# Option B — generate an ephemeral self-signed certificate at startup.
# Convenient for development; NEVER use in production. Ignored if cert/key
# are set above.
self_signed_dev = true
dev_hostnames = ["localhost", "irc.example.test"]
[limits]
# Wire-length budgets. Kept separate for tags and body per IRCv3.
max_tag_bytes = 8191
max_body_bytes = 512
# Fatal frame length before the connection is dropped (DoS guard).
# Must be >= max_tag_bytes + max_body_bytes plus a little slack.
max_line_bytes = 8704
# Seconds a connection may stay unregistered before it is closed (slot guard).
registration_timeout_secs = 30
# Seconds allowed for the TLS handshake before the attempt is aborted.
handshake_timeout_secs = 15
# Retained messages per target for chathistory (0 keeps only the current run).
history_len = 500
# Cap on the number of distinct in-memory history targets (channels + DM pairs).
# Bounds total history memory regardless of activity; least-recently-active
# targets are evicted past this. Persisted rows (if enabled) are unaffected.
history_max_targets = 50000
# Idle seconds before the server PINGs a quiet client (disconnects on a 2nd miss).
ping_interval_secs = 120
# --- DoS controls ---
# Maximum simultaneous connections from one source IP (connection throttling).
max_clients_per_ip = 10
# Maximum channels a single client may be in at once (advertised as CHANLIMIT).
max_channels = 50
# Outbound SendQ depth in lines; a client whose queue overflows is dropped.
sendq_lines = 2048
# Inbound command burst allowance, then sustained rate/sec (token bucket).
# Exceeding this disconnects the client with "Excess Flood".
recv_burst = 20
recv_rate = 10
# --- SASL seed accounts ---
# Each account may authenticate via SASL PLAIN (password) and/or EXTERNAL
# (TLS client-certificate fingerprint).
#
# [[accounts]]
# name = "alice"
# # Development convenience: plaintext, hashed with Argon2id at startup. This
# # form also derives the account's SCRAM-SHA-256 credentials automatically.
# password = "change-me"
# # Preferred in production: a pre-computed Argon2 PHC hash instead of `password`.
# # The server never sees the plaintext, so SCRAM credentials cannot be derived
# # from it — supply them explicitly for SASL SCRAM-SHA-256. Both lines are
# # printed together by `ferrixd hash-password --toml`.
# # password_hash = "$argon2id$v=19$m=19456,t=2,p=1$...$..."
# # scram = "4096:<b64 salt>:<b64 stored_key>:<b64 server_key>"
# # SASL EXTERNAL: permitted client-cert SHA-256 fingerprints (lowercase hex).
# # fingerprints = ["a1b2c3..."]
# --- IRC operators (used by the OPER command) ---
# [[operators]]
# name = "admin"
# password = "change-me" # or password_hash = "$argon2id$..."
# # Optional hostmask allowlist for this block: `nick!user@host` globs or bare
# # IPs. OPER from anywhere else is refused with 491. Empty/omitted = any host.
# # hosts = ["*!*@203.0.113.7", "*!*@*.trusted.example"]
# --- Startup server bans (K-Lines), matched at registration ---
# [[bans]]
# mask = "*!*@203.0.113.0/24" # nick!user@host glob (* and ?)
# reason = "Banned network"
# --- Trusted WEBIRC gateways (IRCv3 WEBIRC) ---
# A web/IRC gateway may rewrite a client's apparent host and IP so users behind
# it are seen (and moderated) by their real address. A gateway must present the
# shared password AND connect from one of the `hosts` globs; both are checked
# (the password in constant time) before any rewrite. Empty = WEBIRC disabled.
# [[webirc]]
# name = "kiwi" # matched against the WEBIRC <gateway> field
# password = "long-random-shared-secret"
# hosts = ["127.0.0.1", "10.0.0.*"] # source-address globs the gateway may use
# --- Durable persistence (SQLite; optional) ---
# When set, chathistory AND channel registrations survive restarts (they share
# the same database file). Omit for in-memory-only state.
# [persistence]
# path = "/var/lib/ferrixd/ferrixd.db"
# load_limit = 5000 # recent messages to load into RAM at startup
# --- Prometheus metrics endpoint (optional; bind to loopback) ---
# [metrics]
# bind = "127.0.0.1:9090" # scrape at http://127.0.0.1:9090/metrics
# --- WASM plugin host (optional) ---
# Loads every *.wasm file in `dir` at startup. Plugins are sandboxed and run
# under a per-call fuel (instruction) budget; a plugin's on_message hook may
# block channel messages. See crates/ferrixd/src/plugin.rs for the ABI.
# [plugins]
# dir = "/etc/ferrixd/plugins"
# fuel = 5000000 # per-hook-call instruction budget
# max_memory = 16777216 # per-instance linear-memory cap (bytes)
# expose_private_messages = false # feed DMs to plugins (privacy: your call)
# state_dir = "/var/lib/ferrixd/plugin-state" # persist plugin KV stores
#
# Capability grants (deny-by-default): plugin file stem -> capabilities.
# [plugins.grants]
# "20-modbot" = ["send_notice"]
# --- S2S peer links ---
# Trust is the pinned TLS certificate fingerprint plus the shared password.
# Set server.link_bind above to accept inbound links.
# Links refuse to form cycles: keep the topology a tree.
# [[links]]
# name = "irc.peer.test"
# connect = "peer.example:6666" # omit for accept-only links
# fingerprint = "a1b2c3…" # peer cert SHA-256 (lowercase hex)
# password = "shared-link-secret"
# protocol = "ferrix" # or "ts6" to bridge a charybdis-family
# # IRCd (solanum, …) into the network