Multiplayer rooms
For games. Rooms are real-time sessions over one WebSocket: quick match, private rooms with a 6-letter code, a public lobby, message relay, shared room state, per-player state, host migration and reconnection. No server code to write.
Join a room#
const room = await kumo.rooms.quickMatch('duel'); // resolves when a room is ready
// or
const room = await kumo.rooms.create('duel', { private: true });
showCode(room.code); // e.g. "K7QM3X"
// the other player:
const room = await kumo.rooms.join('k7qm3x'); // room id or code, case-insensitive| Method | Notes |
|---|---|
rooms.quickMatch(mode) | joins the fullest open public room of that mode, or waits in a queue until minPlayers are ready (or fillTimeoutSeconds pass) |
rooms.cancelMatch() | stop waiting (the pending quickMatch rejects with match_cancelled) |
rooms.create(mode, opts) | private, maxPlayers, metadata (≤ 2 KB, shown in the lobby), custom code, hostOnlyState |
rooms.join(idOrCode) | room_not_found, room_full, room_locked |
rooms.list(mode?) | open public rooms for a lobby, most players first |
Modes come from config. A project without modes gets a built-in default mode (2–8 players) so two browser tabs
can meet before you write any config:
{ "multiplayer": { "modes": [
{ "key": "duel", "minPlayers": 2, "maxPlayers": 2, "fillTimeoutSeconds": 20 },
{ "key": "party", "minPlayers": 1, "maxPlayers": 8, "fillTimeoutSeconds": 0 }
] } }Talk#
room.send('move', { x, y }); // to everyone else (fire-and-forget, ordered)
room.send('hit', { dmg: 3 }, { to: [targetId] }); // to specific players
room.on('message', ({ from, type, data }) => { … });
await room.setState({ round: 2 }); // shared state: everyone (you too) gets 'stateChanged'
await room.setMyState({ ready: true, skin: 'red' }); // your player state: 'playerStateChanged'
room.state; // current shared state
room.players; // [{ id, displayName, connected, joinedAt, state }]State patches are applied in the same order on every client (a null value deletes a key), so all clients agree.
Use send for high-frequency data (positions), state for things late joiners must see (scores, seats, settings).
Host#
room.hostId / room.isHost: the earliest-joined connected player. If the host leaves or drops, the next player
becomes host immediately (hostChanged), and a returning player never takes it back. Use the host for authority:
simulate on the host and broadcast snapshots. With hostOnlyState: true, only the host can setState.
room.lock() (host only) stops new players from joining a match in progress.
Events#
| Event | Payload |
|---|---|
message | { from, type, data } |
playerJoined / playerLeft | player / { playerId, reason: 'left' | 'timeout' } |
playerDisconnected / playerReconnected | { playerId } — their seat is kept while they reconnect |
stateChanged | { patch, state, by, version } |
playerStateChanged | { playerId, patch, state } |
hostChanged | { hostId, previousHostId } |
reconnecting / resumed | your own connection dropped / came back (state refreshed from a snapshot) |
closed | { reason } — left, seat_expired, connection_lost, replaced, shutdown, unauthorized |
kumo.rooms.on('connection', state => …) reports connecting, open, reconnecting, closed.
Reconnects and page reloads#
The SDK reconnects automatically with backoff; the server keeps your seat for 20 seconds. After a page reload the
Room object is gone but the seat is not:
const held = await kumo.rooms.fetchHeldRoom(); // { roomId, mode, code, expiresInMs } or null
if (held) room = await kumo.rooms.rejoin(); // back in, state restored from a snapshotOr just start something new — create, join or quickMatch release the old seat as a normal leave.
Peer-to-peer mode#
By default every message goes through KUMODeck's servers. A mode with "transport": "p2p" instead sends game
messages directly between players over WebRTC. KUMODeck still does the tedious parts: matchmaking, room codes,
joining and leaving, choosing the host, relaying the WebRTC handshake, and handing out short-lived TURN relay
credentials. Your code does not change:
{ "key": "coop", "minPlayers": 2, "maxPlayers": 4, "transport": "p2p", "p2p": { "maxPlayers": 4, "relay": "fallback" } }const room = await kumo.rooms.quickMatch('coop');
room.transport; // 'p2p'
room.send('pos', { x, y }, { reliable: false }); // unordered, no retransmit: good for positions
room.on('peer', ({ playerId, state, relayed }) => { … }); // connecting | connected | failed | closed
room.peers; // [{ playerId, state, relayed }]The SDK connects everyone to everyone (a mesh), uses the TURN relay as p2p.relay says (below), and keeps
shared state working when the host leaves: the server picks the next host (same rule as above), which continues
from its copy of the state, and unconfirmed setState calls are resent to it.
Warning — no referee. In peer-to-peer rooms the host's browser decides the shared state, and a modified client can send anything. Use the default server transport for competitive, prize or paid matches.
Warning — IP addresses. A direct WebRTC connection shows each player's IP address (roughly where they live) to the others. Choose with
p2p.relay:
p2p.relay | What happens | Gives up |
|---|---|---|
always | every message goes through the TURN relay; only the relay's address is visible; connects wherever the relay is reachable | relay traffic is billed by the amount sent |
fallback | direct first; the relay only when a direct connection fails | players see each other's IP address when direct |
never | direct only; never any relay cost | players who cannot reach each other directly fail to connect (peer event state: 'failed') |
Which to pick, and the price per match of each way, is in Play online together: the
multiplayer Skill asks the creator once and picks it.
| Players | 2–8 per mesh (p2p.maxPlayers, default 4 — every player uploads to every other player). A full mesh refuses joins with p2p_room_full (details.max) |
| Cost | relay traffic is billed at cost as turn_egress_bytes (no markup); signaling is tiny |
| Errors | webrtc_unavailable (no WebRTC in this environment), turn_unavailable (a relay is required but not available), setState rejects with timeout if the host cannot be reached |
| State | the same limits as server rooms (64 KB shared, 8 KB per player); it disappears when everyone leaves |
To try it, open the game in two different browsers (or one normal and one private window), quick match the same
p2p mode, and check room.peers shows connected in both.
Limits#
| Limit | Value |
|---|---|
| Messages | 30/s per connection (burst 60); excess is dropped with a warning event |
| Message size | 16 KB (payload_too_large) |
| Shared state | 64 KB total; player state 8 KB each |
| Players per room | up to 64 (per mode) |
| Idle socket | closed after 10 minutes outside any room (reconnects on the next call) |
One browser profile = one player: a second tab replaces the first tab's connection. To test multiplayer on one
machine, give each tab its own storage (the templates use ?player=2).
Note Rooms relay and synchronise; they do not run your game logic on the server. Competitive games should be host-authoritative (see
examples/sky-duel), or run the match logic in your own Functions (Durable Objects give you stateful WebSocket rooms).