Skip to content

WebSocket

The WebSocket channel pushes live KPI snapshots to connected dashboards, eliminating polling.


Connection

WS /ws/kpis?token=<jwt>

Authenticates the same way as REST. The endpoint tries, in order:

# Method Example
1 Authorization: Bearer <token> header Authorization: Bearer eyJhbGci…
2 Sub-protocol jwt.<token> Sec-WebSocket-Protocol: jwt.eyJhbGci…
3 ACCESS_TOKEN cookie (browser) auto-sent
4 ?token= query param CLI/tests only

The token must decode to a valid JWT with type == "access".

Rejections (connection closed with 1008 Policy Violation):

  • Missing/invalid token
  • Rate limit exceeded (ws_limiter per client IP)

Initial Snapshot

On connect, the server immediately sends the current KPI state:

{
  "type": "kpi:snapshot",
  "payload": { "total_events": 482113, "…": "…" }
}

payload is the same shape as GET /api/kpis.


Message Protocol

Direction Message Meaning
Server → Client kpi:snapshot Full KPI state
Client → Server ping Liveness check
Server → Client pong Reply to ping
Server → Client ping Server-side keepalive (every 30 s idle)
sequenceDiagram
  participant C as Client (React)
  participant W as WS Server /ws/kpis
  participant R as Redis Pub/Sub
  C->>W: connect (Bearer jwt)
  W-->>C: kpi:snapshot (initial)
  W->>R: subscribe events:new
  R-->>W: new events published
  W-->>C: kpi:snapshot (recomputed)
  loop Keepalive
    C->>W: ping
    W-->>C: pong
  end
  C-->>W: disconnect

Keepalive

The client sends ping text frames. The server also sends ping if the socket is idle for 30 s, and closes the connection when the client is unreachable.


Manager & Limits

  • Connection manager tracks all active sockets by user ID (manager.connect / disconnect).
  • Reconnect logic lives in the frontend useKpisSocket hook; it auto-reconnects with backoff.
  • The browser's HttpOnly cookie path works automatically for WS because cookies are sent with the handshake.