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 branchversion/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
- Unversioned, untyped event payloads (many handlers take
any). - Client-SDK drift (§4) — the SDK targets an older/aspirational event set.
- No shared event-name registry — event strings are duplicated literals
across server handlers,
events/*emitters, andapps/weblisteners; renaming is a manual cross-cut.