# Client Integration Guide This document describes the current server contract you can rely on when implementing the client. ## Base Auth Model - REST endpoints under `/api/*` require `Authorization: Bearer `. - WebSocket gateway at `/gateway` also requires JWT. - Gateway token sources: 1. `Authorization: Bearer ` (preferred when possible) 2. query fallback: `?token=` or `?access_token=` (useful for browser WebSocket clients) ## Gateway (`/gateway`) ### Connect - URL: `ws://:/gateway` (or `wss://` behind TLS) - On successful auth, server sends: ```json { "op": "ready", "connectionId": "gw_1", "userId": "alice@example.com", "username": "alice" } ``` If auth fails, handshake is rejected with HTTP error (`401`/`403`) and no WebSocket session is established. ### Client -> Server Messages All frames are JSON text. #### Subscribe ```json {"op":"subscribe","channelId":12} ``` or ```json {"op":"subscribe","channelIds":[12,13]} ``` Response: ```json { "op": "subscribed", "channelIds": [12, 13], "subscriptions": [12, 13] } ``` If one or more channels do not exist: ```json { "op": "subscribed", "channelIds": [12], "subscriptions": [12], "unknownChannelIds": [999] } ``` #### Unsubscribe ```json {"op":"unsubscribe","channelId":12} ``` or ```json {"op":"unsubscribe","channelIds":[12,13]} ``` Response: ```json { "op": "unsubscribed", "channelIds": [12], "subscriptions": [13] } ``` #### Heartbeat Client may send: ```json {"op":"ping"} ``` or ```json {"op":"heartbeat"} ``` Server replies: ```json { "op": "pong", "ts": "2026-02-20T12:34:56.000Z" } ``` ### Server -> Client Event Messages After subscription, channel message mutations are pushed as: #### Message Created ```json { "op": "event", "event": "message.created", "channelId": 12, "message": { "id": 101, "content": "hello", "authorId": "alice@example.com", "authorUsername": "alice", "authorAvatarUrl": null, "authorHomeServer": "https://home.example.com", "channelId": 12, "createdAt": "2026-02-20T12:34:56.000", "editedAt": null }, "emittedAt": "2026-02-20T12:34:56.000Z" } ``` #### Message Updated ```json { "op": "event", "event": "message.updated", "channelId": 12, "message": { "...same message shape..." }, "emittedAt": "2026-02-20T12:35:56.000Z" } ``` #### Message Deleted ```json { "op": "event", "event": "message.deleted", "channelId": 12, "messageId": 101, "emittedAt": "2026-02-20T12:36:56.000Z" } ``` ### Gateway Error Payload Malformed input or unsupported ops return: ```json {"op":"error","message":"..."} ``` Common causes: - invalid JSON payload - missing/invalid `op` - unsupported op - missing `channelId`/`channelIds` for subscribe/unsubscribe ## Message History Pagination (`GET /api/messages`) ### Query Params - `channelId` (required, int) - `limit` (optional, int, default `50`, max `100`) - `before` (optional, message ID cursor) - `after` (optional, message ID cursor) Rules: - Do not send `before` and `after` together. - `before`/`after` must be positive integers. ### Response Shape ```json { "messages": [ { "id": 100, "content": "text", "authorId": "alice@example.com", "authorUsername": "alice", "authorAvatarUrl": null, "authorHomeServer": "https://home.example.com", "channelId": 12, "createdAt": "2026-02-20T12:30:00.000", "editedAt": null } ], "channelId": 12, "limit": 50, "hasMore": true, "nextBefore": 51, "nextAfter": 100 } ``` ### Ordering Semantics - no cursor: newest -> oldest (descending by message id) - `before=`: older than cursor, still newest -> oldest - `after=`: newer than cursor, oldest -> newest (ascending) For client state: - `nextBefore`: cursor to request older page - `nextAfter`: cursor to request newer page ## Recommended Client Flow 1. Login/register/join and store JWT. 2. Initial channel load: `GET /api/messages?channelId=&limit=50`. 3. Connect `/gateway` with token. 4. Send `subscribe` for active channel(s). 5. Merge realtime events into local message list: - `message.created`: insert if absent - `message.updated`: patch by id - `message.deleted`: remove/tombstone by id 6. Pagination: - load older: call `before=` - catch up newer (after reconnect): call `after=` ## Practical Notes - REST is source of truth; gateway is fanout only. - Gateway registry is currently in-memory on server instance (no cross-instance sync yet). - Channel permissions are not enforced yet by gateway subscriptions; only channel existence is checked.