Documentation

Run evrd

Install it, attach your clients, and scale from one node to a cluster.

15 sectionssource evrd/docsconfig evrd.confadmin evrctl
Get started

Quick start #

evrd keeps your IRC connection alive while you're away, buffers what you miss, and lets clients (native or web) attach and detach whenever you like. The quickest way to run it is with the published container images: ghcr.io/4evrd/evrd (the daemon and evrctl) and ghcr.io/4evrd/evrd-web (the web app on nginx).

  1. Make a secret key

    It encrypts the network passwords evrd saves (SASL and server passwords) and secrets set in Admin → Settings. Keep a copy somewhere safe; without it they can't be read back.

    shell
    echo "EVRD_SECRET_KEY=$(docker run --rm --entrypoint evrctl ghcr.io/4evrd/evrd genkey)" > .env
  2. Write a config file

    A single node needs no external database: SQLite holds accounts and an embedded Pebble store holds history.

    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
    cookie_secure no   # until TLS is in front — then remove this line
  3. Start evrd and the web app

    The web image proxies /api and /auth to evrd-api:8098, so name the daemon's service that (or point the web container at it with EVRD_API=host:port).

    docker-compose.yml
    services:
      evrd-api:
        image: ghcr.io/4evrd/evrd:latest
        command: ["-config", "/etc/evrd/evrd.conf"]
        environment:
          EVRD_SECRET_KEY: ${EVRD_SECRET_KEY}   # from .env
        volumes:
          - ./evrd.conf:/etc/evrd/evrd.conf:ro
          - evrd-data:/var/lib/evrd
        ports:
          - "6697:6697"   # IRC — clients attach here
        restart: unless-stopped
      evrd-web:
        image: ghcr.io/4evrd/evrd-web:latest
        ports:
          - "8080:80"     # web app; put TLS in front of it
        restart: unless-stopped
    volumes:
      evrd-data:
  4. Create the first admin
    shell
    docker compose exec evrd-api evrctl -config /etc/evrd/evrd.conf \
      user add -admin james 'a-long-password'
  5. Sign in and add a network

    Open the web app (port 8080, or your public_url once a TLS proxy is in front), sign in, and add a network such as irc.libera.chat on +6697. evrd dials it, registers, autojoins your channels and starts buffering. Then attach a native client.

Passwords need at least 10 characters. A password an admin sets in the web app (Admin → Users) is temporary: the user picks their own at next sign-in. Passwords set with evrctl aren't. Once TLS is in front of the web app, remove cookie_secure no so the session cookie is HTTPS-only. For Postgres and metrics, the repository's deploy/docker setup has them ready.

Get started

Install on OpenBSD #

evrd runs natively on OpenBSD (amd64 and arm64), without Docker. One script installs it as a confined system daemon, with SQLite and the embedded Pebble history store, so there's no database server to run. One command, as root (ftp is OpenBSD's built-in downloader, so no curl is needed):

shell
ftp -o - https://raw.githubusercontent.com/4evrd/evrd/main/deploy/openbsd/install.sh | sh

The questions come up at your terminal even though the script arrives through a pipe. Not root? Use | doas sh. It downloads the latest release for your machine, checks it against the release's SHA256 file, and asks:

QuestionDefaultAnswers
Host name for the web appthe machine's namee.g. bnc.example.org
Certificateletsencrypt for a public nameletsencrypt (automatic, renewed), selfsigned, or none (plain HTTP, for trying it out)
Who serves HTTPSevrdevrd itself on 443 and 80, or relayd in front (with httpd and acme-client)
HTTPS port, Let's Encrypt email443, none
IRC host name and portsthe web host, 6697 (TLS), no plain port
Single sign-onnonean OpenID Connect issuer (e.g. Logto's https://id.example.org/oidc), client ID, secret (not echoed), button label and admin role. It shows the redirect URI to register: <web app>/auth/callback
Admin username and passwordnonethe first admin account

Then it starts evrd and prints the address. Open the ports in pf (/usr/local/share/examples/evrd/pf.conf has the rules). For Let's Encrypt, the names must already resolve to the machine, with ports 80 and 443 reachable. Every question has an option (-d host, -c certificate, -w evrd|relayd, -o SSO issuer, -a admin, …), passed as | sh -s -- -y -d bnc.example.org …. -y takes the defaults, for provisioning without a terminal. Run the same command again to upgrade: it keeps the config and the key. evrd restarts only if its program changed (a release that only touches the web app goes live without one), and even then nobody leaves IRC: the connection holder, evrd-links, keeps everyone's connections open while evrd restarts. Certificate renewals reload evrd (rcctl reload evrd) instead of restarting it.

How evrd is confined

rc.d starts evrd as root. It binds its ports and reads /etc/evrd/evrd.conf, the secret key and any certificate files, then:

  1. chroots to /var/evrd, so the rest of the filesystem is gone;
  2. becomes _evrd. The chroot and everything in it but data/ belong to root, so evrd can't change its own web app or name resolution;
  3. unveils only its data, the web app and resolv.conf / hosts (read-only);
  4. pledges stdio rpath wpath cpath fattr flock inet dns unix sendfd: it can't execute anything or open other kinds of sockets (unix sendfd hands connections to the holder).

evrd-links is confined the same way but tighter: chrooted to /var/evrd as _evrd, it pledges only stdio inet unix recvfd and can't open any files.

PathWhat
/etc/evrd/evrd.confthe config (root:_evrd, 0640)
/etc/evrd/secret.keyencrypts saved network passwords and certificate keys. Back it up, apart from the data
/var/evrd/data/the database and history (_evrd only)
/var/evrd/web/the web app, served by evrd (web_root)
/etc/rc.d/evrdrcctl start|stop|restart|reload evrd; logs go to /var/log/daemon
/etc/rc.d/evrd_linksthe connection holder, started before evrd. Its socket is /var/evrd/run/links.sock. Restarting it drops the IRC connections, so the installer only does that when a release changed it

The same confinement works anywhere evrd starts as root, with chroot and user in evrd.conf (unveil and pledge are OpenBSD's). The full guide is in the repository: docs/openbsd.md.

Get started

Build from source #

evrd is pure Go (no cgo), so it cross-compiles anywhere Go 1.26 runs. The web client is a Svelte 5 + Vite app built with pnpm.

shell
git clone https://github.com/4evrd/evrd.git && cd evrd
make build       # bin/evrd and bin/evrctl
make web         # web/build
./bin/evrd -version
./bin/evrd -config evrd.conf

Development stack

make up runs the whole stack with hot reload: an upstream ergo IRC server, two evrd nodes behind nginx, Postgres, ScyllaDB, VictoriaMetrics and the web client on http://localhost:5173. make e2e runs the Playwright suite against it.

Use evrd

Web client #

The web app shows your networks, channels and private messages with unread counts, mentions, members and topics, and stays in sync with every attached IRC client. Anything you send there shows up in your other clients too, because all sessions share one upstream connection.

The chat input understands /join, /part, /msg, /query, /me, /notice, /nick, /topic, /mode, /kick, /close, /quote and /help, and runs *status commands with /status help (all but jump, playback and detach, which are for IRC clients).

Use evrd

Native IRC clients #

Point HexChat, irssi, WeeChat or any other IRC client at the bouncer and authenticate with a server password:

Server passwordAttaches to
user:passwordyour first network (the oldest one you added)
user/network:passworda specific network

The password is either your account password or an IRC client password made on the web app's IRC clients page. Client passwords are the only option for accounts that sign in with SSO, and you can revoke them one client at a time.

irssi
/connect -tls bnc.example.org 6697 james/libera:a-long-password

What you get on connect

  • The welcome burst, your channels with names and topic, backlog replay, then live traffic.
  • /join and /part update the network's saved channel list, and /nick changes your nick upstream.
  • Your own notices and /me actions are saved and shown on your other clients.
  • Capabilities echo-message and server-time.

Channel events in backlog

Joins, parts, quits, kicks, nick changes, topics and modes are saved alongside messages. On replay they come from *buffextras as plain text, for example bob is now known as robert, the same way ZNC's buffextras module does it, so they never confuse a client's nick list.

Port 6667 is plaintext. Use 6697 with TLS anywhere outside a private network, and set tls_cert / tls_key for a real certificate.

Use evrd

*status commands #

/msg *status help lists the bouncer's own commands. ZNC's names work too: listnetworks, jumpnetwork, addnet, delnet, listchans, shownetwork, setnetwork and the …TrustedServerFingerprint commands.

CommandDoes
networksYour networks, their status, and which one this client is on
jump <network>Switch this client to another network
connect [network]Bring a network up (default: this one)
disconnect [network]Bring a network down and stop reconnecting it (default: this one)
addnetwork <name> <host> [[+]port] [nick]Add a network; +port is TLS (default +6697)
delnetwork <network>Remove a network and its history (not the one this client is on)
show [network]A network's settings
set [network] <setting> <value>Change name, nick, host / port / tls (of the first server), ident, realname, password and the settings below; - clears a text setting. Reconnects when needed
channelsThis network's saved channels
playback <channel|nick> [lines]Replay a buffer (default 50, up to 500)
info / versionYour account and limits / the evrd version
listserversThis network's servers, in failover order
addserver <host> [[+]port] [password]Add a server, tried in turn when the link drops; +port is TLS (default +6697)
delserver <host> [port]Remove a server (not the only one)
listfingerprintsCertificate fingerprints trusted for this network
addfingerprint <sha-256> / delfingerprint <sha-256>Trust, or stop trusting, a server certificate
detach <#channel>[,…]Hide channels from IRC clients; evrd stays in them and keeps logging. /join brings them back (clients can also send /detach #chan)

Servers, certificates and tuning

listservers, addserver, delserver, listfingerprints, addfingerprint, delfingerprint (and ZNC's AddTrustedServerFingerprint names), detach, and set for altnick, sasluser, bindhost, quitmsg, encoding, trustallcerts, trustpki, floodrate, floodburst, joindelay.

Use evrd

Network settings #

Each network has its own page in the web app (/networks/<id>) with everything ZNC offers without modules.

  • Servers: several, each with host, port, TLS and an optional server password. evrd connects to the first and moves on to the next with each reconnect attempt.
  • Identity: nick, alternate nick, ident, real name, quit message and bind host. Empty fields use the account defaults.
  • Authentication: SASL PLAIN account and password, sent as PASS if the network has no SASL.
  • Certificates: verify with system CAs (TrustPKI), trust everything (TrustAllCerts), or trusted SHA-256 fingerprints. When a certificate is refused, its fingerprint is shown with a button to trust it.
  • Channels: key, detached (joined and logged but hidden from IRC clients until they /join), join-on-connect.
  • Advanced: encoding for non-UTF-8 networks, flood protection (lines per second and burst), join delay.

Per-user settings

Under Account → Bouncer: default ident, quit message and bind host; how many lines to play back, and whether channels and private messages only replay what arrived since you were last attached; timestamps for clients without server-time; fixed CTCP replies; and the addresses allowed to sign in.

Operate

Configuration #

evrd reads one plain-text file of directive value lines (a space between them); pass it with evrd -config evrd.conf or set EVRD_CONFIG. A # starts a comment, at the start of a line or after a space. These are the directives you'll set first:

DirectiveExamplePurpose
listenircs://:6697IRC listener; repeat for irc+insecure://:6667
api:8098HTTP + WebSocket API for the web app
dbpostgres://…/evrdAccounts and settings; sqlite:/path for single-file installs
historyscylla://h1,h2:9042/evrdChat history; pebble:/path for one node
tlsacmeWhere certificates come from: acme (Let's Encrypt), selfsigned or files (tls_cert / tls_key). See HTTPS and certificates
https_listen:443evrd serves the web app over HTTPS itself (with http_listen :80 to redirect)
web_root/var/lib/evrd/webThe built web app, for evrd to serve without nginx
chroot / user/var/evrd / _evrdConfine evrd after startup (see OpenBSD)
links_socket/var/evrd/run/links.sockThe connection holder's socket: IRC connections stay up while evrd restarts (single node)
public_urlhttps://bnc.example.orgWeb app address (and SSO callback)
irc_hostbnc.example.orgHostname shown to users for attaching
max_networks0Per-user default, 0 = unlimited
max_clients0Attached IRC clients at once, 0 = unlimited
backlog_lines2000Kept per channel or query (0 keeps everything), pruned every 10 minutes

History backends

Chat history never goes in Postgres. ScyllaDB is shared by every node, with one partition per buffer per month and queued writes (up to 16k lines) so a slow database doesn't stall IRC. Pebble is an embedded store on local disk for single-node installs. evrctl history copy <from> <to> copies history between them.

Operate

HTTPS and certificates #

evrd's own TLS ports (the ircs:// IRC listeners, and the web app when evrd serves HTTPS itself) share one certificate source. Set it with tls, or switch it while evrd runs under Admin → Settings → HTTPS & TLS:

tlsCertificate
acmeLet's Encrypt: evrd gets one for each name in acme_hosts (default: public_url's host and irc_host) on first use, and renews it 30 days before it expires. Picking it accepts the CA's subscriber agreement.
selfsignedMade once and kept. Browsers and IRC clients ask you to accept it. The default without tls_cert.
filestls_cert and tls_key (PEM, with the chain) from certbot, acme-client or your own CA, re-read when the file changes and on SIGHUP (rcctl reload evrd), without a restart.

Certificates evrd gets or makes, and its ACME account, are kept in the database, encrypted with secret_key. Every node of a cluster serves the same ones, and nothing is written to disk.

Who serves HTTPS

evrd itself: set https_listen and http_listen, and web_root to the built web app. No proxy or web server is needed. Port 80 redirects to HTTPS and answers Let's Encrypt's http-01 challenge, and 443 answers tls-alpn-01.

evrd.conf
public_url   https://bnc.example.org
irc_host     bnc.example.org
https_listen :443
http_listen  :80                     # redirects, and Let's Encrypt's challenges
web_root     /var/lib/evrd/web       # the release archive's web/
tls          acme
acme_email   ops@example.org         # optional: expiry notices

A proxy in front (nginx, Caddy, Traefik, relayd, a cloud load balancer): leave https_listen unset. The proxy terminates TLS and forwards to api, including the WebSocket, and evrd's certificate is then only for the IRC ports. This is how the Docker images run.

The settings page says which of the two is in use, and shows each certificate's issuer, names and expiry, or why getting one failed. Saving a choice that can't work (Let's Encrypt for localhost, say) is refused.

Operate

Accounts and sign-in #

Every user has a role (user or admin), per-user limits and optional suspension. Web sessions are stored in the database behind an HttpOnly cookie, so they survive restarts, and users can see and revoke them on their Account page.

SSO with OpenID Connect

Use Logto or any OIDC provider; SAML and social logins are connectors in the provider. Accounts are created on first sign-in and linked by the token's subject.

evrd.conf
public_url         https://bnc.example.org   # callback: <public_url>/auth/callback
oidc_issuer        https://id.example.org/oidc
oidc_client_id     <app id>
# secret: oidc_client_secret, or EVRD_OIDC_CLIENT_SECRET
oidc_label         IRCforever account
oidc_auto_create   yes
oidc_link_existing no
oidc_admin_role    evrd-admin

Provisioning API

For tools that bring users in from elsewhere, such as moving ZNC users over, evrd can create an account and its networks before the owner ever signs in. It's off until provision_token is set (or EVRD_PROVISION_TOKEN), and every call sends that token:

shell
curl -X PUT http://127.0.0.1:8098/api/provision/users/alice \
  -H "Authorization: Bearer $EVRD_PROVISION_TOKEN" \
  -d '{"email": "alice@example.org", "connect": true,
       "networks": [{"name": "libera", "host": "irc.libera.chat", "port": 6697,
                     "tls": true, "nick": "alice", "channels": ["#evrd"]}]}'

Networks take the same fields as the web app's network editor. Each one comes back as created, exists (so a call can be repeated) or failed. connect connects every listed network, so a migration can create everything, take the old bouncer off IRC, then call again to connect. A new account has no password and is claimable: the first SSO sign-in with the same username and email links to it. Keep the token secret and the API on a local address.

Suspending a user signs them out everywhere, drops their IRC clients and disconnects their networks; lifting the suspension reconnects them. The last active admin can't be demoted, suspended or deleted.

Operate

Server settings #

Admins can change the general, limits, connection (connect_rate, bindhost), HTTPS & TLS, sign-in, single sign-on and metrics settings from Admin → Settings. Each value comes from, in order (later wins):

  1. built-in defaults
  2. evrd.conf
  3. the database: values set on the Settings page, shared by every node
  4. the environment on that node (EVRD_OIDC_CLIENT_SECRET, EVRD_METRICS_URL, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT)

Most settings apply live on every node within a few seconds. public_url, the SSO provider, metrics export and the certificate file paths are marked Restart needed. With the connection holder running, that restart doesn't take anyone off IRC. Startup and per-node settings (listen, api, https_listen, http_listen, web_root, chroot, user, db, history, cluster settings, upstream_bind / upstream_prefix6, secret keys, settings_write_conf) stay in the file or environment.

settings_write_conf yes writes values set in the UI back into each node's own config file; the file's directory must be writable by evrd (not a read-only single-file mount). Use it on a single node; in cluster mode, leave it off and let the database be the one place these settings live.

Operate

Saved passwords #

Network passwords (the SASL password and server passwords) and the values set in Admin → Settings (such as the SSO client secret) are encrypted in the database with AES-256-GCM under a key that lives outside it. Account and client passwords are bcrypt hashes either way.

evrd.conf
# make one with: evrctl genkey
secret_key      <32 bytes, base64>   # or EVRD_SECRET_KEY, or secret_key_file /path
secret_key_old  <previous key>       # optional: still decrypts while rotating

Rotating the key

Set the new secret_key, move the old one to secret_key_old, restart, then remove the old key. At startup evrd encrypts anything saved before a key was set and re-encrypts anything under an old key, network passwords and settings alike. Every node needs the same key; keep it out of database backups.

Operate

Running several nodes #

Run any number of nodes against the same Postgres and ScyllaDB, behind a plain round-robin load balancer for IRC (TCP passthrough) and the API (HTTP + WebSocket). Each user is owned by one node, which runs their network connections and serves their clients; any other node forwards that user's traffic to it.

If a node dies, its users move to the others in about 20 seconds (cluster_lease); on shutdown a node hands them over within one tick (5 s).

evrd.conf · every node
internal_listen      :8099   # internal HTTP — between nodes only
internal_irc_listen  :6699   # internal IRC — between nodes only
history              scylla://db1,db2/evrd
environment · per node
EVRD_NODE_ID=node-a
EVRD_INTERNAL_URL=http://evrd-a.internal:8099
EVRD_INTERNAL_IRC_ADDR=evrd-a.internal:6699
EVRD_CLUSTER_SECRET=<the same long random secret on every node>

See the architecture page for diagrams of each deployment shape, request routing and failover.

The repository ships a tested nginx config (deploy/lb.conf) and a Traefik equivalent (deploy/traefik/, Traefik ≥ 3.7 for TCP health checks). With internal_listen set, all four of cluster_secret, internal_url, internal_irc_listen and internal_irc_addr are required (as directives, or the environment variables above). A lone node without internal_listen still works and owns every user.

Operate

Connections to networks #

Large networks throttle and cap connections per source address, and every user on a node would otherwise share the node's 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        # each user's own IPv6 address (IPv6 servers)
bindhost         192.0.2.20 vhost.example.org

A prefix needs Linux and a local route: ip -6 route add local 2001:db8:42::/64 dev lo. bindhost lists addresses users may pick to connect from, like ZNC's BindHost. Admins can use any address and can lock a user's choice.

Operate

Metrics #

evrd exports OpenTelemetry metrics over OTLP/HTTP and reads them back from VictoriaMetrics for the dashboard's traffic chart and health numbers.

evrd.conf
otel_endpoint http://victoriametrics:8428/opentelemetry/v1/metrics
otel_interval 15s
metrics_url   http://victoriametrics:8428

Run VictoriaMetrics with -opentelemetry.usePrometheusNaming to get evrd_irc_lines_total, evrd_upstream_up, evrd_upstream_lag_seconds, evrd_irc_clients, evrd_cluster_users, evrd_history_queue and friends. Without metrics_url the dashboard still works, and the chart says metrics are off.

Behind a proxy with basic auth, put the credentials in both URLs (https://user:pass@vm.example.org). evrd sends them as basic auth, and Admin → Settings shows the password masked.

Reference

evrctl #

Command-line admin for a running install: evrctl [-config file | -db dsn] <command>. -config defaults to $EVRD_CONFIG; it writes to the database directly.

CommandDoes
versionPrint the evrctl version
genkeyPrint a new secret_key
user add [-admin] [-email e] <name> [password]Create an account (no password = SSO or client passwords only)
user listList accounts
user passwd <name> <password>Set an account's password
user role <name> user|adminChange an account's role
user suspend <name> [reason]Suspend an account (signs them out; their node drops their connections within a few seconds)
user unsuspend <name>Lift a suspension
user delete <name>Delete an account and everything it owns
history copy <from> <to>Copy all chat history between stores (stop evrd first when copying from Pebble)
Reference

Releases and versions #

Releases are cut automatically by semantic-release from Conventional Commits on main. Each release is tagged vX.Y.Z, gets GitHub release notes grouped by type, and tags the images :X.Y.Z and :X.Y. Every push to main also publishes :latest and :sha-<commit>. evrd -version reports the version.

CommitRelease
fix: …, perf: …, revert: …patch
feat: …minor
feat!: … or BREAKING CHANGE:major

evrd stays at 0.x until a breaking change says otherwise. Get the latest release →