IRC community server with a web chat client and API-backed authentication. Ergo IRCd + Node.js API + Preact client. Users register on the web; their account is created in both the API DB and Ergo (via oper). The web client connects to IRC through the API bouncer; traditional IRC clients connect directly to Ergo. No SASL required — connect then identify with NickServ.
- Node.js 20+ (for local scripts: migrate, seed)
- Docker and Docker Compose
- Yarn (or npm)
Note: These instructions target Ubuntu 24.04. Other distros may need different setup.
Use the default Ubuntu packages:
sudo apt install docker.io docker-compose-v2 docker-buildx-plugin
sudo usermod -aG docker $USERUse the default Debian packages (names differ from Ubuntu):
sudo apt install docker.io docker-cli docker-compose docker-buildx
sudo usermod -aG docker $USERUse the docker-compose command (with hyphen) for Compose; the rest of this doc uses docker compose (space), which is equivalent.
Log out and back in after adding yourself to the docker group. If you see permission denied connecting to the Docker socket, add your user: sudo usermod -aG docker $USER, then log out and back in (or run newgrp docker in your shell).
Not used for local development — use Development (yarn dev, docker-compose.dev.yml). setup.sh is only for deploying the Caddy + production stack on a server.
Recommended: run the interactive installer from the repo root (requires Node + Yarn on the host):
chmod +x setup.sh update.sh # once
./setup.shThat writes .env, TLS material, ergo/ircd.yaml (via yarn generate-config), builds the chat client into srv/client/ and the admin app into srv/admin/, writes Caddyfile from deploy/Caddyfile.template, and brings up docker-compose.yml (Caddy, API, Ergo, cert-sync). Updates: ./update.sh.
Full operator notes (DNS, firewall, first-boot order, manual deploy without setup.sh): docs/deployment.md.
AWS: optional deploy/aws/ CloudFormation stack (EC2 + Elastic IP + security group); see deploy/aws/README.md.
Local development does not use setup.sh or root docker-compose.yml. Use yarn dev (or docker compose -f docker-compose.dev.yml …) and .env.dev — see Quick start below.
Follow-ups and polish backlog: docs/polish-list.md.
# 1. Install dependencies
yarn install
# 2. Create .env.dev (copy from template, edit ADMIN_NICK to your preferred first-admin nick)
cp .env.dev.example .env.dev
# Edit .env.dev: set ADMIN_NICK=yournick (the nick you'll register with)
# 3. Generate self-signed TLS certs and oper client cert
yarn dev:certs
# Copy the printed SHA-256 fingerprint into ergo/ircd.dev.yaml:
# opers.anemoia-api.certfp = "YOUR_FINGERPRINT_HERE"
# 4. Start Ergo + API
yarn dev:infra
# 5. Open the web chat at http://localhost:5173 and register (use the nick you set in ADMIN_NICK)
# 6. Restart the dev stack so the admin bootstrap runs
# (Ctrl+C, then yarn dev:infra again — or: docker compose -f docker-compose.dev.yml restart dev)Alternative: seed test users — If you prefer seeded accounts instead of registering:
yarn seed:docker # Creates admin, alice, bob, charlie (password: password)Then set ADMIN_NICK=admin in .env.dev and restart. The seed creates admin with role=admin, so ADMIN_NICK bootstrap won't run — but you'll have an admin account ready.
First admin via ADMIN_NICK: If you register (not seed), set ADMIN_NICK=yournick in .env.dev before step 4. After registering, restart the dev container so the bootstrap promotes your user to admin.
| Port | Service |
|---|---|
| 3000 | API |
| 5173 | Web chat client (Vite dev server) |
| 6667 | IRC (plaintext) |
| 6697 | IRC (TLS, self-signed) |
API docs: With the API running, OpenAPI + Swagger UI are at http://localhost:3000/api/docs (machine JSON: /api/docs/json). The listed server URL uses DOMAIN, OPENAPI_SERVER_URL, or http://127.0.0.1:$PORT — set OPENAPI_SERVER_URL if “Try it out” should hit a different origin.
SMTP (e.g. Gmail + app password for email_code): docs/smtp.md.
Server: localhost (or 127.0.0.1)
Port: 6667 (plaintext) or 6697 (TLS)
Auth: No SASL. Connect with your nick, then identify: /msg NickServ IDENTIFY yourpassword
HexChat: Add server localhost/6697 (SSL). Do not enable SASL. After connect, send /msg NickServ IDENTIFY password (or your password).
WeeChat: /server add anemoia localhost/6667 (or -ssl for 6697) → /connect anemoia → /msg NickServ IDENTIFY password
New users: Register on the web app first; your nick and password are created in both the API and Ergo. Then connect to IRC and /msg NickServ IDENTIFY yourpassword.
Command is IDENTIFY (with a Y), not IDENTITY. Use one argument (your password); your current nick is used automatically.
"Account does not exist"? The seeded users must be created in Ergo. With the stack running, run yarn seed:docker so the seed script creates admin, alice, bob, charlie in both the DB and Ergo.
Can join/speak without identifying? If you had Ergo running before Phase 3, default channels may have been created without +R/+M. Reset the Ergo data volume so channels are recreated with the correct modes, then re-seed:
docker compose -f docker-compose.dev.yml down
docker volume rm anemoia_ergo_data
docker compose -f docker-compose.dev.yml up -d
# Wait for Ergo to start, then:
yarn seed:dockerThe Preact chat client runs at http://localhost:5173 (Vite dev server; proxies API and WebSocket to port 3000).
- Register — Create an account (display name, email, password). Your nick is derived from your display name.
- Login — Use your nick and password.
- Chat — Select a channel from the sidebar (#general by default), type messages. You’re connected to IRC via the API bouncer; messages sync with traditional IRC clients.
- Settings — Change password (synced to IRC).
Seeded logins (if you ran yarn seed:docker): nicks admin, alice, bob, charlie — password: password
Full request/response contracts (registration modes, verify-email, rate limits, error code fields) live in docs/api-protocol.md.
| Method | Path | Purpose |
|---|---|---|
| POST | /api/auth/register |
Register (nick, email, password) |
| POST | /api/auth/login |
Login (nick, password) → token + cookie |
| POST | /api/auth/verify-email |
Email verification (email_code mode); see protocol doc |
| POST | /api/auth/resend-code |
Resend verification code |
| PATCH | /api/auth/password |
Change password (Bearer); syncs to Ergo |
| GET | /api/auth/me |
Current user (Bearer) |
| POST | /api/auth/logout |
Invalidate session |
| GET | /ws |
WebSocket (IRC bouncer; ?token=... + credential cookie) |
| GET | /api/health |
API + Ergo health |
| GET | /api/server/info |
Community name, channels, user count |
| GET | /api/channels |
Channel list (name, topic) |
| GET | /~:nick |
User profile |
Admin API (Bearer token, role=admin): POST/GET/DELETE /api/admin/channels, POST/GET/DELETE /api/admin/channels/:name/ops, POST/GET/DELETE /api/admin/super-ops, POST /api/admin/kick-from-all, and (approval registration) GET/POST …/pending-registrations — see docs/api-protocol.md.
yarn dev:certs # Generate self-signed TLS certs + oper client cert
yarn dev:certs:force # Same, overwriting existing PEMs (or: yarn dev:certs -- --force)
yarn dev # Start Ergo + API (foreground; same as dev:infra)
yarn dev:infra # Start Ergo + API (foreground)
yarn dev:infra:down # Stop containers (keeps volumes and ./data)
yarn dev:teardown # Full tear-down: stop, remove volumes, delete ./data
yarn dev:reset # Teardown everything, then start fresh
yarn seed:docker # Seed users (run with stack up)- Port 3000 in use:
kill $(lsof -t -i :3000)or stop whatever is using it. - ADMIN_NICK not promoting: Ensure the user exists (register first), set ADMIN_NICK in .env.dev, then restart the dev container. The bootstrap runs only at API startup.
- Oper cert fingerprint mismatch: After
yarn dev:certs, copy the printed SHA-256 fingerprint intoergo/ircd.dev.yaml→opers.anemoia-api.certfp. To regenerate everything,yarn dev:certs:force(then update the fingerprint again).
- Environment: Copy
.env.dev.exampleto.env.devand edit. Dev scripts load.env.devfirst, then.envif present. For production, copy.env.exampleto.env. - Registration modes:
REGISTRATION_MODE(open,email_code,approval), SMTP,PENDING_REGISTRATION_KEY, rate limits, andTRUST_PROXYare documented in.env.example/.env.dev.exampleand docs/api-protocol.md. Default local dev isopenwith rate limits disabled (*_MAX=0). - Docker dev: The Ergo container uses a custom image (
ergo/Dockerfile.dev) that addscurlso the auth-wrapper can call the API. The API container is built withERGO_HOST=ergoandERGO_PORT=6667in the image so the bouncer connects to Ergo by service name; if those env vars are missing at runtime, the API still usesergowhen it detects it is running inside Docker (/.dockerenv). - IRC credential key (dev): In development, if
IRC_CREDENTIAL_KEYis not set in.env.dev, a built-in fallback key is used so login and the web client work without generating one. Production must setIRC_CREDENTIAL_KEY(e.g.openssl rand -base64 32). - Admin email (dev): Seeded admin uses
ADMIN_EMAILfrom.env.dev; the default isadmin@localhost.localdomainso it is a valid email format.
For most servers, use ./setup.sh (see Production above) instead of following these manual steps.
cp .env.example .envEdit .env and set:
COMMUNITY_NAME,DOMAIN,ADMIN_EMAIL,ADMIN_PASSWORDREGISTRATION_MODE—open(default),email_code, orapproval; non-open requiresPENDING_REGISTRATION_KEYand (foremail_code) SMTP — see comments in.env.exampleTRUST_PROXY—1if the API is behind a reverse proxy (rate limits use forwarded client IP)- Rate limit vars (
RATE_LIMIT_*) —0for*_MAXdisables that limiter ERGO_API_TOKEN—openssl rand -base64 32IRC_CREDENTIAL_KEY—openssl rand -base64 32(encrypts IRC password cookie for bouncer)SESSION_SECRET—openssl rand -base64 32NETWORK_NAME,SERVER_NAME(for Ergo)BUFFER_MAX_MESSAGES— optional; default 500 (in-memory replay per user)
See docs/api-protocol.md for HTTP status codes and admin pending-registration routes.
Values with spaces (e.g. COMMUNITY_NAME) must be quoted.
yarn generate-configWrites ergo/ircd.yaml from ergo/ircd.yaml.template using values from .env (tokens, oper fingerprint, server name, etc.). That file is gitignored — it is not in the repository, so you must run this after a fresh clone or whenever .env changes. ./setup.sh runs it for you.
For production, use real certs (e.g. Let's Encrypt). Place:
certs/fullchain.pemcerts/privkey.pem
For local/testing only, use self-signed:
yarn dev:certsdocker compose up -dOption A — ADMIN_NICK bootstrap: Register a user on the web app, set ADMIN_NICK=thatnick in .env, then restart the API. On startup, if no admins exist, that user is promoted.
Option B — Seed script: yarn seed:docker (or docker compose run --rm dev node /app/scripts/seed.js). Creates admin, alice, bob, charlie (password: password). Uses ADMIN_EMAIL and ADMIN_PASSWORD from .env for the admin account.
-
.envfilled with production values -
ERGO_API_TOKEN,IRC_CREDENTIAL_KEY, andSESSION_SECRETgenerated (not dev defaults) - Real TLS certs in
certs/(not self-signed) -
yarn generate-configrun after any.envchange (createsergo/ircd.yaml, which is not committed) - Firewall: 80, 443 (HTTP/S + ACME), 6697 (IRC TLS), and 6667 only if you use plaintext IRC — with Caddy, the API is not published on 3000 (see docs/deployment.md)
├── packages/
│ ├── api/ # Fastify API, auth, WebSocket bouncer, Ergo client
│ ├── client/ # Preact chat client (Vite)
│ └── common/ # Validation, shared protocol (nick, display name, WS message types)
├── ergo/ # Ergo: ircd.yaml.template (source), ircd.dev.yaml (dev); ircd.yaml generated (gitignored)
├── scripts/ # gen-dev-certs, generate-config, seed, migrate
├── .env.example # Production env template
├── .env.dev.example # Dev env template
├── setup.sh # Production interactive setup (see above)
├── update.sh # Rebuild/restart production stack
├── deploy/
│ ├── aws/ # Optional CloudFormation (EC2 + VPC + Elastic IP)
│ ├── Caddyfile.template
│ └── cert-sync.Dockerfile
├── docker-compose.yml
└── docker-compose.dev.yml