No description
Find a file
ircforever 12c0af3f9d
Some checks failed
ci / core (push) Successful in 1m58s
ci / web (push) Successful in 47s
ci / image-core (push) Failing after 58s
ci / image-web (push) Failing after 42s
fix(client): reconcile nick so clients recognize the self-JOIN
If a client registered under a different nick than the upstream's, it kept
its own nick and didn't treat the synthesized ':<upnick> JOIN #chan' as a
self-join — so channels never opened. Send an explicit NICK change to the
upstream nick after the welcome burst when they differ.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:56:59 -04:00
.forgejo/workflows feat: working bouncer MVP — real connections + live web console 2026-06-21 09:09:42 -04:00
cmd feat: TLS listener, upstream SASL, and cross-session live fan-out 2026-06-21 10:40:26 -04:00
deploy feat: TLS listener, upstream SASL, and cross-session live fan-out 2026-06-21 10:40:26 -04:00
dev feat(dev): hot-reload docker stack; fix web image build 2026-06-21 08:35:58 -04:00
internal fix(client): reconcile nick so clients recognize the self-JOIN 2026-06-23 12:56:59 -04:00
web feat(web): private messages — list PM buffers and /msg to send 2026-06-21 10:50:57 -04:00
.dockerignore feat: scaffold evrd monorepo (core, cli, web) 2026-06-21 07:54:59 -04:00
.gitignore test(web): Playwright e2e suite against the live stack 2026-06-21 09:26:26 -04:00
docker-compose.dev.yml feat: TLS listener, upstream SASL, and cross-session live fan-out 2026-06-21 10:40:26 -04:00
docker-compose.yml feat(dev): hot-reload docker stack; fix web image build 2026-06-21 08:35:58 -04:00
Dockerfile feat: working bouncer MVP — real connections + live web console 2026-06-21 09:09:42 -04:00
Dockerfile.dev feat: working bouncer MVP — real connections + live web console 2026-06-21 09:09:42 -04:00
go.mod refactor(store): GORM behind a Store interface; SQLite or Postgres by DSN 2026-06-21 09:35:58 -04:00
go.sum refactor(store): GORM behind a Store interface; SQLite or Postgres by DSN 2026-06-21 09:35:58 -04:00
Makefile test(web): Playwright e2e suite against the live stack 2026-06-21 09:26:26 -04:00
README.md feat: TLS listener, upstream SASL, and cross-session live fan-out 2026-06-21 10:40:26 -04:00

evrd

The IRCForever IRC bouncer — stay connected, forever.

evrd keeps your IRC connection alive while you're away, buffers what you miss, and lets clients (native or web) attach and detach at will.

Layout

This is a monorepo with three parts:

Part Path What it is
core cmd/evrd, internal/ The bouncer daemon and its engine (IRC, sessions, storage).
cli cmd/evrctl Command-line admin for a running daemon.
spa web/ The Svelte web client (Vite + Svelte 5 + Tailwind).

cmd/evrd and cmd/evrctl are thin entrypoints; the real logic lives in internal/:

  • store — persistence (users, networks, buffered history) via GORM behind a Store interface. SQLite by default (pure-Go, no CGO); switch to Postgres by changing the DSN — db postgres://user:pass@host/evrd — no code change.
  • irc — protocol message parse/encode
  • upstream — one live IRC connection per network: dial, register, autojoin, reconnect, parse + persist + fan out messages, send
  • bouncer — owns the store and the set of running upstream connections
  • api — HTTP + WebSocket admin/client API consumed by the web app
  • client — downstream IRC listener: native clients (Hexchat, irssi, …) attach with a server password, get backlog replay, and a full-fidelity live feed bridged to/from the upstream

Develop

The whole stack — an upstream IRC server, evrd, and the web client — runs with Docker Compose, with hot reload on both services (air rebuilds the daemon on .go changes; vite HMR reloads the web client on source changes):

make up          # dev stack with hot reload
make down        # stop it

Create an account, then sign in on the web client and add a network:

docker compose -f docker-compose.dev.yml exec evrd \
  go run ./cmd/evrctl -db sqlite:/var/lib/evrd/evrd.db user add <name> <password>

In the dev stack, point a network at the internal upstream: host ircd, port 6667, TLS off.

Attaching a native IRC client

Point any IRC client at the bouncer and authenticate with a server password of <user>:<password> (or <user>/<network>:<password> to pick a network):

server:   localhost
port:     6697 (TLS, self-signed in dev — enable "accept invalid cert")
          6667 (plaintext)
password: bncuser:bncpass

On connect you get the welcome burst, your channels (with names/topic), backlog replay, and live traffic. Anything you send shows up in the web console and on your other attached clients too — all sessions share one upstream connection.

For a real TLS cert, set tls_cert / tls_key in the config. Upstream networks authenticate with SASL PLAIN when the network has a password and the server supports it (falling back to a server PASS).

The upstream ircd (ergo, a stand-in for the production ngircd-27) is internal-only — evrd reaches it as ircd:6667 over the compose network, so it never collides with a host port.

To run the built images instead (what CI publishes; web served by nginx on :8080):

make up-prod

End-to-end tests

Playwright specs in web/e2e drive the UI against the real daemon (login, add

  • connect a network, send a message in the console):
make e2e-setup   # one-time: install web deps + the Playwright browser
make e2e         # brings up the dev stack and runs the specs

Without Docker

make build       # builds bin/evrd and bin/evrctl
make web         # builds web/dist
./bin/evrd -version
cd web && pnpm dev

Status

Working bouncer. Create an account, add a network, and it really connects: the daemon dials the upstream, registers, autojoins, stays connected (reconnecting with backoff), and buffers history. Two ways to use it, sharing one upstream:

  • Web client — login, manage networks, live console.
  • Native IRC client — attach on 6697 (TLS) or 6667 with a server password; get backlog + a full-fidelity live feed (names, joins, topics, everyone's messages). Multiple clients + the web stay in sync over one upstream.

Upstreams authenticate via SASL PLAIN (or server PASS). The IRC listener does TLS (self-signed in dev, or your own cert).