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