Skip to content

WebSocket protocol

Clients (player phones and the GM console) talk to the engine over a WebSocket, joining a session by its code. This page documents the message types from packages/wage-protocol/src/events.ts. You don’t need it to build a game — it’s for anyone working on a client or the engine itself.

A client opens a WebSocket to the engine’s /ws endpoint for a session (addressed by join code) and then sends either a join (to play) or spectate (read-only, used by the GM console) message.

type ClientMessage =
| { type: "join"; sessionId: string; name: string }
| { type: "spectate" }
| { type: "location"; position: Position }
| { type: "action"; actionId: string; payload?: Record<string, unknown> }
| { type: "chat"; text: string };
  • join — enter the session as a player with a display name. The server assigns a team and starting state and replies with welcome.
  • spectate — subscribe read-only; the server streams world snapshots but never expects location/action. The GM console uses this — spectators get the full, unfiltered world.
  • location — a GPS update (sent ~every 2 s). Position is { lat, lng, ts, accuracyMeters?, headingDegrees? }.
  • action — perform an interaction (or confirm/deny a mutual one), identified by actionId with an optional payload.
  • chat — free text. The author is resolved server-side from the socket; a client-supplied name is never trusted.
type ServerMessage =
| { type: "welcome"; playerId: string; world: World }
| { type: "world"; world: World }
| { type: "event"; event: GameEvent }
| { type: "error"; message: string }
| { type: "chat"; message: ChatMessage }
| { type: "chat_history"; messages: ChatMessage[] };
  • welcome — sent once after join, carrying the client’s playerId and the current world.
  • world — a full world snapshot, broadcast every tick (and on demand). The client renders from this. Position visibility is filtered per viewer by the ruleset’s visibility rules (the GM is never filtered).
  • event — a GameEvent ({ type: string; … }) such as a toast or countdown, produced by send_event actions.
  • chat / chat_history — a new message, or recent history replayed once after join/spectate. The GM appears as Gamemaster.
  • error — a problem with the last message.

The World the server broadcasts is the whole renderable game state: session id, status (stopped/started/ended), tick number, timers (startedAt, pausedAt, countdownEndsAt), teams, the map plus placed zones/points/ annotations, all players (id, name, team, position, state, objective), all items, and the mirrored presentation data (itemTypes, theme, bots, interactions). Clients keep the latest snapshot and re-render on each one.

Full shapes live in packages/wage-protocol/src/world.ts.

Because state is server-side, reconnecting is just opening a new socket and re-join/spectate-ing: the client receives a fresh welcome/world and is immediately back in sync. The GM console persists the active session and reconnects automatically on reload.