Skip to main content

WebSocket API

The backend does not handle WebSocket connections directly; it mints the credentials a client presents to open a real-time connection and authorises channel subscriptions on behalf of the broker. The active system is Centrifugo, exposed through the endpoint and internal route documented below.

An earlier Supabase Realtime design (HS256 JWT signed with SUPABASE_JWT_SECRET) was replaced by Centrifugo in this phase. An even earlier Cloudflare office-router Worker design (Durable Objects + an RS256 handshake token) was implemented but never routed and was removed in a dead-code purge (qubital-backend #4). Neither reflects current code.

Connection token — POST /websocket/token

Mints the Centrifugo connection token for the authenticated user. The response includes the signed token and the public WebSocket URL to connect to.

FieldValue
EndpointPOST /websocket/token
AuthCookie (session)
AlgorithmHS256
TTL~10 minutes (DefaultConnectionTokenTTL)
401No authenticated session
403Session has no active organisation (token is per-org; org switch = reconnect)

Response body

{
"token": "<HS256 JWT>",
"expiresAt": 1234567890,
"url": "wss://your-centrifugo-host/connection/websocket"
}
FieldTypeDescription
tokenstringSigned HS256 connection JWT to present to the Centrifugo broker
expiresAtintegerToken expiry as a Unix timestamp (seconds)
urlstringPublic WebSocket endpoint (CENTRIFUGO_PUBLIC_WS_URL) to connect to with the token

Token claims

The connection token carries:

  • sub — WorkOS user ID (the broker-side user identity).
  • channels — server-side subscription list: user:<org>:<uid> (personal channel) and org:<org> (org-wide channel). These are subscribed automatically on connect; no client-side subscribe call is needed for them.
  • meta.org — the caller's WorkOS org ID, embedded server-side for the subscribe proxy's zero-DB authorization check. The broker forwards this on every subscribe-proxy request (include_connection_meta); it is never sent to the client.

Subscribe proxy — POST /internal/centrifugo/subscribe

A gin-only route (not in the OpenAPI contract) called by the Centrifugo broker, never by browsers. It authorises client-side channel subscriptions that are in a proxied namespace.

Only the status: namespace is proxied. All other namespaces use server-side subscriptions via the connection token's channels claim and never reach this handler.

Authentication

Every broker request carries the header:

X-Centrifugo-Proxy-Secret: <CENTRIFUGO_PROXY_SECRET>

The handler compares it to CENTRIFUGO_PROXY_SECRET with a constant-time comparison. Requests without a matching header are rejected with HTTP 401 before any channel logic runs.

Authorization logic

For a status:<org>:<uid> channel subscription:

  1. Parse the channel name with StatusChannelOrg — reject (deny) if it is not a well-formed status: channel.
  2. Check that meta.org (from the connection token, forwarded by the broker) is non-empty and equals the channel's org segment — deny if it differs (cross-org subscription attempt).
  3. Allow if both checks pass.

No database I/O occurs; the org check is a string comparison against the org pinned into the connection token at mint time.

Response protocol

The proxy always returns HTTP 200. The broker-relayed outcome is in the JSON body:

OutcomeBody
Allow{"result":{}}
Deny — not a status channel{"error":{"code":403,"message":"channel not allowed"}}
Deny — cross-org{"error":{"code":403,"message":"permission denied"}}

Non-200 is reserved for requests that are not well-formed broker traffic (e.g. unreadable body).

Channel scheme

ChannelFormatSubscription methodCarries
User personaluser:<org>:<uid>Server-side (connection token channels claim)Private-visibility conversation events, personal events
Org-wideorg:<org>Server-side (connection token channels claim)Org-visibility conversation events
User statusstatus:<org>:<uid>Client-side (subscribe proxy gated)status.changed events for that user
  • Chat — how the frontend mints and uses the token
  • ConfigurationCENTRIFUGO_PROXY_SECRET, CENTRIFUGO_PUBLIC_WS_URL