LobbyLab wire protocol (multiplayer-wire/v0)

LobbyLab is a hosted multiplayer backend. Your game keeps its own UI, engine and domain; players join LobbyLab rooms over WebSockets through @lobbylab/client, and LobbyLab keeps the rooms, invite codes, matchmaking and shared game state. Game rules that run on LobbyLab's servers come later. Request an alpha key at https://lobbylab.gg/developers.

Status: multiplayer-wire/v0 runs every room on lobbylab.gg today. The Multiplayer API alpha reuses it on its own socket path. The REST routes that hand out a seat and the lobbylab.* intent names below arrive with the alpha and may change before then. The SDKs hide this protocol; you need it only to write a client in another language.

Messages are JSON text frames. Every message has a type.

Handshake

  1. Get a seat. Planned for the alpha: POST https://api.lobbylab.gg/v1/rooms/join with {"code":"KXPTQM"} returns seat.wsUrl, seat.memberId and seat.credential.
  2. Open a WebSocket to seat.wsUrl. Within 5 seconds, send:
{"type":"hello","wireVersion":"multiplayer-wire/v0","sessionId":"<room id>","credential":"<seat credential>","logicalStreamId":"<one id per tab, 8 to 200 characters>"}
  1. The server answers ready, then a snapshot (turn-based rooms) or frame messages (live rooms), and a members list. Without a hello in 5 seconds, the server closes with 4401.

Client messages

typeFieldsUse
hellowireVersion, sessionId (1 to 200 characters), credential (16 to 2,000), logicalStreamId (8 to 200)The first message on every socket
intentintentId (8 to 200 characters, unique), clientSequence (goes up on each socket), name (1 to 100), payload, optional basedOnRevisionA change or action. Retrying with the same intentId is safe
inputseq, payload (at most 1 KiB)Live rooms only: your whole player slot. seq must keep increasing across reconnects, so start it at the current time in milliseconds
pingnonceThe server answers pong with the same nonce
resyncnoneAsks for a fresh snapshot

Server messages

typeMeaning
readyYou are seated: memberId, phase, revision, roles; in live rooms also the tick, frame and input rates
membersThe member list (memberId, displayName, connected, roles), sent on every change
snapshotTurn-based rooms: revision, viewHash, your view after a change, and its events
frameLive rooms: tick, serverTimeMs, your view, events and inputAck (your highest input seq used)
ackYour intent was accepted: intentId, triggerSequence, revision
rejectionYour intent was refused: code, retryable, and intentId when known
phaseThe room phase changed; ended means the room closed
pong, fatal_errorLiveness answer; unrecoverable error with a code

Intents planned for the alpha

{ name: "lobbylab.state", payload: { ops: StateOp[]; guards?: { path: string; equals: Json }[]; ifVersion?: number } }
{ name: "lobbylab.send",  payload: { topic: string; data: Json; to?: "all" | "others" | "host" | string[]; echo?: boolean } }
{ name: "lobbylab.host",  payload: { action: "kick" | "lock" | "meta" | "transfer" | "close"; ... } }

Close codes

CodeMeaningWhat a client does
4401Bad or expired seat credentialRenew the seat once, then reconnect; stop if that fails
4403Removed (left, kicked, banned, room closed)Stop and report the reason
4400A message the server could not acceptStop
4429Rate or abuse limitStop and show the error
1011, 1006, 1001, any other code, or no answerTransientReconnect after 0.8 s, 1.6 s, 3.2 s, then every 5 s

Staying connected