Files
chat-server/CLIENT_INTEGRATION.md
T

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/* 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:
{
  "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/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

{
  "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
  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
  1. 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.