WebSocket
Real-time communication via WebSocket using a channel-based pub/sub protocol.
Connection
| Environment | URL |
|---|---|
| Staging | wss://lumio.api.staging.zaflun.dev/v1/ws |
| Production Preview | wss://lumio.api.prod.zaflun.dev/v1/ws |
| Production | coming soon -- official domain not yet assigned |
The endpoint is an HTTP GET /v1/ws request that is upgraded to a WebSocket connection. All fields in both directions use snake_case JSON.
Protocol
Lumio uses a custom JSON-message channel-subscription protocol. Clients subscribe to named channels (e.g. events:\{account_id\}, overlay:\{key\}) and receive messages pushed by the server whenever workers publish to the matching Redis pub/sub channel.
Lumio does not use graphql-ws and does not expose GraphQL subscriptions (EmptySubscription). All real-time updates flow through this WebSocket.
Authentication
Auth is resolved by the same middleware that guards REST and GraphQL. The server accepts either:
Authorization: Bearer <token>header -- System Key, User API Key, or JWT.?token=<token>query parameter on the WebSocket URL -- Popout Token (prefixlm_pop_) or JWT. Browsers must use this form because the nativeWebSocketAPI cannot send custom headers.
Anonymous connections are allowed but can only subscribe to public overlay:* channels.
Channel access is enforced at subscribe time:
| Channel prefix | Auth requirement |
|---|---|
overlay:\{key\} | Public -- any connection (used by OBS browser sources). |
events:\{account_id\} | Authenticated caller with access to the account. |
spotify:\{account_id\} | Authenticated caller with access to the account. |
chat:\{account_id\} | Account access + chat:read permission. |
automations:\{account_id\} | Account access + automations:read permission. |
Client Messages
All client messages are JSON with a type discriminator.
type | Fields | Purpose |
|---|---|---|
subscribe | channel: string | Subscribe to a channel. Server responds with subscribed or error. |
unsubscribe | channel: string | Unsubscribe. Server responds with unsubscribed. |
broadcast | channel: string, payload: string | Broadcast a raw JSON string (payload is pre-serialized) to every session subscribed to channel. Requires the same access as subscribe for that channel. Wrapped by the server as \{"type":"event","data":<payload>\} before forwarding. |
ping | -- | Heartbeat. Server replies with pong. |
Example:
{"type":"subscribe","channel":"events:550e8400-e29b-41d4-a716-446655440000"}
Messages larger than 64 KiB are rejected with MESSAGE_TOO_LARGE.
Server Messages
All server messages are JSON with a type discriminator.
type | Fields | Meaning |
|---|---|---|
welcome | session_id: string, server_version: string | Sent once immediately after the socket opens. |
subscribed | channel: string | Subscription accepted. |
unsubscribed | channel: string | Unsubscription accepted. |
pong | -- | Reply to ping. |
error | message: string, code: string | Error. See codes below. |
event | data: object | Event payload relayed from a subscribed channel. See "Relay envelope" below. |
spotify:now-playing | data: object | Spotify playback-state update relayed from spotify:\{account_id\}. |
chat:message | data: object | Chat event relayed from chat:\{account_id\} (new messages, deletions, moderation logs, etc.). |
automation | data: object | Automation-engine update relayed from automations:\{account_id\}. |
Relay envelope
When a worker publishes to a Redis pub/sub channel, the API's event-relay worker forwards the raw payload to every subscribed WebSocket session, wrapped in a typed envelope. The type of the envelope depends on the Redis channel prefix:
| Redis channel | WebSocket envelope type | Published to sessions subscribed on |
|---|---|---|
lumio:events:\{account_id\} | event | events:\{account_id\} |
lumio:spotify:\{account_id\} | spotify:now-playing | spotify:\{account_id\} |
lumio:chat:\{account_id\} | chat:message | chat:\{account_id\} |
lumio:automations:\{account_id\} | automation | automations:\{account_id\} |
The relay is one-way (Redis to WebSocket) and message-preserving -- data contains the exact JSON the publisher sent (including fields like type, event_type, payload, etc. that are specific to the event family).
Example relayed follower event on events:\{account_id\}:
{"type":"event","data":{"type":"twitch:follower","payload":{"username":"viewer123"}}}
Error codes
code | Meaning |
|---|---|
UNAUTHORIZED | Caller lacks access to the requested channel (missing account access or permission). |
SUBSCRIBE_FAILED | Server-side failure while registering the subscription. |
INVALID_FORMAT | Message was not valid JSON or did not match any known client-message shape. |
MESSAGE_TOO_LARGE | Text frame exceeded the 64 KiB limit. |
Heartbeats and timeouts
The server ticks a heartbeat every 5 seconds and disconnects any session that has been silent for 10 seconds. Clients should either respond to WebSocket-level pings, send a {"type":"ping"} every few seconds, or maintain any other traffic to keep the connection alive. Standard WebSocket Ping control frames are auto-answered with Pong.
Example Channels
events:\{account_id\}-- Stream events (follows, subs, cheers, raids, redemptions, tips, channel online/offline, bot-command updates).spotify:\{account_id\}-- Spotify "now playing" state changes for the Spotify overlay widget.chat:\{account_id\}-- Live chat messages, deletions, moderation logs, user-treatment updates.automations:\{account_id\}-- Automation-engine lifecycle updates (triggers, execution progress).overlay:\{key\}-- Per-overlay updates for browser-source popouts. Public channel keyed by the overlay's non-enumerable key.