Skip to content

Realtime / Socket API 0.1 — De Facto Specification

Status: Descriptive. Documents the WebSocket (Socket.IO) contract as it ships today (retroactively numbered 0.1; unversioned). Written from production source on branch version/0.40.0. Quirks flagged ⚠︎.

1. Transport & rooms

Socket.IO server created at index.ts:176 (new Server); connection handler at index.ts:439 (io.on('connection')). Rooms are campaign-scoped (joinCampaign / leaveCampaign) with additional map-scoped presence (presence:updateMap). Entity mutations broadcast to the relevant campaign room so all clients converge without polling.

Identity (v0.95.0, G.9). Every socket carries an actor or is refused at the handshake: the client sends the identity provider's token as auth: { token } (an auth function in the client, re-read on every reconnect), and middleware/socketAuth.ts verifies it the way requireAuth does for HTTP, putting an Actor on socket.data. Without a CLERK_SECRET_KEY the socket is dev_user, as the HTTP side is. A campaign room is joined through the permission resolver — joinCampaign and presence:join join only when the actor may read the campaign, and answer joinCampaign:denied { campaignId } otherwise. user:join joins the actor's own room and ignores its argument; presence entries carry the actor's id, not the client's claim. The relay events (initiative:*, requestRoll:*, targetingPreview:*) carry no private data and are unchanged. See Permissions, read routes and the Gatekeeper.

2. Event catalog

Client→server handlers are registered inline in index.ts; server→client broadcasts are emitted from the per-domain helpers in events/ (chatEvents.ts, drawingEvents.ts, mapObjectEvents.ts, tokenEvents.ts, rollRequestRelay.ts).

Group Events
Lifecycle connection, disconnect, ping/pong, joinCampaign, leaveCampaign, user:join, user:leave
Presence / cursor presence:join, presence:updateMap, presence:leave, cursor:move, cursor:hide, focusAllPlayers
Entity CRUD broadcasts (:created/:updated/:deleted) token, drawing, mapObject, fogShape, vblShape, chatMessage, map (map:updated), plus value-tables: currency, damageType, lightSourceType, visionType
Token drag (live) tokenDrag:start, tokenDrag:move, tokenDrag:end
Initiative initiative:start, initiative:update, initiative:movement, initiative:end, initiative:request
Dice dice:roll (broadcast a roll for synced 3D dice — see dice-api-0.1.md)
Roll-request relay requestRoll:request, requestRoll:response (§3)
Media / voice image:ready, requestTranscription, voiceTranscription
History (v0.100.0) history:commit — the commit row (with restoreOf) after every commit of a campaign, map or character, to the entity's audience; viewstate:changed with entity: 'history' after a restore. See the commit path.

Handler line anchors (server): joinCampaign index.ts:460, requestRoll:request :486, requestRoll:response :565, presence:join :587, initiative:start :638.

3. Roll-request relay (ties to the Action API)

requestRoll:request / requestRoll:response (events/rollRequestRelay.ts) implement the socket half of the Action API's target.requestRoll(...) (see action-api-0.1.md §5). The server relays a roll request to the token owner's client and pipes the response total back to the requester, carrying the action invocation marker so the owner's roll groups into the requesting action's chat frame.

4. Client SDK surface — ⚠︎ contract drift

packages/client-sdk wraps the socket in WebSocketManager (WebSocketManager.ts:14) with AuthClient/ChatClient/GameClient/VBLClient facades.

⚠︎ The SDK's socket contract does not match the live server. The SDK emits chat:join, chat:leave, game:join, game:leave, game:action (WebSocketManager.ts:128-149, GameClient.ts:64) — none of which the server handles (the server uses joinCampaign, entity :created/… broadcasts, etc.). The VBLClient entity subscriptions (vblShape:*, lightSourceType:*) do match. So the SDK is partially stale relative to the apps/web socket usage, which is the real client.

5. Known gaps

  1. Unversioned, untyped event payloads (many handlers take any).
  2. Client-SDK drift (§4) — the SDK targets an older/aspirational event set.
  3. No shared event-name registry — event strings are duplicated literals across server handlers, events/* emitters, and apps/web listeners; renaming is a manual cross-cut.