Lobby integration guide

Generic WebSocket room relay for minigames. The server knows rooms, tokens and whether a room has started. It does not run or validate game logic.

Quick flow

  1. The game opens the lobby with a return URL and its player limit: /?return=<game-url>&maxPlayers=4.
  2. The host creates a room. The return URL and player limit are stored on the room.
  3. Guests open the join link (?room=<id>), enter a name, and the server sends the stored return URL and limit in joined.
  4. The host sends {"t":"start"}. Every lobby client then opens the return URL with the room id and its credentials:
    <returnUrl>?room=<id>#token=<token>&name=<name>&lobby=<ws-url>
  5. The game connects to <ws-url> and joins the same room with {"t":"join","room":"…","token":"…","name":"…"}.
The lobby page and the game are usually different origins. sessionStorage is not shared, so the token is passed in the URL fragment. Fragments are not sent to the server or in the Referer, but they do appear in browser history. Do not paste game URLs with tokens into public places.

Navigate in the same tab (the lobby uses location.replace). Joining with the same token replaces the old socket: the server closes it with 4000 replaced.

HTTP endpoints

endpointpurpose
GET /Lobby UI.
GET /guide.htmlThis page.
GET /challenge{challenge, bits} for room-creation hashcash.
GET /qr?text=…SVG QR code, text max. 512 chars.
GET /healthzok.
WS /wsRoom protocol below.

Under the preview gateway the lobby is mounted at /lobby/; prefix the HTTP and WebSocket paths above with /lobby there.

WebSocket messages

Client → server

{ "t": "create", "proof": "<challenge>:<n>", "name": "Ada",
  "returnUrl": "https://co3d.eu/dreiziehn/dreiziehn.html", "maxPlayers": 4 }

{ "t": "join", "room": "<room-id>", "token": "<token or null>", "name": "Bob" }
{ "t": "start" }
{ "t": "kick", "id": "<peer id>" }
{ "t": "msg", "data": { … } }

create is hashcash-gated. start and kick are host-only; a kicked player gets {"t":"closed","reason":"removed"} and its socket is closed. msg is forwarded verbatim to the other room members; the server never inspects data.

Server → client

{ "t": "joined", "room": "…", "token": "…", "you": "…", "name": "Ada",
  "host": true, "started": false, "returnUrl": "https://…",
  "maxPlayers": 4, "peers": [{ "id": "…", "name": "Ada", "host": true, "online": true }] }

{ "t": "peers", "started": false, "maxPlayers": 4, "peers": [ … ] }
{ "t": "started" }
{ "t": "msg", "from": "…", "name": "Bob", "data": { … } }
{ "t": "error", "code": "full", "message": "…" }
{ "t": "closed", "reason": "idle-timeout" }

joined is sent to the joining socket. peers is broadcast on join/leave/kick/start. msg goes to everyone except the sender, so a client must apply its own action locally.

Return URL and player limit

Game data: validate it

The relay is a dumb pipe. Any participant can send any data. Games must validate the envelope and the payload before using it. Include a data version and a type allowlist:

{ "v": 1, "type": "move", "to": "<hostId>", "move": { … } }
{ "v": 1, "type": "state", "state": { … } }
{ "v": 1, "type": "reject", "to": "<guestId>", "reason": "not your turn" }

Reject unknown v or type, non-object payloads, wrong recipients and out-of-range indices. For a friendly game, make the host authoritative: guests send moves to the host, the host validates and broadcasts the resulting state.

Rate limit and state size

Each participant has a token bucket of 5 KiB/s with a 1 s burst. Messages over the limit are dropped and answered with rate-limit (at most one warning per second).

Security and caveats