Wire protocol
The frame table below is derived from the implementation
(packages/protocol/lib/src/room_hub.dart) and kept honest by a build-time
cross-check. The machine-readable version — with limits, lanes and close
codes — is /spec/rooms-protocol.json.
All frames are JSON text. Client → server frames are capped at 4096 characters and rate-limited to 120 per second per peer. The handshake, social and error frames ride a reliable control lane; disposable state deltas prefer a separate state lane when the transport provides one.
Frame table
Section titled “Frame table”Handshake — client → server
Section titled “Handshake — client → server”| Frame | Fields | Semantics |
|---|---|---|
join |
type, protocol (int), name (string, fresh joins), resume (string, optional), binaryStates (bool, optional), game fields |
First frame on a fresh transport, within 5 s. protocol must exactly match. resume re-attaches a reserved slot; unknown/expired → error. The whole message goes to validateJoin. Rejections (banned, locked, full, invalid) → error + close 1008 |
Handshake — server → client
Section titled “Handshake — server → client”| Frame | Fields | Semantics |
|---|---|---|
welcome |
type, protocol, id (peer id), resume (one-time token), capacity, authority welcome() fields |
Sent after a successful join or resume; ids are stable across resume (p1, p2, …) |
baseline |
type, epoch, tick, full snapshot state |
Reliable full quantized snapshot; the reference point for all following deltas |
snapshot |
type, epoch, tick, initial (bool, optional), full state |
Sent with initial: true right after the baseline; refreshed ~1 s / on epoch change; recovery copies for stalled receivers carry initial: true too |
Input and social — client → server
Section titled “Input and social — client → server”| Frame | Fields | Semantics |
|---|---|---|
ack |
type, ack (int) |
Confirms the last applied snapshot tick. The ack field is honored on any frame (ackTick ≤ ack ≤ sentTick) and drives stall eviction |
chat |
type, text |
1–280 chars, single line; 4 msgs / 10 s. Rejection → socialNotice, no disconnect |
ping |
type, kind (here/help/ball) |
Location comes from the authority (RoomSocialAuthority.pingLocation), never the client; 3 / 5 s |
moderate |
type, action (lock/kick/ban), locked (bool, lock), playerId (kick/ban) |
Leader only. lock refuses fresh joins; kick/ban remove the target (removed frame + close 1008); ban records the transport-verified identity (≤ 256) |
leave |
type |
Voluntary leave; slot released immediately, reply left |
| game commands | type (game-defined), ack (optional), payload |
Everything else goes verbatim to authority.command() — e.g. relay publish/publishRoom, movement input, the example’s increment. Throwing from command disconnects the sender |
Social and errors — server → client
Section titled “Social and errors — server → client”| Frame | Fields | Semantics |
|---|---|---|
social |
type, leaderId, locked, members ([{id, name, connected, canBan}]), history (on join) |
Roster broadcast on every membership/lock change; the join copy carries chat history (≤ 100) |
chat |
type, message ({id, playerId, playerName, text, system}) |
User chat and system notices (joined / reconnected / left / removed) |
ping |
type, ping ({id, playerId, playerName, kind, location {x, y, z, epoch}}) |
Broadcast; expires after 8 s |
left |
type |
Sent to the peer that just left |
removed |
type, message |
Sent to a kicked/banned peer before close 1008 |
socialNotice |
type, message |
Rejected social attempt; sender only, no disconnect |
error |
type, message |
Any protocol error; always followed by close 1008 |
State replication — server → client
Section titled “State replication — server → client”| Frame | Fields | Semantics |
|---|---|---|
state |
type, base (int), tick, epoch, delta ({remove?, set}) |
Baseline-relative delta on the disposable lane. Deltas whose base/epoch do not match the receiver’s baseline are dropped and repaired by the next baseline |
Close codes
Section titled “Close codes”| Code | Reason | When |
|---|---|---|
| 1000 | Reconnected elsewhere |
the session resumed on a new transport |
| 1001 | Host closed the room / State receiver stalled |
room closed or drained; receiver unacked > 5 s while blocked |
| 1008 | Join timeout / Invalid request / Removed by host |
no join in 5 s; any protocol error; kick/ban |
Repository document
Section titled “Repository document”Rooms protocol overview
This is a compact description of the wire behavior implemented in
packages/protocol. It is rewritten from the protocol documentation that
ships with the code’s original consumer (a multiplayer golf game); the
protocol itself is game-agnostic.
Shapes
One room engine (RoomHub) 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 |
A consuming game implements RoomAuthority — validation, simulation and
snapshots. The networking never imports physics, UI or game types.
Session protocol
- Join/resume handshake. A client sends
joinwith aprotocolversion (exact match required), player name and an optionalresumetoken. The hub answerswelcomewith the peer id, a one-time resume token, capacity and a sequence seed, followed by the initial state. A disconnected peer’s slot is reserved for a grace window (default 15 s); resuming with the token restores the id and replays roster/chat history. A resume token bound to a verified transport identity cannot be used by another identity. - Fixed-step simulation. The hub ticks the authority at
simulationHz(default 120) on a fixed-step clock that never dilates simulated time by more than 0.1 s under load; snapshots are cut atsnapshotHz(default 20). - Input and acks. Clients send
input/control frames carrying a sequence and the epoch/tick they saw; the authority receives them throughcommand(). Every applied snapshot is acknowledged (ack); a bounded acknowledgment window plus reliable recovery frames keep a lossy or reordered lane from stalling a receiver. Input is rate-limited (default 120 packets/s, 4 KB frames); a stalled host legitimately buffers a bounded burst (600 packets, 1 s expiry) that ordinary floods do not get. Epoch changes invalidate stale commands. - State replication. Snapshots flow through
SnapshotEncoderas baseline-relative deltas: a reliable full baseline refreshed once per second (and on epoch change), disposable deltas in between. Deltas are quantized (4 decimal places by default) with a float32 deadband, so sub-quantum changes drop out; presence masks preserve resets to zero, false or null. A lost or reordered delta cannot corrupt the next one. - Binary state lane. When the transport implements
BinaryGameConnectionand aWireStateCodec(e.g. FlatBuffers) is configured, states are encoded as binary frames; JSON text remains the fallback. Binary frames never begin with{, so binary and JSON frames can share a transport during migration. - Eviction and capacity. Overloaded or silent receivers are evicted;
joins beyond
RoomAuthority.capacityare refused; kicks release slots immediately.
Room social
Chat, pings, roster and moderation ride the same reliable lane as control
frames (RoomSocialHost/RoomSocialClient in protocol):
- Chat text is single-line, capped at 280 characters, rejecting hidden control characters and bidi overrides; history is capped 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 (first connected peer unless the authority implements
RoomSocialAuthority) can lock the room, and kick or ban players; the room-lifetime ban list is capped at 256 identities. Bans require an identity verified by the transport adapter, never a join payload.
Packages
packages/protocol— everything above. Nodart:io; compiles for the web (dart compile js tool/web_compile_smoke.dart).packages/client—RoomClient(typed subclass per game), resumable reconnects with a playback clock for interpolated rendering, and WebSocket adapters for native (dart:io) and web (binary frames surfaced as byte lists, mirroringdart:iosemantics).packages/server_sdk— native-only dedicated room server (RoomServer,ServerWebSocketConnection).
See examples/counter for a complete join → chat → shared-state round trip.
Generated from docs/protocol.md (embedded above via markdown import) and
the frame handling in packages/protocol/lib/src/room_hub.dart; the message
table is cross-checked against the source at build time
(site/scripts/check-spec.mjs).