docs: update README schema section to match actual database schema
All checks were successful
check / check (push) Successful in 1m11s

Update the Schema section and related references throughout README.md to
accurately reflect the current 001_initial.sql migration:

- Rename 'users' table to 'sessions' with new columns: uuid, password_hash,
  signing_key, away_message
- Add new 'clients' table (uuid, session_id FK, token, created_at, last_seen)
- Add topic_set_by and topic_set_at columns to 'channels' table
- Update channel_members FK from user_id to session_id
- Add params column to messages table
- Update client_queues FK from user_id to client_id
- Update Queue Architecture diagram labels and surrounding text
- Update In-Memory Broker description to use client_id terminology
- Update Multi-Client Model MVP note to reflect sessions/clients split
This commit is contained in:
user
2026-03-17 02:17:34 -07:00
parent cab5784913
commit bd4326ad6f

View File

@@ -207,12 +207,12 @@ User Session
└── Client C (token_c, queue_c)
```
**Current MVP note:** The current implementation creates a new user (with new
nick) per `POST /api/v1/session` call. True multi-client (multiple tokens
sharing one nick/session) is supported by the schema (`client_queues` is keyed
by user_id, and multiple tokens can point to the same user) but the session
creation endpoint does not yet support "add a client to an existing session."
This will be added post-MVP.
**Current MVP note:** The current implementation creates a new session (with new
nick) per `POST /api/v1/session` call. True multi-client (multiple clients
sharing one session/nick) is supported by the schema `clients` references
`sessions` via `session_id`, and `client_queues` is keyed by `client_id` — but
the session creation endpoint does not yet support "add a client to an existing
session." This will be added post-MVP.
**Rationale:** The fundamental IRC mobile problem is that you can't have your
phone and laptop connected simultaneously without a bouncer. Server-side
@@ -265,8 +265,8 @@ The server implements HTTP long-polling for real-time message delivery:
- The client disconnects (connection closed, no response needed)
**Implementation detail:** The server maintains an in-memory broker with
per-user notification channels. When a message is enqueued for a user, the
broker closes all waiting channels for that user, waking up any blocked
per-client notification channels. When a message is enqueued for a client, the
broker closes all waiting channels for that client, waking up any blocked
long-poll handlers. This is O(1) notification — no polling loops, no database
scanning.
@@ -398,28 +398,28 @@ The entire read/write loop for a client is two endpoints. Everything else
│ │ │
┌─────────▼──┐ ┌───────▼────┐ ┌──────▼─────┐
│client_queue│ │client_queue│ │client_queue│
user_id=1 │ │ user_id=2 │ │ user_id=3
client_id=1│ │ client_id=2│ │ client_id=3│
│ msg_id=N │ │ msg_id=N │ │ msg_id=N │
└────────────┘ └────────────┘ └────────────┘
alice bob carol
Each message is stored ONCE. One queue entry per recipient.
Each message is stored ONCE. One queue entry per recipient client.
```
The `client_queues` table contains `(user_id, message_id)` pairs. When a
The `client_queues` table contains `(client_id, message_id)` pairs. When a
client polls with `GET /messages?after=<queue_id>`, the server queries for
queue entries with `id > after` for that user, joins against the messages
queue entries with `id > after` for that client, joins against the messages
table, and returns the results. The `queue_id` (auto-incrementing primary
key of `client_queues`) serves as a monotonically increasing cursor.
### In-Memory Broker
The server maintains an in-memory notification broker to avoid database
polling. The broker is a map of `user_id → []chan struct{}`. When a message
is enqueued for a user:
polling. The broker is a map of `client_id → []chan struct{}`. When a message
is enqueued for a client:
1. The handler calls `broker.Notify(userID)`
2. The broker closes all waiting channels for that user
1. The handler calls `broker.Notify(clientID)`
2. The broker closes all waiting channels for that client
3. Any goroutines blocked in `select` on those channels wake up
4. The woken handler queries the database for new queue entries
5. Messages are returned to the client
@@ -1740,33 +1740,52 @@ The database schema is managed via embedded SQL migration files in
**Current tables:**
#### `users`
#### `sessions`
| Column | Type | Description |
|-------------|----------|-------------|
|-----------------|----------|-------------|
| `id` | INTEGER | Primary key (auto-increment) |
| `uuid` | TEXT | Unique session UUID |
| `nick` | TEXT | Unique nick |
| `token` | TEXT | Unique auth token (64 hex chars) |
| `created_at`| DATETIME | Session creation time |
| `password_hash` | TEXT | Password hash (default empty) |
| `signing_key` | TEXT | Ed25519 signing key (default empty) |
| `away_message` | TEXT | Away message (default empty) |
| `created_at` | DATETIME | Session creation time |
| `last_seen` | DATETIME | Last API request time |
Index on `(uuid)`.
#### `clients`
| Column | Type | Description |
|--------------|----------|-------------|
| `id` | INTEGER | Primary key (auto-increment) |
| `uuid` | TEXT | Unique client UUID |
| `session_id` | INTEGER | FK → sessions.id (cascade delete) |
| `token` | TEXT | Unique auth token (64 hex chars) |
| `created_at` | DATETIME | Client creation time |
| `last_seen` | DATETIME | Last API request time |
Indexes on `(token)` and `(session_id)`.
#### `channels`
| Column | Type | Description |
|-------------|----------|-------------|
|---------------|----------|-------------|
| `id` | INTEGER | Primary key (auto-increment) |
| `name` | TEXT | Unique channel name (e.g., `#general`) |
| `topic` | TEXT | Channel topic (default empty) |
| `created_at`| DATETIME | Channel creation time |
| `updated_at`| DATETIME | Last modification time |
| `topic_set_by`| TEXT | Nick of the user who set the topic (default empty) |
| `topic_set_at`| DATETIME | When the topic was last set |
| `created_at` | DATETIME | Channel creation time |
| `updated_at` | DATETIME | Last modification time |
#### `channel_members`
| Column | Type | Description |
|-------------|----------|-------------|
|--------------|----------|-------------|
| `id` | INTEGER | Primary key (auto-increment) |
| `channel_id`| INTEGER | FK → channels.id |
| `user_id` | INTEGER | FK → users.id |
| `channel_id` | INTEGER | FK → channels.id (cascade delete) |
| `session_id` | INTEGER | FK → sessions.id (cascade delete) |
| `joined_at` | DATETIME | When the user joined |
Unique constraint on `(channel_id, user_id)`.
Unique constraint on `(channel_id, session_id)`.
#### `messages`
| Column | Type | Description |
@@ -1776,6 +1795,7 @@ Unique constraint on `(channel_id, user_id)`.
| `command` | TEXT | IRC command (`PRIVMSG`, `JOIN`, etc.) |
| `msg_from` | TEXT | Sender nick |
| `msg_to` | TEXT | Target (`#channel` or nick) |
| `params` | TEXT | JSON-encoded IRC-style positional parameters |
| `body` | TEXT | JSON-encoded body (array or object) |
| `meta` | TEXT | JSON-encoded metadata |
| `created_at`| DATETIME | Server timestamp |
@@ -1786,11 +1806,11 @@ Indexes on `(msg_to, id)` and `(created_at)`.
| Column | Type | Description |
|-------------|----------|-------------|
| `id` | INTEGER | Primary key (auto-increment). Used as the poll cursor. |
| `user_id` | INTEGER | FK → users.id |
| `message_id`| INTEGER | FK → messages.id |
| `client_id` | INTEGER | FK → clients.id (cascade delete) |
| `message_id`| INTEGER | FK → messages.id (cascade delete) |
| `created_at`| DATETIME | When the entry was queued |
Unique constraint on `(user_id, message_id)`. Index on `(user_id, id)`.
Unique constraint on `(client_id, message_id)`. Index on `(client_id, id)`.
The `client_queues.id` is the monotonically increasing cursor used by
`GET /messages?after=<id>`. This is more reliable than timestamps (no clock