Configuration
ferrixd reads a single TOML file — ./ferrixd.toml by default, or wherever -c/--config points. This page is a guided tour; the complete field-by-field schema lives in the configuration reference.
The workflow
ferrixd gen-config # 1. scaffold a commented ferrixd.toml
$EDITOR ferrixd.toml # 2. edit
ferrixd check # 3. validate config AND TLS material, print a summary
ferrixd # 4. run (equivalent to `ferrixd run`)ferrixd check is not a syntax check only — it loads the certificates and keys, so a broken PEM file fails here, not at 3 a.m. during a restart.
Fail-closed configuration
The parser rejects unknown keys. If you typo moddt = […], the server refuses to start instead of silently ignoring your MOTD. This is deliberate: a config that loads is a config that means what you think it means.
A minimal real server
[server]
name = "irc.example.org" # advertised server name (numerics, prefixes)
network = "examplenet" # ISUPPORT NETWORK=
tls_bind = "0.0.0.0:6697"
[tls]
cert = "/etc/ferrixd/fullchain.pem"
key = "/etc/ferrixd/privkey.pem"That's everything a working TLS-only server needs. Everything else on this page is opt-in.
Section by section
[server] — identity and listeners
[server]
name = "irc.example.org"
network = "examplenet"
casemapping = "ascii" # or "rfc1459"; must match network-wide
motd = [
"Welcome to examplenet.",
"Be excellent to each other.",
]
tls_bind = "0.0.0.0:6697"casemappingdecides how nicks and channel names fold (asciiis the modern default;rfc1459treats{}|^as[]\~). Pick one per network and don't change it with users online.motdis a list of lines, no file needed. Reloadable withREHASH.icon(optional) advertises a network icon URL to clients that support it (IRCv3draft/network-icon, sent asISUPPORT draft/ICON=):
icon = "https://examplenet.org/icon.svg"Plaintext, if you really need it, is loopback-only by design:
plain_bind = "127.0.0.1:6667"
# binding plain_bind to a non-loopback address is a config ERROR unless:
# allow_plain_nonlocal = trueWebSockets let browser clients connect natively (no gateway):
wss_bind = "0.0.0.0:443" # secure; terminates TLS with the [tls] cert
ws_bind = "127.0.0.1:8080" # plaintext; loopback-only (same rule as plain_bind)wss_bind is the browser-facing form and reuses the [tls] certificate (and its REHASH reloads). Each IRC line is one WebSocket message; the server negotiates the text.ircv3.net/binary.ircv3.net subprotocols. See TLS → WebSocket transport.
Host cloaking hides user hostnames behind an unforgeable HMAC:
cloak_key = "a-long-random-secret-keep-this-private"See Operators & Moderation for what cloaks look like and how bans interact with them.
Federation identity (only needed when linking servers):
sid = "42F" # unique server id across the network
link_bind = "0.0.0.0:6666" # inbound S2S listener (omit for connect-only)[tls] — certificates
[tls]
cert = "/etc/ferrixd/fullchain.pem"
key = "/etc/ferrixd/privkey.pem"
# or, for development only:
# self_signed_dev = true
# dev_hostnames = ["localhost", "irc.example.test"]cert/key must be set together. If neither is set, self_signed_dev must be true. Full details, Let's Encrypt notes, and the built-in gen-cert helper: TLS Certificates.
[limits] — budgets and DoS controls
Every limit has a sensible default; the section is optional. The interesting knobs:
[limits]
registration_timeout_secs = 30 # unregistered connections are dropped after this
handshake_timeout_secs = 15 # TLS handshake budget
ping_interval_secs = 120 # idle PING; 2nd miss disconnects
max_clients_per_ip = 10 # per-IP connection throttle
max_channels = 50 # per-client channel cap (opers exempt)
sendq_lines = 2048 # outbound queue depth; overflow = disconnect
recv_burst = 20 # inbound token bucket: burst…
recv_rate = 10 # …and sustained commands/sec
history_len = 500 # retained messages per chathistory target
history_max_targets = 50000 # bound on distinct in-memory history targetsWire-length budgets (max_tag_bytes, max_body_bytes, max_line_bytes) default to the IRCv3 values — leave them alone unless you know why you're changing them. The full table with all defaults: Limits & Defaults.
[[accounts]] — seed accounts for SASL
[[accounts]]
name = "alice"
password_hash = "$argon2id$v=19$m=19456,t=2,p=1$…$…" # ferrixd hash-password
fingerprints = ["a1b2c3…"] # SASL EXTERNAL (optional)Plaintext password = "…" also works (hashed with Argon2id at startup) but belongs in development only. Users can also self-register at runtime with REGISTER, and those accounts persist if [persistence] is enabled. All of this: Accounts & SASL.
[[operators]] — IRC operators
[[operators]]
name = "admin"
password_hash = "$argon2id$…"Grants access to OPER, and through it KILL, K/D/G-lines, WALLOPS, CHGHOST, and REHASH. See Operators & Moderation.
[[bans]] — startup K-lines
[[bans]]
mask = "*!*@203.0.113.0/24"
reason = "Banned network"Matched at registration; reloadable with REHASH.
[[webirc]] — trusted WEBIRC gateways
Let a web/IRC gateway present a client's real host and IP (so users behind it are seen and moderated by their own address, not the gateway's):
[[webirc]]
name = "kiwi" # matched against the WEBIRC <gateway> field
password = "long-random-shared-secret"
hosts = ["127.0.0.1", "10.0.0.*"] # source addresses the gateway may useA WEBIRC is honoured only as the connection's first command, from an allow-listed source, with a matching secret (compared in constant time); the spoofed IP is then re-checked against D-lines. Reloadable with REHASH.
[persistence] — SQLite durability
[persistence]
path = "/var/lib/ferrixd/ferrixd.db"
load_limit = 5000 # recent messages loaded into RAM at startupOne file, three jobs: chathistory rows, registered channels, and self-registered accounts all live here and survive restarts — including msgid continuity. Omit the section for in-memory-only operation. Details: Message History.
[metrics] — Prometheus endpoint
[metrics]
bind = "127.0.0.1:9090" # scrape http://127.0.0.1:9090/metricsBind it to loopback (or a private interface) — there is no auth on the endpoint. Metric catalogue: Metrics.
[plugins] — WASM plugin host
[plugins]
dir = "/etc/ferrixd/plugins" # every *.wasm in here is loaded at startup
fuel = 5000000 # per-call instruction budgetSee WASM Plugins.
[[links]] — S2S peers
[[links]]
name = "irc2.example.org"
connect = "irc2.example.org:6666" # omit for accept-only
fingerprint = "a1b2c3…" # peer cert SHA-256 (ferrixd fingerprint)
password = "shared-link-secret"See Federation for the full linking walkthrough.
Reloading without restarting
REHASH (oper-only) re-reads the config file and applies the reloadable subset — accounts, operators, bans, the MOTD, WEBIRC gateways, the connection password, and the TLS certificate/key — without dropping a single connection. Listener bind addresses and limits still require a restart. See what REHASH reloads and Operators & Moderation.
Validating in CI
ferrixd check exits non-zero on any problem, which makes it a natural pre-deploy gate:
ferrixd -c deploy/ferrixd.toml check