games data flow
The games modality is multiplayer rooms with three baked-in channels, each mapped onto the QUIC lane that fits its delivery semantics. You don’t wire up tracks or pick priorities — you create a Games client for a (room, player) and call publishState / publishInput / publishEvent. The room namespace, the lane, and the per-channel QoS come pre-set so you can’t mis-route a channel.

The three channels

ChannelDirectionLaneDeliveryUse for
stateserver → alldatagram (lossy)latest-winstick-rate world snapshots
inputplayer → serverdatagram (lossy)latest-winstick-rate player inputs
eventany → anystream (reliable)ordered, no-dropchat, lobby, item grants, settlement
state and input ride QUIC datagrams — there is no retransmit, so a dropped tick is simply superseded by the next one. That’s exactly what you want for a position snapshot or a controller frame: re-sending a stale tick would arrive too late to matter. event rides a reliable, ordered MoQT subgroup stream — nothing is dropped and order is preserved, which is what you want for anything a player would notice going missing (a chat line, a “you picked up the item” grant, an end-of-match settlement).
Internally all three channels are MoQT frame tracks — opaque binary with a per-frame priority — fanned out by the relay mesh. state/input default to priority 100 on the datagram lane; event defaults to priority 50 on the reliable stream lane. You can override priority per write.

Authoritative server vs player roles

A room has exactly one authority (the server) and any number of players. The role is decided by one option: whether you pass a playerId.

Authority (server)

Construct Games without playerId. It publishes state to every player and subscribes to inputs from all of them on a single callback. It is the source of truth for the world.

Player (client)

Construct Games with a playerId. It publishes input up to the server and subscribes to state coming down. It may also publish/subscribe events.
The roles are enforced. Calling publishInput (or publishEvent) without a playerId throws — only players carry the from identity the wire format needs. The authority never publishes input and never needs a playerId.
// authority — no playerId
const auth = new Games({ relayHost, token, roomId: "duel-42" });

// player — playerId required
const me = new Games({ relayHost, token, roomId: "duel-42", playerId: "alice" });

The from header — fan-in without N subscriptions

Every player publishes input to the same track (game/<room>/input), and the relay fans every player’s frames to the authority. To tell senders apart without a track-per-player, the SDK prefixes a tiny header on each input/event frame:
[u8 from_len][utf-8 player_id][payload]
The authority’s subscribeInputs((playerId, bytes) => …) callback gets the decoded playerId for free — the SDK strips the header. state frames skip the header entirely, because the source is always the room authority.
A playerId must encode to 255 bytes or fewer in UTF-8; the SDK throws on overflow. Keep player ids short and stable for the life of the room.

Tick rates

Both state and input are tick-driven. You drive your own loop and call write once per tick; the modality does not run a clock for you. When you create the state publisher you can pass a tickHz hint:
const state = await auth.publishState({ tickHz: 30 });
setInterval(() => state.write(serializeWorld(world)), 1000 / 30);
tickHz is a hint — it informs the relay’s admission control and the recorder, but it does not pace your writes. Typical rates:
Game shapestate tickinput tick
Turn-based / card / board1–5 Hzevent-driven
Casual real-time / .io10–20 Hz20–30 Hz
Action / shooter30–60 Hz60+ Hz
Keep state snapshots small and self-contained — latest-wins means each one must stand alone. Send deltas only if your client can recover from a dropped delta (e.g. periodic keyframes). For anything that must not be lost, use an event, not state.

Latency budget

A round trip of player input → server tick → state broadcast is dominated by the player→relay RTT plus the relay→player RTT — one MoQT group time each way. On the same continent as a relay POP this is typically 30–80 ms end to end; across continents it is RTT-bound. Datagram lanes add no retransmit or head-of-line blocking, so a single lost tick costs you one tick, not a stall.

Architecture

Under the modality sits the shared substrate every modality uses:
  • One QUIC connection, one session. A Games client opens a single MoQT session per (room, player), lazily on the first publish or subscribe, and reuses it for every channel.
  • MoQT track fan-out. state/input/event are MoQT frame tracks; the relay mesh fans them to every subscriber. The authority does not know how many players are subscribed to state — it publishes once.
  • Right lane per channel. Lossy snapshots take the QUIC datagram lane; reliable events take an ordered subgroup stream. One connection multiplexes both — no separate stack per channel.
  • Kernel-bypass data plane. The engine runs a shard-per-core reactor with an AF_XDP NIC fast path and lock-free mcache / dcache rings on io_uring, so tick fan-out stays flat as rooms scale.
  • One auth token. A single tenant token, scoped to (tenant, room, player?), authorizes the session — the same token model as every other modality.

When to use it

Use games when…

You have authoritative-server multiplayer: rooms, ticks, a server world, and players sending inputs and receiving snapshots. Real-time .io games, action games, board/card games, co-op sessions.

Use Netcode (Unity) when…

You’re in Unity and want Netcode for GameObjects / Entities to “just work” over the same wire — drop in the transport package instead of calling this client directly. See Netcode (Unity).

Use data when…

You want hierarchical MQTT-style topics with + / # filters and retained messages rather than fixed room channels. See Data.

Drop to MoQT when…

Your wire model isn’t a room with three channels — import @clutchcall/sdk/moqt and publish/subscribe frames directly.

Wire convention

game/<room>/state             server → all     datagram, no from-header
game/<room>/input             player → server  datagram, from-header
game/<room>/event/<channel>   any → any        reliable stream, from-header
Capability tags: game.state, game.input, game.event. The session URL encodes the role — players dial …/games/<room>/<player>, the authority dials …/games/<room>/_authority.