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
- The game opens the lobby with a return URL and its player limit:
/?return=<game-url>&maxPlayers=4. - The host creates a room. The return URL and player limit are stored on the room.
- Guests open the join link (
?room=<id>), enter a name, and the server sends the stored return URL and limit injoined. - 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> - The game connects to
<ws-url>and joins the same room with{"t":"join","room":"…","token":"…","name":"…"}.
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
| endpoint | purpose |
|---|---|
GET / | Lobby UI. |
GET /guide.html | This page. |
GET /challenge | {challenge, bits} for room-creation hashcash. |
GET /qr?text=… | SVG QR code, text max. 512 chars. |
GET /healthz | ok. |
WS /ws | Room 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
returnUrlis stored at create and sent to every participant. It must be anhttp/httpsURL without credentials and at most 1024 characters.maxPlayersis per room, clamped to 1…12 (the server hard cap). A join beyond the room limit gets{"t":"error","code":"full"}.- The lobby client only redirects to origins in
ALLOWED_RETURN_ORIGINS(public/return-url.js). Add your game origin there.
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).
- For 4 players, a full state with compact numeric card ids is roughly 1.5–2 KiB.
- 5 KiB/s is enough for a turn-based game if you coalesce state broadcasts (e.g. at most every 250–500 ms) or send small move events and full state only on start/reconnect.
- If you send a full JSON state with card objects after every single card, raise
RATE_KBPS(e.g. 16) or switch to deltas.
Security and caveats
- WebSocket upgrades are checked against
ALLOWED_ORIGINSwhen the browser sends anOrigin. Add every game/lobby origin you deploy; non-browser clients without an Origin are allowed. - HTTP responses set
nosniff,no-referrer,frame-ancestors 'none'and a restrictive CSP. - Fairness and cheating are not enforced. The host is just whoever created the room. Host-authoritative play is fine for friends; do not use this for ranked or public competitive games without server-side validation.
- Rooms live in process memory only. They disappear on restart and expire after
ROOM_TTL_MIN/IDLE_TTL_MIN. - One server instance only. No shared state, no sticky sessions, no persistence.