# Relay rooms Relay rooms are the multiplayer tier that needs no server code. Every game with SDK multiplayer approved gets them automatically: hosted lobbies, shareable invite codes, public quick match, reconnect handling, and message fan out, without registering a server module or running any backend. The same `joinRoom()` API drives all tiers, so a game can start on relay rooms and graduate to a refereed module or an external authoritative server later without client rewrites. ## What the server owns (and what it refuses to) The relay server authoritatively owns everything it can own *generically*: - the roster and seat cap, - host designation and succession, - admission (signed tickets, room binding, per-seat connection caps), - message fan out with rate and size guardrails. It deliberately does not interpret game payloads, so it cannot referee them. Relay outcomes are client trusted: relay rooms never emit `match_end`, never report results to Bounty Board, and never feed win/loss records or leaderboards. Games that need authoritative results (anything ranked, recorded, or reward adjacent) must use a refereed Bounty hosted module or a registered external authority instead. ## Joining ```js import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer'; const room = await joinRoom({ match: true, // or create: true / code: 'ABCD' joinData: { roomSize: 4, skin: 'golem' }, }); ``` - `create: true` mints a room with a four-character invite code (`room.code`). - `code` joins a friend's room. - `match: true` enters the game's open public room, or founds one. All three go through the standard signed ticket flow; nothing about relay rooms weakens admission. ### Room size The room's **first joiner founds it** and fixes the seat count from `joinData.roomSize`: an integer from 2 through 64. Anything else (absent, out of range, non-integer) founds the room at the default of 8. Later joiners cannot renegotiate the size; once the room empties completely, the next arrival re-founds it. Ship the same `roomSize` from every client of your game so matchmade rooms are founded consistently. Quick match fills rooms to the founded size. Because matchmaking reserves seats conservatively, a burst of simultaneous quick match joins can briefly overshoot a small room; losers of that race receive `room_full` and the SDK automatically retries into the next room. ## Messages `room.send(payload)` relays `payload` to **every** player in the room, including the sender. There are no private messages: every client sees every payload, so never send secrets (hidden roles, private hands) through a relay room. Payload semantics are entirely yours. The server never reads them. Messages are batched per server tick and delivered through `room.on('event')`: ```js room.on('event', event => { if (event.type === 'relay') { for (const { from, data } of event.messages) { handlePeerMessage(from, data); // from = room-scoped player id } } if (event.type === 'relay_host') { setHost(event.hostId); // null only while the room is empty } }); ``` Guardrails (traffic over the limit is dropped silently; the state snapshot's `dropped` counter is the debugging breadcrumb): | Limit | Value | | --- | --- | | Message size | 1 KB of JSON per message | | Per player | 15 messages per second | | Per room | 120 messages per second | | Delivery | Batched per tick (20 Hz), ~50 ms worst-case added latency | Design for these budgets: send compact deltas on a timer, not per-frame positions. A 4-player game sending 10 messages per second each fits with headroom. ## Host The oldest seat in the room is the host. `room.state.hostId` carries it (compare with your own `room.playerId`), and host changes broadcast `{ type: 'relay_host', hostId }`. A disconnect inside the 15-second reconnect grace keeps both the seat and the host role, so a brief network blip does not thrash host succession. Use the host as your game's coordinator: it can own spawn timing, level seeds, or authoritative enough game state for casual play. Remember the trust model. A modified client can lie, which is acceptable for casual co-op and couch style games and not acceptable for anything with stakes. ## State snapshots Relay room state is a minimal header, identical for every viewer: ```json { "mode": "relay", "hostId": "p3f9…", "size": 4, "dropped": 0 } ``` It arrives in the `joinRoom()` welcome (read it before waiting for events) and refreshes at a low heartbeat cadence via `room.on('snapshot')`. Roster changes arrive as `playerJoin`/`playerLeave` events and `room.players` stays current. ## Rooms are endless Relay rooms never settle: there is no `end` event, and a room lives while it has players (seats survive a 15-second reconnect grace). Implement your own notion of rounds or matches in game messages, and call `room.leave()` when the player exits. Leaving is immediate and never reconnects. ## Graduating to a refereed tier If your game outgrows client trust (ranked results, tournaments, anything paid), the ticket flow, room codes, and quick match all stay the same; the room's simulation moves server-side. Contact Bounty Board about a refereed Bounty hosted module, or keep your own server and register it under the external authority contract (see `external-authoritative-servers.md`).