End-to-end worked examples: an authoritative 1v1 duel server, a browser player client, and a reliable lobby + chat layer.
Longer, end-to-end examples that combine the games methods into realistic
mini-apps. Each one is self-contained — bring your own serialization (the wire
payloads are opaque bytes).
These examples use a tiny placeholder codec (serialize / deserialize). In
production use a compact binary format — a fixed struct, FlatBuffers, or
bit-packed snapshots — so snapshots stay small enough to fit a datagram.
A complete server loop: read every player’s input, step the world at a fixed
tick, broadcast state, and emit a reliable settlement event when the match ends.
1
Create the authority client
Omit playerId to take the authority role.
import { Games } from "@clutchcall/sdk/games";const auth = new Games({ relayHost: "relay.clutchcall.dev", token: await mintServerToken("duel-42"), roomId: "duel-42", onState: (s) => console.log("relay state", s),});
2
Fan in every player's input
One callback collects all players. Stash the latest input per player; the tick
loop reads it.
const world = newWorld();const latestInput = new Map<string, Input>();await auth.subscribeInputs((playerId, bytes) => { latestInput.set(playerId, deserializeInput(bytes)); // latest-wins});
3
Step + broadcast at a fixed tick
Apply the buffered inputs, advance the world, and write a self-contained
snapshot every tick.
The matching client: sample local input each frame, push it up, render incoming
state, and react to the settlement event.
1
Join as a player
import { Games } from "@clutchcall/sdk/games";const me = new Games({ relayHost: "relay.clutchcall.dev", token: await fetchRoomToken("duel-42", "alice"), roomId: "duel-42", playerId: "alice", onState: (s) => setOnlineBadge(s === 1),});
2
Push input on the client tick
publishInput binds to alice, so the server’s fan-in already knows who sent
the frame.
const input = await me.publishInput();function clientTick() { input.write(serializeInput(readGamepad())); // datagram — drop is fine requestAnimationFrame(clientTick);}requestAnimationFrame(clientTick);
3
Render server state
Each state callback is one authoritative snapshot. Interpolate toward it for
smoothness; never block waiting for a missed one.
let target = null;await me.subscribeState((bytes) => { target = deserializeState(bytes); });function renderLoop() { if (target) interpolateAndDraw(target); requestAnimationFrame(renderLoop);}requestAnimationFrame(renderLoop);
4
React to the settlement event
Subscribe to the same reliable channel the server publishes results on.
Before the match, a room often needs a reliable coordination layer: who’s
ready, chat, and a host kicking off the game. This rides entirely on the
event channels — no state/input loop yet.
1
Open the lobby channels
Use one channel per concern. Each is an isolated reliable, ordered track.
When everyone is ready, the host (a player or your server authority) emits a
start event. On receipt, every client tears down the lobby and constructs the
gameplay client from Recipe 2.
await lobby.subscribeEvents({ channel: "start" }, async () => { await lobby.close(); // drop the lobby session startGameplay(); // construct the gameplay Games client});
You can keep the lobby’s Games instance open through the match and just open
the state/input channels on it — one session multiplexes all channels. Tearing
it down here is only to keep the example’s two phases clearly separated.