01
Overview
Use this path only when a mature server must remain responsible for admission, simulation, state, reconnects, and results.
Your server owns
Admission, game rules, input validation, simulation, viewer safe state, and real terminal results.
The SDK owns
Host ticket requests, the WebSocket lifecycle, bounded join data, reconnect, and a small transport envelope.
02
Registration
External authorities are reviewed and enabled per Arcade game. Registration is not self serve.
03
Verify every ticket
The SDK receives a 60 second HMAC ticket from the trusted host. Your server verifies it before admitting the socket.
payloadB64 = base64url(UTF8(JSON(payload)))
signatureB64 = base64url(HMAC-SHA256(payloadB64, ticketSecret))
ticket = payloadB64 + "." + signatureB64{
"v": 1,
"sub": "player:<opaque-game-scoped-value>",
"name": "Display Name",
"avatarUrl": null,
"slug": "your-game-slug",
"roomId": "ROOM-CODE",
"guest": false,
"iat": 1700000000000,
"exp": 1700000060000
}04
Connect from the SDK
The public game API stays the same whether Bounty Board hosts the room module or connects to a registered external authority.
import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer';
const room = await joinRoom({
code: 'ROOM-CODE', // or create: true
joinData: {
schemaVersion: 1,
loadout: 'starter',
mode: 'friends',
},
});
room.on('snapshot', ({ state }) => renderAuthoritativeState(state));
room.on('connection', ({ connected }) => setReconnecting(!connected));
if (!room.trySend({ type: 'move', x: 1, y: 0 })) {
setReconnecting(true);
}wss://multiplayer.example.com/bountyboard?ticket=<encoded>&join=<encoded-json>05
Room messages
All frames are JSON text. Send welcome only after ticket and admission validation, and keep every snapshot viewer safe.
{
"t": "welcome",
"playerId": "room-scoped-player-id",
"players": [
{ "id": "room-scoped-player-id", "name": "Display Name", "avatarUrl": null }
],
"state": { "phase": "lobby" }
}// Server -> client
{ "t": "snapshot", "tick": 42, "state": { "phase": "playing" } }
{ "t": "event", "data": { "type": "round_started", "round": 2 } }
{ "t": "player_leave", "playerId": "p2" }
// Client -> server: input only, never authoritative state
{ "t": "input", "seq": 7, "data": { "type": "move", "x": 1, "y": 0 } }06
Reconnect safely
The SDK requests a fresh ticket for the same room and reuses the original joinData on each of its up to 3 reconnect attempts.
07
Finite results only
A match ending message is a server authored fact, not a client request. Endless rooms must not invent one.
{
"t": "match_end",
"results": [
{
"playerId": "p1",
"name": "Display Name",
"placement": 1,
"score": 1200,
"outcome": "win"
}
]
}08
Production checklist
Prove the trust boundary and failure paths before Bounty Board enables the authority in production.