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
- Get a seat. Planned for the alpha:
POST https://api.lobbylab.gg/v1/rooms/joinwith{"code":"KXPTQM"}returnsseat.wsUrl,seat.memberIdandseat.credential. - 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>"}
- The server answers
ready, then asnapshot(turn-based rooms) orframemessages (live rooms), and amemberslist. Without ahelloin 5 seconds, the server closes with4401.
Client messages
| type | Fields | Use |
|---|---|---|
hello | wireVersion, sessionId (1 to 200 characters), credential (16 to 2,000), logicalStreamId (8 to 200) | The first message on every socket |
intent | intentId (8 to 200 characters, unique), clientSequence (goes up on each socket), name (1 to 100), payload, optional basedOnRevision | A change or action. Retrying with the same intentId is safe |
input | seq, 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 |
ping | nonce | The server answers pong with the same nonce |
resync | none | Asks for a fresh snapshot |
Server messages
| type | Meaning |
|---|---|
ready | You are seated: memberId, phase, revision, roles; in live rooms also the tick, frame and input rates |
members | The member list (memberId, displayName, connected, roles), sent on every change |
snapshot | Turn-based rooms: revision, viewHash, your view after a change, and its events |
frame | Live rooms: tick, serverTimeMs, your view, events and inputAck (your highest input seq used) |
ack | Your intent was accepted: intentId, triggerSequence, revision |
rejection | Your intent was refused: code, retryable, and intentId when known |
phase | The room phase changed; ended means the room closed |
pong, fatal_error | Liveness 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
| Code | Meaning | What a client does |
|---|---|---|
4401 | Bad or expired seat credential | Renew the seat once, then reconnect; stop if that fails |
4403 | Removed (left, kicked, banned, room closed) | Stop and report the reason |
4400 | A message the server could not accept | Stop |
4429 | Rate or abuse limit | Stop and show the error |
1011, 1006, 1001, any other code, or no answer | Transient | Reconnect after 0.8 s, 1.6 s, 3.2 s, then every 5 s |
Staying connected
- The server sends WebSocket protocol pings every 4 seconds and drops a socket after 2 missed pings. Browsers answer these on their own.
- A new
hellowith the same seat returns the same member and a fresh snapshot or frame, so state is current after every reconnect.