Skip to content

Concepts

A room is an authoritative simulation with attached clients. Your game implements RoomAuthority — validation, simulation and snapshots — and the room engine (RoomHub) owns everything else: transports, the session handshake, acks, rate limits, snapshot encoding and the social lane.

The boundary is strict in both directions:

  • Networking never imports physics, UI or game types.
  • The game never sees a socket; it sees peer ids, commands and ticks.

Clients send requests (inputs, publishes, chat); the authority owns all state; everyone else receives snapshots. That is the entire security model — a misbehaving client can only ever change what the authority lets it change. The full call-site contract is specified in /spec/room-authority.md.

One room engine runs everywhere; hosting is a composition choice, not an architecture:

Shape Entry point Transport in
Dedicated server RoomServer (server_sdk), binds HTTP and upgrades /play/<roomKey> WebSocket requests Real WebSocket listener (native only)
In-process host RoomHub + MemoryTransportPair (protocol) Memory pipe, no sockets
Player-hosted RoomHub + any GameConnection adapter (e.g. WebRTC) Anything implementing the connection interfaces
Rooms node packages/node — many rooms in one process behind a supervisor One port, /play/<roomKey> + /v1 HTTP API
  1. Join. The client sends a join frame with a protocol version (exact match required), a player name and optional game fields. The hub validates, calls validateJoin, assigns a stable peer id (p1, p2, …) and answers welcome — with a fresh one-time resume token — followed by a baseline, an initial snapshot and the roster (with chat history).
  2. Play. Commands flow client → authority; snapshots flow hub → clients at snapshotHz.
  3. Disconnect. A lost transport starts a grace window (default 15 s). The player’s slot and state are reserved; the roster shows them as disconnected but present.
  4. Resume. Reconnecting with the resume token inside the window restores the same peer id, replays roster/chat history and closes any old transport. Resumes never need a new join token from the control plane.
  5. Reap. Past the grace window the player is removed for good (removePlayer), their row disappears from snapshots, and the next join is a fresh id.

A resume token bound to a verified transport identity cannot be used by another identity.

Snapshots flow through SnapshotEncoder as baseline-relative deltas:

  • A reliable full baseline is refreshed once per second and on every epoch change. Every delta refers to the current baseline — never to another delta — so a lost or reordered delta cannot corrupt the next one; the next baseline always repairs the receiver.
  • Disposable deltas ride a separate (optionally unreliable) state lane between baselines. Receivers that miss too much get display-only recovery snapshots instead of a new baseline.
  • Floats are quantized to 4 decimal places with a float32 deadband, so sub-quantum changes drop out entirely; presence masks preserve resets to zero, false and null.
  • Acknowledgment runs the other way: every applied snapshot tick is acked, and a receiver that stops ackging for > 5 s while blocked is evicted.
  • When the transport supports binary and a WireStateCodec (e.g. FlatBuffers) is configured, states are encoded as binary frames; JSON text remains the fallback, and the two can share a transport during migration.

The exact frame shapes are in the wire protocol reference and /spec/rooms-protocol.json.

Chat, pings, roster and moderation ride the same reliable lane as control frames — never the disposable state lane:

  • Chat text is single-line, capped at 280 characters, rejects hidden control characters and bidi overrides; history caps at 100 entries; rate limit 4 messages / 10 s per player.
  • Pings (here, help, ball) carry authority-supplied locations and expire after 8 s; speech bubbles after 10 s.
  • The room leader can lock the room, and kick or ban players. Bans require an identity verified by the transport adapter — never a join payload — and the room-lifetime ban list caps at 256 identities.

When you do not want to write Dart at all, the rooms node runs platform-provided authorities configured by JSON room configs:

  • Relay — client-authoritative state rows (presence, chat, small published state), quota-enforced. Your game publishes key/value rows; the server owns quotas and replication.
  • Movement — server-simulated physics over configured static colliders with spawn points, movement parameters and an input whitelist. Positions are server-simulated and server-validated (anti-cheat by construction).

See the room templates reference for the config schemas.


Distilled from docs/protocol.md and the source of packages/protocol; the wire protocol reference embeds the repository document verbatim.