4.6 KiB
4.6 KiB
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/*requireAuthorization: Bearer <jwt>. - WebSocket gateway at
/gatewayalso requires JWT. - Gateway token sources:
Authorization: Bearer <jwt>(preferred when possible)- query fallback:
?token=<jwt>or?access_token=<jwt>(useful for browser WebSocket clients)
Gateway (/gateway)
Connect
- URL:
ws://<host>:<port>/gateway(orwss://behind TLS) - On successful auth, server sends:
{
"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
{"op":"subscribe","channelId":12}
or
{"op":"subscribe","channelIds":[12,13]}
Response:
{
"op": "subscribed",
"channelIds": [12, 13],
"subscriptions": [12, 13]
}
If one or more channels do not exist:
{
"op": "subscribed",
"channelIds": [12],
"subscriptions": [12],
"unknownChannelIds": [999]
}
Unsubscribe
{"op":"unsubscribe","channelId":12}
or
{"op":"unsubscribe","channelIds":[12,13]}
Response:
{
"op": "unsubscribed",
"channelIds": [12],
"subscriptions": [13]
}
Heartbeat
Client may send:
{"op":"ping"}
or
{"op":"heartbeat"}
Server replies:
{
"op": "pong",
"ts": "2026-02-20T12:34:56.000Z"
}
Server -> Client Event Messages
After subscription, channel message mutations are pushed as:
Message Created
{
"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
{
"op": "event",
"event": "message.updated",
"channelId": 12,
"message": { "...same message shape..." },
"emittedAt": "2026-02-20T12:35:56.000Z"
}
Message Deleted
{
"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:
{"op":"error","message":"..."}
Common causes:
- invalid JSON payload
- missing/invalid
op - unsupported op
- missing
channelId/channelIdsfor subscribe/unsubscribe
Message History Pagination (GET /api/messages)
Query Params
channelId(required, int)limit(optional, int, default50, max100)before(optional, message ID cursor)after(optional, message ID cursor)
Rules:
- Do not send
beforeandaftertogether. before/aftermust be positive integers.
Response Shape
{
"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 -> oldestafter=<id>: newer than cursor, oldest -> newest (ascending)
For client state:
nextBefore: cursor to request older pagenextAfter: cursor to request newer page
Recommended Client Flow
- Login/register/join and store JWT.
- Initial channel load:
GET /api/messages?channelId=<id>&limit=50. - Connect
/gatewaywith token. - Send
subscribefor active channel(s). - Merge realtime events into local message list:
message.created: insert if absentmessage.updated: patch by idmessage.deleted: remove/tombstone by id
- 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.