Architecture

How evrd fits together

One Go process holds your IRC connections and serves your clients. Run it on its own with a file on disk, next to Postgres and ScyllaDB, or as a cluster of nodes that share the work and take over when one fails.

shapes 3failover cluster_lease 20ssource docs/scaling.md
01 · Components

One process, three kinds of storage

Native clients attach to the IRC listener, while the web app talks to the HTTP and WebSocket API. Both share the connection to each upstream network. Accounts go to a SQL database, chat history to a separate store, and metrics out over OpenTelemetry.

Clients
HexChatIRC · :6697 TLS
irssiIRC · :6697 TLS
Web appHTTPS + WebSocket
evrdcmd/evrd · one Go process
clientIRC listener, backlog replay
upstreamone connection per network
apiHTTP + WebSocket on :8098
bouncerruns every user's upstreams
storeaccounts, sessions, networks
historymessages and channel events
IRC networks
Libera.ChatSASL PLAIN · +6697
OFTC+6697
hackintreconnecting with backoff
Postgres / SQLitedb
ScyllaDB / Pebblehistory
VictoriaMetricsotel · optional
Client and network traffic Storage and metrics Reconnecting Hover or tab to a box to trace its connections

Where state lives

StateStored inShared by nodes?
Accounts, sessions, client passwords, networksPostgres (or SQLite)Yes with Postgres; SQLite is one node only
Chat historyScyllaDB (or Pebble)Yes with ScyllaDB; Pebble is one node only
Which node owns which userPostgres nodes, user_leasesYes
Live connections to IRC networksThe user's owner nodeForwarded
Attached IRC clients, web chat socketsThe user's owner nodeForwarded

Why history has its own store

Chat history never goes into Postgres. The history directive picks the backend:

  • ScyllaDB, shared by every node. Each buffer gets one partition per month, read newest first. Writes are queued (up to 16k lines), so a slow database doesn't stall IRC.
  • Pebble, an embedded store on local disk, for one evrd process per directory.

evrctl history copy <from> <to> copies history between them.

02 · Deployment shapes

Three ways to run it

Every shape uses the same binary. The db and history directives pick the backends. Nodes that share a Postgres share the users between them, and internal_listen (with cluster_secret, internal_url, internal_irc_listen and internal_irc_addr) lets them forward clients to each other.

Clients
IRC clients:6697 TLS
BrowserHTTPS
Web app
evrd-webnginx · static SPA
evrdone process · one disk
Accounts · SQLite/var/lib/evrd/evrd.db History · Pebble/var/lib/evrd/history/
Networks
Libera.Chat+6697
OFTC+6697
evrd.conf
listen     ircs://:6697
api        :8098
db         sqlite:/var/lib/evrd/evrd.db
history    pebble:/var/lib/evrd/history
public_url https://bnc.example.org
irc_host   bnc.example.org
Processesevrd, evrd-web
External servicesnone
Backup/var/lib/evrd
Failoverrestart the process

Good for a personal bouncer, a Raspberry Pi or a small VPS. Pebble allows one evrd process per directory.

03 · Request routing

Any node is a front door

In a cluster each user is owned by exactly one node, which runs their networks and serves their clients. A client can land on any node, and that node forwards it to the owner. Nodes need no message bus, only one extra hop.

Client
alice · HexChatPASS alice/libera:…
L4 balancer
Load balancerround-robin · TCP
Nodes
node-aowns alice
node-bpicked by the balancer
Network
Libera.Chatalice's upstream
Postgresnodes · user_leases
TCP :6697

The client connects to the balancer

The balancer passes IRC through as plain TCP, so TLS on 6697 terminates at evrd. It can't route by user, because IRC only reveals the username in PASS once the connection is already open.

04 · Failover

When a node goes away

Ownership is a lease in Postgres. Nodes heartbeat every 5 s and count as live if they beat within 15 s. Leases last 20 s. Step through a crash, or switch to a rolling deploy, where the node drains on SIGTERM instead.

Client
alice · HexChatauto-reconnect on
Balancer
Load balancerhealth checks · 2s connect
Nodes
node-aepoch 7
node-bnext in alice's order
node-cits own users
Network
Libera.Chatalice's upstream
Postgresuser_leases · TTL 20s
t = 0

Normal operation

node-a owns alice. It runs her Libera.Chat connection, serves her HexChat and renews her lease with one batched statement per heartbeat.

Fencing: a node that can't renew its leases, for example because it lost Postgres, stops its users' connections at half the TTL, before anyone else may take over. As long as the nodes' clocks agree, two nodes never run the same user. A short Postgres outage is better than split-brain.

05 · Outbound addresses

Give every user an address of their own

Large networks throttle and cap connections per source address. Without help, every user on a node would connect to Libera.Chat from that node's single address.

Node
node-aconnect_rate 5
upstream_prefix6 2001:db8:42::/64
alice2001:db8:42:0:9c3e:51a2:7d10:a1f3
bob2001:db8:42:0:41d7:e08b:2a6c:7c20
carol2001:db8:42:0:b85f:13c9:60ee:e911 · paced
Network
Libera.Chatsees 3 addresses
Addresses come from the 2001:db8::/32 documentation range. On a given node, a user always connects from the same address.
evrd.conf
connect_rate     5                       # most connects/second to one server, per node
upstream_bind    192.0.2.10 192.0.2.11   # spread users over these (stable per user)
upstream_prefix6 2001:db8:42::/64        # every user's own IPv6 address (IPv6 servers)
bindhost         192.0.2.20 vhost.example.org   # addresses users may pick

# a prefix needs Linux and a local route:
ip -6 route add local 2001:db8:42::/64 dev lo

What each one does

  • connect_rate spaces connects to each IRC server host per node, and reconnect backoff is jittered, so a takeover or netsplit recovery doesn't flood a network.
  • upstream_bind spreads users over a list of source addresses, with each user keeping the same one on that node. Per node (EVRD_UPSTREAM_BIND).
  • upstream_prefix6 gives every user their own IPv6 address from a routed prefix when connecting over IPv6 (IPv4 servers still use upstream_bind). Per node (EVRD_UPSTREAM_PREFIX6); needs Linux.
  • bindhost is ZNC's BindHost. It lists addresses users may choose from, and admins can lock a user's choice. Like connect_rate, it can also be set for every node at once in Admin → Settings.

Per-address caps on large networks are usually the first real limit. Talk to the networks you care about about limits for your ranges.

06 · Restarts

Upgrade without anyone leaving IRC

A connection to an IRC server lives in the process that opened it, so restarting a bouncer normally makes every user quit and rejoin every channel. On a single node, a second, much smaller process holds those connections instead, and evrd can come and go underneath it.

Clients
IRC clientsreconnect by themselves
Browserreconnects by itself
evrdrestarts, upgrades
Opens each connectionbind host, pacing, then hands over the socket History, clients, the web appeverything else, as before
Holder
evrd-linksTLS · PINGs · 4 MB buffer per network
Networks
Libera.Chatsees one connection
OFTCsees one connection
evrd and evrd-links talk over a unix socket (links_socket); connections are passed between them as file descriptors.

What happens on a restart

  1. evrd stops. It detaches from every connection instead of sending QUIT. The holder keeps them open, answers the servers' PINGs and buffers whatever arrives.
  2. The new evrd starts and asks the holder for each network's connection. It gets back the nick, the server's 005 lines and the buffered lines, so it skips registration and doesn't rejoin anything.
  3. It catches up: buffered messages go into history, and NAMES and TOPIC rebuild each channel. Clients reconnect to evrd and replay what they missed, as usual.

Switching a network off or deleting it still sends QUIT. If the holder isn't running, evrd connects directly, as it always has.

run the holder first, as root
evrd-links -socket /var/evrd/run/links.sock \
  -chroot /var/evrd -user _evrd

# evrd.conf
links_socket /var/evrd/run/links.sock

The holder is small and rarely changes, so it rarely needs a restart. Restarting it does drop the connections. The OpenBSD installer sets it up and restarts it only when a release changed it. It's for a single node. A cluster moves users between nodes instead (Failover).

07 · Choosing a setup

Start small, grow when you need to

The shapes differ only in configuration, so you can start with the simplest one and move up when you need to.

Single fileSingle node + databasesCluster
evrd processes112 or more
Accounts (db)SQLitePostgresPostgres, shared
History (history)PebbleScyllaDB or PebbleScyllaDB, shared
Load balancerNoneNonenginx or Traefik ≥ 3.7
A node failsDown until restartDown until restartUsers move in ≈ 20 s
Deploying a new versionRestart; with evrd-links, nobody leaves IRCRestart; with evrd-links, nobody leaves IRCRolling, drain on SIGTERM
Good forOne person, a Pi or a small VPSA group on one machineA community with no machine to lose
moving history to ScyllaDB
# stop evrd first when copying from a Pebble store
evrctl history copy pebble:/var/lib/evrd/history scylla://db1,db2/evrd

Before you go multi-node

  • History has to be shared, so clusters need ScyllaDB.
  • Every node needs the same secret_key and cluster_secret. Keep both out of database backups.
  • Leave settings_write_conf off, so the database is the one place admin settings live.
  • Keep the internal ports (:8099, :6699) on a private network.