# chat A modern IRC-inspired chat server written in Go. Decouples session state from transport connections, enabling mobile-friendly persistent sessions over HTTP. The **HTTP API is the primary interface**. It's designed to be simple enough that writing a terminal IRC-style client against it is straightforward — just `curl` and `jq` get you surprisingly far. The server also ships an embedded web client as a convenience/reference implementation, but the API comes first. ## Motivation IRC is in decline because session state is tied to the TCP connection. In a mobile-first world, that's a nonstarter. Not everyone wants to run a bouncer or pay for IRCCloud. This project builds a chat server that: - Holds session state server-side (message queues, presence, channel membership) - Exposes a minimal, clean HTTP+JSON API — easy to build clients against - Supports multiple concurrent connections per user session - Provides IRC-like semantics: channels, nicks, topics, modes - Uses structured JSON messages with IRC command names and numeric reply codes ## Architecture ### Transport: HTTP only All client↔server and server↔server communication uses HTTP/1.1+ with JSON request/response bodies. No WebSockets, no raw TCP, no gRPC — just plain HTTP. - **Client polling**: Clients long-poll `GET /api/v1/messages` — server holds the connection until messages arrive or timeout. One endpoint for everything. - **Client sending**: `POST /api/v1/messages` with a `to` field. That's it. - **Server federation**: Servers exchange messages via HTTP to enable multi-server networks (like IRC server linking) The entire read/write loop for a client is two endpoints. Everything else is channel management and history. ### Message Protocol All messages use **IRC command names and numeric reply codes** from RFC 1459/2812. The `command` field identifies the message type. #### Message Envelope Every message is a JSON object with these fields: | Field | Type | Required | Description | |-----------|-----------------|----------|-------------| | `command` | string | ✓ | IRC command name or 3-digit numeric code | | `from` | string | | Sender nick or server name | | `to` | string | | Destination: `#channel` or nick | | `params` | array\ | | Additional IRC-style parameters | | `body` | array \| object | | Structured body (never a raw string) | | `meta` | object | | Extensible metadata (signatures, etc.) | | `id` | string (uuid) | | Server-assigned message ID | | `ts` | string | | Server-assigned ISO 8601 timestamp | **Important:** Message bodies MUST be objects or arrays, never raw strings. This enables: - Multiline messages (array of lines) - Deterministic canonicalization for hashing/signing (RFC 8785 JCS) - Structured data where needed (e.g. PUBKEY) #### IRC Command Mapping **Client-to-Server (C2S):** | Command | Description | |----------|-------------| | PRIVMSG | Send message to channel or user | | NOTICE | Send notice (no auto-reply expected) | | JOIN | Join a channel | | PART | Leave a channel | | QUIT | Disconnect from server | | NICK | Change nickname | | MODE | Set/query channel or user modes | | TOPIC | Set/query channel topic | | KICK | Kick a user from a channel | | PING | Client keepalive | | PUBKEY | Announce public signing key | **Server-to-Client (S2C):** All C2S commands may be echoed back as S2C (relayed to other users), plus: | Command | Description | |----------|-------------| | PONG | Server keepalive response | | PUBKEY | Relayed public key from another user | | ERROR | Server error message | **Numeric Reply Codes (S2C):** | Code | Name | Description | |------|-------------------|-------------| | 001 | RPL_WELCOME | Welcome after registration | | 002 | RPL_YOURHOST | Server host information | | 322 | RPL_LIST | Channel list entry | | 353 | RPL_NAMREPLY | Names list for a channel | | 366 | RPL_ENDOFNAMES | End of names list | | 372 | RPL_MOTD | Message of the day line | | 375 | RPL_MOTDSTART | Start of MOTD | | 376 | RPL_ENDOFMOTD | End of MOTD | | 401 | ERR_NOSUCHNICK | No such nick or channel | | 403 | ERR_NOSUCHCHANNEL | No such channel | | 433 | ERR_NICKNAMEINUSE | Nickname already in use | **Server-to-Server (S2S):** | Command | Description | |---------|-------------| | RELAY | Relay message to linked server | | LINK | Establish server link | | UNLINK | Tear down server link | | SYNC | Synchronize state between servers | | PING | Server-to-server keepalive | | PONG | Server-to-server keepalive response | #### Message Examples ```json {"command": "PRIVMSG", "from": "alice", "to": "#general", "body": ["hello world"], "meta": {"sig": "base64...", "alg": "ed25519"}} {"command": "PRIVMSG", "from": "alice", "to": "#general", "body": ["line one", "line two"]} {"command": "001", "to": "alice", "body": ["Welcome to the network, alice"]} {"command": "353", "to": "alice", "params": ["=", "#general"], "body": ["alice", "bob", "@charlie"]} {"command": "JOIN", "from": "bob", "to": "#general", "body": []} {"command": "ERROR", "body": ["Closing link: connection timeout"]} ``` #### JSON Schemas Full JSON Schema (draft 2020-12) definitions for all message types are in [`schema/`](schema/). See [`schema/README.md`](schema/README.md) for the complete index. ### Canonicalization and Signing Messages support optional cryptographic signatures for integrity verification. #### Canonicalization (RFC 8785 JCS) To produce a deterministic byte representation of a message for signing: 1. Remove `meta.sig` from the message (the signature itself is not signed) 2. Serialize using [RFC 8785 JSON Canonicalization Scheme (JCS)](https://www.rfc-editor.org/rfc/rfc8785): - Object keys sorted lexicographically - No whitespace - Numbers in shortest form - UTF-8 encoding 3. The resulting byte string is the signing input This is why `body` must be an object or array — raw strings would be ambiguous under canonicalization. #### Signing Flow 1. Client generates an Ed25519 keypair 2. Client announces public key: `{"command": "PUBKEY", "body": {"alg": "ed25519", "key": "base64..."}}` 3. Server relays PUBKEY to channel members / stores for the session 4. When sending a message, client: a. Constructs the message without `meta.sig` b. Canonicalizes per JCS c. Signs with private key d. Adds `meta.sig` (base64) and `meta.alg` 5. Recipients verify by repeating steps a–c and checking the signature against the sender's announced public key #### PUBKEY Message ```json {"command": "PUBKEY", "from": "alice", "body": {"alg": "ed25519", "key": "base64-encoded-pubkey"}} ``` Servers SHOULD relay PUBKEY messages to all channel members. Clients SHOULD cache public keys and use them to verify `meta.sig` on incoming messages. Key distribution is trust-on-first-use (TOFU) by default. ### Core Concepts #### Users - Identified by a unique user ID (UUID) - Authenticate via token (issued at registration or login) - Have a nick (changeable, unique per server at any point in time) - Maintain a persistent message queue on the server #### Sessions - A session represents an authenticated user's connection context - Session state is **server-held**, not connection-bound - Multiple devices can share a session (messages delivered to all) - Sessions persist across disconnects — messages queue until retrieved - Sessions expire after a configurable idle timeout (default 24h) #### Channels - Named with `#` prefix (e.g. `#general`) - Have a topic, mode flags, and member list - Messages to a channel are queued for all members - Channel history is stored server-side (configurable depth) - No eternal logging by default — history rotates ### API Endpoints All endpoints accept and return `application/json`. Authenticated endpoints require `Authorization: Bearer ` header. The API is the primary interface — designed for IRC-style clients. The entire client loop is: 1. `POST /api/v1/register` — get a token 2. `GET /api/v1/state` — see who you are and what channels you're in 3. `GET /api/v1/messages?after=0` — long-poll for all messages (channel, DM, system) 4. `POST /api/v1/messages` — send to `"#channel"` or `"nick"` That's the core. Everything else (join, part, history, members) is ancillary. #### Quick example (curl) ```bash # Register TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/register \ -d '{"nick":"alice"}' | jq -r .token) # Join a channel curl -s -X POST http://localhost:8080/api/v1/messages \ -H "Authorization: Bearer $TOKEN" \ -d '{"command":"JOIN","to":"#general"}' # Send a message curl -s -X POST http://localhost:8080/api/v1/messages \ -H "Authorization: Bearer $TOKEN" \ -d '{"command":"PRIVMSG","to":"#general","body":["hello world"]}' # Poll for messages (long-poll) curl -s http://localhost:8080/api/v1/messages?after=0 \ -H "Authorization: Bearer $TOKEN" ``` #### Registration ``` POST /api/v1/register — Create account { "nick": "..." } → { id, nick, token } ``` #### State ``` GET /api/v1/state — User state: nick, id, and list of joined channels Replaces separate /me and /channels endpoints ``` #### Messages (unified stream) ``` GET /api/v1/messages — Single message stream (long-poll supported) All message types: channel, DM, notices, events Query params: ?after=&timeout=30 POST /api/v1/messages — Send a message (IRC command in body) Body: { "command": "PRIVMSG", "to": "#channel", "body": ["..."] } ``` #### History ``` GET /api/v1/history — Fetch history for a target (channel or DM) Query params: ?target=#channel&before=&limit=50 For DMs: ?target=nick&before=&limit=50 ``` #### Channels ``` GET /api/v1/channels/all — List all server channels POST /api/v1/channels/join — Join a channel { "channel": "#name" } DELETE /api/v1/channels/{name} — Part (leave) a channel GET /api/v1/channels/{name}/members — Channel member list ``` #### Server Info ``` GET /api/v1/server — Server info (name, MOTD) GET /.well-known/healthcheck.json — Health check ``` ### Federation (Server-to-Server) Servers can link to form a network, similar to IRC server linking: ``` POST /api/v1/federation/link — Establish server link (mutual auth via shared key) POST /api/v1/federation/relay — Relay messages between linked servers GET /api/v1/federation/status — Link status ``` Federation uses the same HTTP+JSON transport. S2S messages use the RELAY, LINK, UNLINK, SYNC, PING, and PONG commands. Messages are relayed between servers so users on different servers can share channels. ### Channel Modes Inspired by IRC but simplified: | Mode | Meaning | |------|---------| | `+i` | Invite-only | | `+m` | Moderated (only voiced users can send) | | `+s` | Secret (hidden from channel list) | | `+t` | Topic locked (only ops can change) | | `+n` | No external messages | User channel modes: `+o` (operator), `+v` (voice) ### Configuration Via environment variables (Viper), following gohttpserver conventions: | Variable | Default | Description | |----------|---------|-------------| | `PORT` | `8080` | Listen port | | `DBURL` | `""` | SQLite/Postgres connection string | | `DEBUG` | `false` | Debug mode | | `MAX_HISTORY` | `10000` | Max messages per channel history | | `SESSION_TIMEOUT` | `86400` | Session idle timeout (seconds) | | `MAX_MESSAGE_SIZE` | `4096` | Max message body size (bytes) | | `MOTD` | `""` | Message of the day | | `SERVER_NAME` | hostname | Server display name | | `FEDERATION_KEY` | `""` | Shared key for server linking | ### Storage SQLite by default (single-file, zero-config), with Postgres support for larger deployments. Tables: - `users` — accounts and auth tokens - `channels` — channel metadata and modes - `channel_members` — membership and user modes - `messages` — message history (rotated per `MAX_HISTORY`) - `message_queue` — per-user pending delivery queue - `server_links` — federation peer configuration ### Project Structure Following [gohttpserver CONVENTIONS.md](https://git.eeqj.de/sneak/gohttpserver/src/branch/main/CONVENTIONS.md): ``` chat/ ├── cmd/ │ └── chatd/ │ └── main.go ├── internal/ │ ├── config/ │ ├── database/ │ ├── globals/ │ ├── handlers/ │ ├── healthcheck/ │ ├── logger/ │ ├── middleware/ │ ├── models/ │ ├── queue/ │ └── server/ ├── schema/ │ ├── message.schema.json │ ├── c2s/ │ ├── s2c/ │ ├── s2s/ │ └── README.md ├── web/ ├── go.mod ├── go.sum ├── Makefile ├── Dockerfile ├── CONVENTIONS.md └── README.md ``` ### Required Libraries Per gohttpserver conventions: | Purpose | Library | |---------|---------| | DI | `go.uber.org/fx` | | Router | `github.com/go-chi/chi` | | Logging | `log/slog` (stdlib) | | Config | `github.com/spf13/viper` | | Env | `github.com/joho/godotenv/autoload` | | CORS | `github.com/go-chi/cors` | | Metrics | `github.com/prometheus/client_golang` | | DB | `modernc.org/sqlite` + `database/sql` | ### Web Client The server embeds a single-page web client (Preact) served at `/`. This is a **convenience/reference implementation** — not the primary interface. The primary intended clients are IRC-style terminal applications, bots, and custom clients talking directly to the HTTP API. ### Design Principles 1. **API-first** — the HTTP API is the product. Clients are thin. If you can't build a working IRC-style TUI client in an afternoon, the API is too complex. 2. **IRC semantics over HTTP** — command names and numeric codes from RFC 1459/2812. Familiar to anyone who's built IRC clients or bots. 3. **HTTP is the only transport** — no WebSockets, no raw TCP, no protocol negotiation. HTTP is universal, proxy-friendly, and works everywhere. 4. **Server holds state** — clients are stateless. Reconnect, switch devices, lose connectivity — your messages are waiting. 5. **Structured messages** — JSON with extensible metadata. Bodies are always objects or arrays for deterministic canonicalization (JCS) and signing. 6. **Simple deployment** — single binary, SQLite default, zero mandatory external dependencies. 7. **No eternal logs** — history rotates. Chat should be ephemeral by default. 8. **Federation optional** — single server works standalone. Linking is opt-in. 9. **Signable messages** — optional Ed25519 signatures with TOFU key distribution. ## Status **Implementation in progress.** Core API is functional with SQLite storage and embedded web client. ## License MIT