← Game library
# Arcade Lobby API v1

Base URL: https://coralcourt.ddns.net/api/lobby
Use relative `/api/lobby` URLs from hosted games. All responses are JSON. Send `Content-Type: application/json` for JSON bodies. Errors return `{ "error": "message" }` with a non-2xx HTTP status.

## What this provides

One shared landing-page lobby with live public chat and online players. Game-session rooms, ready checks, host-controlled start/reset, ordered player events and host snapshots remain available to future game integrations; the landing page no longer exposes room creation or joining. Current arcade games remain single-player. A future game must implement multiplayer simulation and rendering itself.

Chat and public presence use Server-Sent Events (SSE). Gameplay state/events still use HTTP polling for turn-based games and lightweight multiplayer prototypes. It is not a low-latency WebSocket server or a cheat-proof authoritative game server. For fast action games, add a suitable authoritative real-time service rather than streaming every animation frame through this API.

## Sessions and identity

POST /session
Body: {"name":"Eric"}
201 response: {"id":"player-uuid","name":"Eric","token":"secret-token","expiresAfterInactiveSeconds":90}

Names are 1–24 characters, unique among active sessions, and cannot contain control characters or angle brackets. They are unverified guest display names, not accounts. The server stamps chat and events with the session's player ID; never trust player IDs inside client event data.

All endpoints except GET /api/lobby and POST /session require:
Authorization: Bearer YOUR_TOKEN

GET /session returns {id,name,room}. It also refreshes presence. Any authenticated API call refreshes presence. Poll at least every 15 seconds while connected. Players disappear from the online list after 45 seconds without contact and their sessions/room membership expire after 90 seconds. Background tabs may expire. On 401, return to the lobby sign-in flow.

DELETE /session signs out and leaves the room.

The landing page uses a session heartbeat every 20 seconds; this does not fetch messages. It stores `{id,name,token}` under sessionStorage key `arcade-lobby-session`. Games launched in the same tab can reuse it. Never put the token in query strings, chat, logs or leaderboards. A separate tab must establish its own session if it does not have this key. The room and match IDs in the game URL are public identifiers, not authentication.

## Live public lobby and chat

GET /api/lobby/stream
A public Server-Sent Events connection (Content-Type: text/event-stream). Open it with EventSource('/api/lobby/stream') and listen for the named `lobby` event. event.data is JSON with the same full snapshot as GET /api/lobby. Each message or presence change is pushed to connected clients. Initial connect/reconnect always sends a full current snapshot. No authentication token belongs in the stream URL; chat is public, while posting still requires a Bearer token.

Use one connection per page and close it on page exit. EventSource reconnects automatically (server retry hint: 3 seconds); this is a reconnect delay, not message polling. The server sends comment heartbeats every 15 seconds to keep proxies connected. Buffering is disabled on the Nginx stream route. Up to 150 streams are allowed, and slow consumers are disconnected to bound memory use. A disconnected client should show a reconnecting indicator and restore its view from the next full snapshot. Use GET /session occasionally to maintain authenticated presence; opening a public stream alone does not keep a signed-in session alive.

GET /api/lobby
Returns {version:1,players:[{id,name}],rooms:[{id,name,game,status,count,capacity}],messages:[{id,playerId,name,message,time}],games:[{id,title,url,minPlayers,maxPlayers}]}.

POST /chat
Body: {"message":"Anyone want to play?"}
201 returns the message record. Messages are 1–300 characters; at most one message per second per session. Render all user-supplied text with textContent, never raw HTML. The public lobby returns the latest 50 messages; up to 200 messages are retained for two hours in memory. Lobby sessions, chat, rooms and game state clear on backend restart. Leaderboard scores remain persistent JSON.

## Rooms

POST /rooms
Body: {"name":"Friday crew","game":"my-game"}
201 returns the full room. Use `open-play` for a social room. Otherwise the game must be registered. A player can belong to one room at a time. Maximum 50 rooms and 8 players per room; registered games specify a smaller capacity if needed.

POST /rooms/{roomId}/join — Join a waiting room. No body required.
POST /rooms/{roomId}/leave — Leave the room. No body required.
GET /rooms/{roomId} — Full state, members only.
POST /rooms/{roomId}/ready — Body: {"ready":true}.
POST /rooms/{roomId}/start — Host only; all players ready and minimum player count met.
POST /rooms/{roomId}/reset — Host only; returns to waiting and clears match state/readiness.

Full room shape:
{id,name,game,status,hostId,capacity,players:[{id,name,ready}],match,snapshot,cursor}

Successful start sets status to `playing` and creates:
match: {id,seed,startedAt,url}

Future game launch controls should open the registered URL with `?room=ROOM_ID&match=MATCH_ID` in the same tab. Validate these against GET /rooms/{roomId}; don't trust query parameters alone. Social rooms cannot start a match. Playing rooms cannot accept new members.

If the host leaves or expires, the next player becomes host and the running match is cancelled back to waiting. Clients must stop simulation when the match ID disappears/changes. An empty room is deleted. Non-host departure updates the roster; the game must decide whether to continue or ask its host to reset.

## Game events and snapshots

POST /rooms/{roomId}/events
Body: {"matchId":"current-match-id","clientEventId":"unique-per-player-event-id","type":"input","data":{"dx":1}}
Types: input or action. Data may be any JSON value, maximum 2048 serialized characters. Rate: one new event per 100 ms per session; the HTTP proxy also limits overall traffic. Prefer low-frequency logical commands, not frame-by-frame input.

201 returns {seq,playerId,clientEventId,type,data,time}. The server assigns sender and room sequence. Repeating the same clientEventId in the same match/player returns the original event (200) while it remains in the 256-event retention window. Do not retry old events beyond that window; game logic should also deduplicate action IDs. Never replay commands across match IDs.

GET /rooms/{roomId}/events?after=LAST_CURSOR
Returns {matchId,cursor,resync,snapshot,events}. Only members can read. Keep a per-match cursor; process events in sequence and advance after applying them. `resync:true` means the cursor is outside retained history: restore a valid host snapshot, discard obsolete input, and replay only events after snapshot.atCursor. If no usable snapshot exists, request a reset or wait for the host to publish one.

POST /rooms/{roomId}/snapshot
Host only. Body: {"matchId":"current-match-id","revision":1,"appliedThrough":1,"state":{"turn":1}}
Returns {revision,state,atCursor,updatedAt}. Revision must increase, state is at most 8192 serialized characters, and updates are limited to one per 250 ms. `appliedThrough` must be the last event sequence actually applied by the host (0 before any events), between 0 and the current room cursor and never decreasing. The server records it as snapshot.atCursor. This prevents events arriving during snapshot publication from being accidentally skipped.

The host implements the game's rules, validates commands against membership, turn order and legal moves, and publishes the resulting state. Other clients render the host's snapshot and may interpolate visually. The relay does not validate game-specific commands or scores. Do not claim competitive anti-cheat protection.

Use SSE for public lobby messages/presence; do not poll for messages. Recommended gameplay polling: game room/events every 1–2 seconds initially. Keep total traffic per session below 10 requests/second. Handle 429 with backoff and jitter, 401 by rejoining, 403 as a permissions error, 404 as room closure, and 409 by reloading room/match state. Pause updates in hidden tabs and reconnect safely on return. Do not blindly retry non-idempotent actions such as chat or room creation.

## Registering a future multiplayer game

Administrator configuration is a JSON array in:
/mnt/TrueNAS/Apps/ngix-data/multiplayer-games.json

Add an entry, preserving existing ones:
{"id":"my-game","title":"My Game","url":"/My-Game.html","minPlayers":2,"maxPlayers":4}

The URL must be a same-site HTML path, with no query/fragment, parent-directory segments or external host. Player limits must be integers from 2 to 8. Restart only the backend after validating the file; active lobby sessions and rooms will reset. No configuration-write endpoint is public.

The game HTML should include:
<meta name="leaderboard-game" content="my-game">

Scores use the existing separate endpoints:
POST /api/score with {"game":"my-game","player":"Eric","score":12500}
GET /api/leaderboard?game=my-game&limit=5

Do not write scores.json from the browser. This leaderboard API accepts client scores; future games must decide who submits each finished result and prevent duplicate submissions.

## Deployment notes

Backend source: /mnt/TrueNAS/Apps/ngix/.nginx/leaderboard.cjs
Persistent scores: /mnt/TrueNAS/Apps/ngix-data/scores.json
Lobby registry: /mnt/TrueNAS/Apps/ngix-data/multiplayer-games.json
Landing page assets: /mnt/TrueNAS/Apps/ngix/lobby-assets/
API guide: https://coralcourt.ddns.net/lobby-api/
Game discovery: /_catalog/ — preserve this endpoint.

Keep the backend single-instance. Sessions/chat/gameplay state are bounded in memory; do not scale replicas without shared state. Do not place score data or the registry inside the publicly served game folder. Back up source/configuration before upgrades and test with two independent sessions.