refactor: model message schemas after IRC RFC 1459/2812
Replace c2s/s2c/s2s taxonomy with IRC-native structure: - schema/commands/ — IRC command schemas (PRIVMSG, NOTICE, JOIN, PART, QUIT, NICK, TOPIC, MODE, KICK, PING, PONG) - schema/numerics/ — IRC numeric reply codes (001-004, 322-323, 332, 353, 366, 372-376, 401, 403, 433, 442, 482) - schema/message.json — base envelope mapping IRC wire format to JSON Messages use 'command' field with IRC command names or 3-digit numeric codes. 'body' is a string (IRC trailing parameter), not object/array. 'from'/'to' map to IRC prefix and first parameter. Federation uses the same IRC commands (no custom RELAY/LINK/SYNC). Update README message format, command tables, and examples to match.
This commit is contained in:
227
README.md
227
README.md
@@ -18,10 +18,87 @@ 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
|
||||
- Supports multiple concurrent clients per user session
|
||||
- Provides IRC-like semantics: channels, nicks, topics, modes
|
||||
- Uses structured JSON messages with IRC command names and numeric reply codes
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Identity & Sessions — No Accounts
|
||||
|
||||
There are no accounts, no registration, no passwords. Identity is a signing
|
||||
key; a nick is just a display name. The two are decoupled.
|
||||
|
||||
- **Session creation**: client connects → server assigns a **session UUID**
|
||||
(user identity for this server), a **client UUID** (this specific device),
|
||||
and an **opaque auth token** (random bytes, not JWT).
|
||||
- The auth token implicitly identifies the client. Clients present it via
|
||||
`Authorization: Bearer <token>`.
|
||||
- Nicks are changeable; the session UUID is the stable identity.
|
||||
- Server-assigned UUIDs — clients do not choose their own IDs.
|
||||
|
||||
### Multi-Client Model
|
||||
|
||||
A single user session can have multiple clients (phone, laptop, terminal).
|
||||
|
||||
- Each client gets a **separate server-to-client (S2C) message queue**.
|
||||
- The server fans out all S2C messages to every active client queue for that
|
||||
user session.
|
||||
- `GET /api/v1/messages` delivers from the calling client's specific queue,
|
||||
identified by the auth token.
|
||||
- Client queues have **independent expiry/pruning** — one client going offline
|
||||
doesn't affect others.
|
||||
|
||||
```
|
||||
User (session UUID)
|
||||
├── Client A (client UUID, token, queue)
|
||||
├── Client B (client UUID, token, queue)
|
||||
└── Client C (client UUID, token, queue)
|
||||
```
|
||||
|
||||
### Message Immutability
|
||||
|
||||
Messages are **immutable** — no editing, no deletion by clients. This is a
|
||||
deliberate design choice that enables cryptographic signing: if a message could
|
||||
be modified after signing, signatures would be meaningless.
|
||||
|
||||
### Message Delivery
|
||||
|
||||
- **Long-poll timeout**: 15 seconds
|
||||
- **Queue depth**: server-configurable, default at least 48 hours worth of
|
||||
messages
|
||||
- **No delivery/read receipts** except in DMs
|
||||
- **Bodies are structured** objects or arrays (never raw strings) — enables
|
||||
deterministic canonicalization via RFC 8785 JCS for signing
|
||||
|
||||
### Crypto & Signing
|
||||
|
||||
- Servers **relay signatures verbatim** — signatures are key/value metadata on
|
||||
message objects (`meta.sig`, `meta.alg`). Servers do not verify them.
|
||||
- Clients handle key authentication via **TOFU** (trust on first use).
|
||||
- **No key revocation mechanism** — keep your keys safe.
|
||||
- **PUBKEY** message type for distributing signing keys to channel members.
|
||||
- **E2E encryption for DMs** is planned for 1.0.
|
||||
|
||||
### Channels
|
||||
|
||||
- **Any user can create channels** — joining a nonexistent channel creates it,
|
||||
like IRC.
|
||||
- **Ephemeral** — channels disappear when the last member leaves.
|
||||
- No channel size limits.
|
||||
- No channel-level encryption.
|
||||
|
||||
### Federation
|
||||
|
||||
- **Manual server linking only** — no autodiscovery, no mesh. Operators
|
||||
explicitly configure server links.
|
||||
- Servers relay messages (including signatures) verbatim.
|
||||
|
||||
### Web Client
|
||||
|
||||
The SPA web client is a **convenience UI**. The primary interface is IRC-style
|
||||
client apps talking directly to the HTTP API.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Transport: HTTP only
|
||||
@@ -30,7 +107,8 @@ 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.
|
||||
the connection for up to 15 seconds 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)
|
||||
@@ -38,6 +116,33 @@ request/response bodies. No WebSockets, no raw TCP, no gRPC — just plain HTTP.
|
||||
The entire read/write loop for a client is two endpoints. Everything else is
|
||||
channel management and history.
|
||||
|
||||
### Session Model
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ User Session (UUID) │
|
||||
│ nick: "alice" │
|
||||
│ signing key: ed25519:... │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Client A │ │ Client B │ ... │
|
||||
│ │ UUID │ │ UUID │ │
|
||||
│ │ token │ │ token │ │
|
||||
│ │ queue │ │ queue │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **User session**: server-assigned UUID. Represents a user on this server.
|
||||
Has a nick (changeable, unique per server at any point in time).
|
||||
- **Client**: each device/connection gets its own UUID and opaque auth token.
|
||||
The token is the credential — present it to authenticate.
|
||||
- **Queue**: each client has an independent S2C message queue. The server fans
|
||||
out messages to all active client queues for the session.
|
||||
|
||||
Sessions persist across disconnects. Messages queue until retrieved. Client
|
||||
queues expire independently after a configurable idle timeout.
|
||||
|
||||
### Message Protocol
|
||||
|
||||
All messages use **IRC command names and numeric reply codes** from RFC 1459/2812.
|
||||
@@ -72,7 +177,7 @@ This enables:
|
||||
|----------|-------------|
|
||||
| PRIVMSG | Send message to channel or user |
|
||||
| NOTICE | Send notice (no auto-reply expected) |
|
||||
| JOIN | Join a channel |
|
||||
| JOIN | Join a channel (creates it if nonexistent) |
|
||||
| PART | Leave a channel |
|
||||
| QUIT | Disconnect from server |
|
||||
| NICK | Change nickname |
|
||||
@@ -96,7 +201,7 @@ All C2S commands may be echoed back as S2C (relayed to other users), plus:
|
||||
|
||||
| Code | Name | Description |
|
||||
|------|-------------------|-------------|
|
||||
| 001 | RPL_WELCOME | Welcome after registration |
|
||||
| 001 | RPL_WELCOME | Welcome after session creation |
|
||||
| 002 | RPL_YOURHOST | Server host information |
|
||||
| 322 | RPL_LIST | Channel list entry |
|
||||
| 353 | RPL_NAMREPLY | Names list for a channel |
|
||||
@@ -144,6 +249,8 @@ complete index.
|
||||
### Canonicalization and Signing
|
||||
|
||||
Messages support optional cryptographic signatures for integrity verification.
|
||||
Servers relay signatures verbatim without verifying them — verification is
|
||||
purely a client-side concern.
|
||||
|
||||
#### Canonicalization (RFC 8785 JCS)
|
||||
|
||||
@@ -179,34 +286,9 @@ under canonicalization.
|
||||
{"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
|
||||
Servers relay PUBKEY messages to all channel members. Clients cache public keys
|
||||
and use them to verify `meta.sig` on incoming messages. Key distribution is
|
||||
trust-on-first-use (TOFU). There is no key revocation mechanism.
|
||||
|
||||
### API Endpoints
|
||||
|
||||
@@ -216,9 +298,9 @@ require `Authorization: Bearer <token>` 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
|
||||
1. `POST /api/v1/session` — create a session, 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)
|
||||
3. `GET /api/v1/messages?timeout=15` — 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.
|
||||
@@ -226,11 +308,11 @@ 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 \
|
||||
# Create a session (get session UUID, client UUID, and auth token)
|
||||
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/session \
|
||||
-d '{"nick":"alice"}' | jq -r .token)
|
||||
|
||||
# Join a channel
|
||||
# Join a channel (creates it if it doesn't exist)
|
||||
curl -s -X POST http://localhost:8080/api/v1/messages \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"command":"JOIN","to":"#general"}'
|
||||
@@ -240,34 +322,41 @@ 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 \
|
||||
# Poll for messages (long-poll, 15s timeout)
|
||||
curl -s "http://localhost:8080/api/v1/messages?timeout=15" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
#### Registration
|
||||
#### Session
|
||||
|
||||
```
|
||||
POST /api/v1/register — Create account { "nick": "..." } → { id, nick, token }
|
||||
POST /api/v1/session — Create session { "nick": "..." }
|
||||
→ { session_id, client_id, nick, token }
|
||||
Token is opaque (random), not JWT.
|
||||
Token implicitly identifies the client.
|
||||
```
|
||||
|
||||
#### State
|
||||
|
||||
```
|
||||
GET /api/v1/state — User state: nick, id, and list of joined channels
|
||||
Replaces separate /me and /channels endpoints
|
||||
GET /api/v1/state — User state: nick, session_id, client_id,
|
||||
and list of joined channels
|
||||
```
|
||||
|
||||
#### Messages (unified stream)
|
||||
|
||||
```
|
||||
GET /api/v1/messages — Single message stream (long-poll supported)
|
||||
GET /api/v1/messages — Single message stream (long-poll, 15s timeout)
|
||||
All message types: channel, DM, notices, events
|
||||
Query params: ?after=<message-id>&timeout=30
|
||||
Delivers from the calling client's queue
|
||||
(identified by auth token)
|
||||
Query params: ?after=<message-id>&timeout=15
|
||||
POST /api/v1/messages — Send a message (IRC command in body)
|
||||
Body: { "command": "PRIVMSG", "to": "#channel", "body": ["..."] }
|
||||
```
|
||||
|
||||
Messages are immutable — no edit or delete endpoints.
|
||||
|
||||
#### History
|
||||
|
||||
```
|
||||
@@ -281,7 +370,9 @@ GET /api/v1/history — Fetch history for a target (channel or DM)
|
||||
```
|
||||
GET /api/v1/channels/all — List all server channels
|
||||
POST /api/v1/channels/join — Join a channel { "channel": "#name" }
|
||||
Creates the channel if it doesn't exist.
|
||||
DELETE /api/v1/channels/{name} — Part (leave) a channel
|
||||
Channel is destroyed when last member leaves.
|
||||
GET /api/v1/channels/{name}/members — Channel member list
|
||||
```
|
||||
|
||||
@@ -294,7 +385,8 @@ GET /.well-known/healthcheck.json — Health check
|
||||
|
||||
### Federation (Server-to-Server)
|
||||
|
||||
Servers can link to form a network, similar to IRC server linking:
|
||||
Servers can link to form a network, similar to IRC server linking. Links are
|
||||
**manually configured** — there is no autodiscovery.
|
||||
|
||||
```
|
||||
POST /api/v1/federation/link — Establish server link (mutual auth via shared key)
|
||||
@@ -303,8 +395,8 @@ 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.
|
||||
UNLINK, SYNC, PING, and PONG commands. Messages (including signatures) are
|
||||
relayed verbatim between servers so users on different servers can share channels.
|
||||
|
||||
### Channel Modes
|
||||
|
||||
@@ -331,7 +423,9 @@ Via environment variables (Viper), following gohttpserver conventions:
|
||||
| `DEBUG` | `false` | Debug mode |
|
||||
| `MAX_HISTORY` | `10000` | Max messages per channel history |
|
||||
| `SESSION_TIMEOUT` | `86400` | Session idle timeout (seconds) |
|
||||
| `QUEUE_MAX_AGE` | `172800` | Max client queue age in seconds (default 48h) |
|
||||
| `MAX_MESSAGE_SIZE` | `4096` | Max message body size (bytes) |
|
||||
| `LONG_POLL_TIMEOUT` | `15` | Long-poll timeout in seconds |
|
||||
| `MOTD` | `""` | Message of the day |
|
||||
| `SERVER_NAME` | hostname | Server display name |
|
||||
| `FEDERATION_KEY` | `""` | Shared key for server linking |
|
||||
@@ -341,11 +435,12 @@ Via environment variables (Viper), following gohttpserver conventions:
|
||||
SQLite by default (single-file, zero-config), with Postgres support for
|
||||
larger deployments. Tables:
|
||||
|
||||
- `users` — accounts and auth tokens
|
||||
- `sessions` — user sessions (UUID, nick, created_at)
|
||||
- `clients` — client records (UUID, session_id, token_hash, last_seen)
|
||||
- `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
|
||||
- `client_queues` — per-client pending delivery queues
|
||||
- `server_links` — federation peer configuration
|
||||
|
||||
### Project Structure
|
||||
@@ -398,30 +493,30 @@ Per gohttpserver conventions:
|
||||
| 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.
|
||||
2. **No accounts** — identity is a signing key, nick is a display name. No
|
||||
registration, no passwords. Session creation is instant.
|
||||
3. **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
|
||||
4. **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
|
||||
5. **Server holds state** — clients are stateless. Reconnect, switch devices,
|
||||
lose connectivity — your messages are waiting in your client queue.
|
||||
6. **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
|
||||
7. **Immutable messages** — no editing, no deletion. Fits naturally with
|
||||
cryptographic signatures.
|
||||
8. **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.
|
||||
9. **No eternal logs** — history rotates. Chat should be ephemeral by default.
|
||||
Channels disappear when empty.
|
||||
10. **Federation optional** — single server works standalone. Linking is manual
|
||||
and opt-in.
|
||||
11. **Signable messages** — optional Ed25519 signatures with TOFU key
|
||||
distribution. Servers relay signatures without verification.
|
||||
|
||||
## Status
|
||||
|
||||
|
||||
Reference in New Issue
Block a user