WebSocket Protocol

Low-level WebSocket message format for direct integration.

Connection

wss://watchtower-api.watchtower-host.workers.dev/v1/rooms/{code}/ws?playerId={id}&apiKey={key}

Query parameters:

  • playerId — Your unique player identifier
  • apiKey — Your API key (required for auth)

Message Format

All messages are JSON. Every message has a type field.

Client → Server

Update Player State

{
  "type": "player_state",
  "state": {
    "x": 100,
    "y": 200,
    "sprite": "running"
  }
}

Update Game State (Host Only)

{
  "type": "game_state",
  "state": {
    "phase": "playing",
    "round": 1,
    "scores": {}
  }
}

Broadcast Message

{
  "type": "broadcast",
  "data": { "type": "chat", "message": "Hello!" },
  "excludeSelf": true
}

Send to Specific Player

{
  "type": "send",
  "to": "player-id",
  "data": { "type": "private", "text": "Hey" }
}

Transfer Host (Host Only)

{
  "type": "transfer_host",
  "newHostId": "player-id"
}

Ping

{
  "type": "ping"
}

Server → Client

Connected

Sent immediately after WebSocket connection established.

{
  "type": "connected",
  "playerId": "your-player-id",
  "room": {
    "code": "ABCD",
    "gameId": "my-game",
    "hostId": "host-player-id",
    "players": [
      { "id": "player-1", "joinedAt": 1706400000000 },
      { "id": "player-2", "joinedAt": 1706400100000 }
    ],
    "playerCount": 2
  },
  "playerStates": {
    "player-1": { "x": 100, "y": 200 },
    "player-2": { "x": 300, "y": 200 }
  },
  "gameState": {
    "phase": "lobby"
  }
}

Player Joined

{
  "type": "player_joined",
  "playerId": "new-player-id",
  "playerCount": 3
}

Player Left

{
  "type": "player_left",
  "playerId": "leaving-player-id",
  "playerCount": 2
}

Players Sync (Batch)

Sent at 20Hz with all player states.

{
  "type": "players_sync",
  "players": {
    "player-1": { "x": 105, "y": 200 },
    "player-2": { "x": 295, "y": 210 }
  }
}

Player State Update (Individual)

Sent immediately when a player updates (for lower latency).

{
  "type": "player_state_update",
  "playerId": "player-1",
  "state": { "x": 105, "y": 200 }
}

Game State Sync

{
  "type": "game_state_sync",
  "state": {
    "phase": "playing",
    "round": 1
  }
}

Host Changed

{
  "type": "host_changed",
  "hostId": "new-host-player-id"
}

Message (Broadcast/Direct)

{
  "type": "message",
  "from": "sender-player-id",
  "data": { "type": "chat", "message": "Hello!" }
}

Pong

{
  "type": "pong",
  "timestamp": 1706400000000
}

Example: JavaScript Client

const ws = new WebSocket(
  'wss://watchtower-api.watchtower-host.workers.dev/v1/rooms/ABCD/ws' +
  '?playerId=my-player&apiKey=wt_live_xxx'
)

ws.onopen = () => {
  console.log('Connected!')
}

ws.onmessage = (event) => {
  const data = JSON.parse(event.data)
  
  switch (data.type) {
    case 'connected':
      console.log('Room:', data.room)
      break
    case 'players_sync':
      updatePlayers(data.players)
      break
    case 'message':
      handleMessage(data.from, data.data)
      break
  }
}

// Send player state
function sendState(state) {
  ws.send(JSON.stringify({
    type: 'player_state',
    state: state
  }))
}

// Broadcast message
function broadcast(data) {
  ws.send(JSON.stringify({
    type: 'broadcast',
    data: data,
    excludeSelf: true
  }))
}

Update Frequency

Message TypeFrequency
players_sync20Hz (every 50ms)
player_state_updateImmediate
game_state_syncOn change
messageImmediate

Keep-Alive

Send periodic ping messages to keep the connection alive. The server responds with pong.

// Send ping every 30 seconds
setInterval(() => {
  ws.send(JSON.stringify({ type: 'ping' }))
}, 30000)