Files
chat-server/CLIENT_INTEGRATION.md
T

244 lines
4.6 KiB
Markdown

# 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 <jwt>`.
- WebSocket gateway at `/gateway` also requires JWT.
- Gateway token sources:
1. `Authorization: Bearer <jwt>` (preferred when possible)
2. query fallback: `?token=<jwt>` or `?access_token=<jwt>` (useful for browser WebSocket clients)
## Gateway (`/gateway`)
### Connect
- URL: `ws://<host>:<port>/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=<id>`: older than cursor, still newest -> oldest
- `after=<id>`: 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=<id>&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=<nextBefore>`
- catch up newer (after reconnect): call `after=<lastSeenMessageId>`
## 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.