Skip to content

WebSocket protocol

The browser and the server exchange JSON messages over a WebSocket at the site's own origin: wss://<host>/, or ws:// on plain HTTP. Every message is an object with a type field t. This page lists every message type, taken from src/server/index.js, src/server/lobby.js and src/server/actor.js.

The protocol is versioned together with the server, and the client is served from the same server. For that reason, HTML, JS and CSS are sent with cache-control: no-cache, because a stale app.js would be a protocol bug, not just an old look.

Transport rules

rulevalue
maximum message size64 KB (maxPayload)
messages per connectiontoken bucket: burst 120, refill 40/s. Exceeding it closes the socket with 1008 too many messages
opening a socketper-address connect bucket. When empty, the socket is closed with 1013 rate limited
keep-alivethe server pings every 25 s and terminates a socket after 2 missed pings
malformed JSON, or a non-objectignored
unknown tignored
server shutdownlive matches get aborted, and sockets are closed with 1001 server restarting

Errors thrown while handling a message come back as { t: 'error', message }.

A typical match

server → welcome        { challenge, serverKey, formats, ... }
client → hello          { name, pub, sig: sign(challenge), tip }
server → identity, lobby
client → seek           { matchLength, clock, ... }
server → seeking | matched
server → handshake      { header, h0 }
client → ack            { sig: sign(h0) }
server → need-entropy
client → entropy        { x }
server → event ×2       (newgame, open)
client → sig            { n, sig }            for every event
client → action         { action: { type: 'roll', x }, expect }
server → event          { ev, hash, state, clocks }
   ...
server → record         { record, rating }

Client → server

Session and lobby

tfieldswhat it does
helloname, pub (64 hex), sig (signature of the connection's challenge), tip (64 hex chain tip)identifies the session. pub is accepted only if sig verifies. Control characters are removed from the name and it is cut to 24 characters (default anon). If the key has a live match, the session is put back into it (resume). Required before seek or join, which answer say hello first without a tip. The client sends hello again with a fresh tip before each match
seekmatchLength, clock, vsBot, difficulty, private, rated, beavers, autoDoubleCapposts or accepts a challenge, or starts a bot match when vsBot is true. Invalid length → 3. Invalid difficulty → medium. Invalid clock → standard. rated defaults to true. autoDoubleCap is clamped to 0–3
joinid (challenge id, up to 12 characters)joins a specific challenge. This is what a #c=<id> link does
cancelwithdraws your open seeks
rematchoffers a rematch on the same length and clock, or accepts one if the opponent already offered. Against a bot, starts a new bot match. If the opponent has left, posts an open seek instead
lobbyasks for a lobby snapshot

In a match

tfieldswhat it does
acksigsigns h0 and marks the seat ready. Play begins once both seats have acknowledged
entropyx (64 hex)this player's next chain link, seeding the next game. It must hash to the previous link
actionaction, expecta game action (below). expect is the event count the client decided on. If the match has moved on, the action is dropped silently
sign (event index), sig (128 hex)the client's signature of event hash h(n)
abandonforfeits the whole match now, with reason resigned. A complete record is still produced

action objects are validated before they reach the rules engine (validateAction):

typefieldsvalidation
rollxmust be 64 lowercase hex characters
playhops: [{ from, to, die }]at most 4 hops. from 0–24 (24 = bar), to 0–25 (25 = off), die 1–6, all integers
double, take, pass, beaver
resignlevel1, 2 or 3 (single, gammon, backgammon)
resign-accept, resign-decline

Illegal actions come back as error with the rules engine's message, for example not your turn, may not double, illegal play, or player 0 sent a reveal outside their committed chain.

Puzzle

tfieldswhat it does
puzzleasks for today's puzzle card
puzzle-attempthopsanswers today's puzzle. Needs a verified key. The first answer is recorded, and later ones get alreadyAnswered: true

Accounts

tfieldswhat it does
registerusername, email, jwkcreates an account on the verified key and stores jwk, the private key, server-side. Emails a sign-in link. Needs a configured mail provider. If the address already has an account, a sign-in link is sent to it and the reply looks identical
login-linkemailemails a sign-in link if the address has an account. Always answers login-sent
accountasks for this key's account status
delete-accountconfirm: 'delete my account'deletes the account on the verified key. See Accounts & email

Email subscription

The server supports these messages, but the current web client does not send them.

tfieldswhat it does
subscribeemailstarts a double opt-in subscription for the verified key and emails a confirmation link
email-prefsdaily (bool) or forget: trueturns the daily mail on or off, or deletes the subscription
email-statusasks for subscription status

Messages that send mail share a quota of 5 per hour per key and per address.

Server → client

Session and lobby

tfieldswhen
welcomeserverKey, challenge (32 hex), dice (protocol id), formats: [{ n, label, cls, clocks: [{ mode, label }] }], botEngineimmediately on connect
identitypub, verified, rating, notereply to hello. note explains unrated play if the key did not verify
lobbyseeks: [{ id, name, rating, matchLength, cls, label, clock, rated, waiting }], online, playingafter hello, on lobby, and broadcast to sessions not in a match whenever the lobby changes
seekingid, matchLength, label, clock, inviteyour seek is posted. invite is the id for private challenges, otherwise null
cancelledreply to cancel
supersededthe same key has identified on another connection
rematch-offerfromthe opponent wants a rematch
rematch-sentyour rematch offer was delivered
errormessageany failure

In a match

tfieldswhen
matchedmatchId, seat (0/1), opponent: { name, pub, bot, rating }, matchLength, label, clockLabel, rateda match has been created for you
handshakeheader, h0sign h0 and reply ack
need-entropygamereveal your next link with entropy
eventev (the event), hash (h(n)), state (snapshot for your seat), clocksafter every event
opponent-leftgrace (seconds)the opponent's socket dropped. They forfeit if they do not return in time
opponent-backthey returned
resumematchId, seat, header, h0, opponent, matchLength, rated, clockLabel, lastLink, opponentHere, grace, needEntropy, hash, state, clocksyou reconnected into a live match. lastLink is the last chain link the server accepted from you
recordrecord, rating: { rated, you: { before, after }, opponent }the match finished. record is the full signed record
abortedreasonthe match ended without a record (handshake timeout, idle expiry, or server restart)

state is Match.snapshot(seat): matchId, matchLength, score, gameNo, over, matchWinner, isCrawfordGame, eventCount, you, and game, which holds pos, turn, phase, dice, cube, pendingCube, cubeOfferedBy, mayBeaver, resign, pips, legal (every legal hop list) and mayDouble.

clocks is null for casual matches. Otherwise it is { mode, delay, running, left: [s0, s1] }.

Game phases: opening, to-roll, to-move, cube-offered, resign-offered, game-over, match-over.

Puzzle and account

tfieldswhen
puzzlepuzzle, stats, streak, yoursreply to puzzle. yours (with the answers) is present only if this key has already answered
puzzle-resultthe scored attempt, plus alreadyAnswered, streak, statsreply to puzzle-attempt
accountregistered, username, email (redacted), canRegister, optionally justSentreply to account and register
login-sentreply to login-link
account-deletednotereply to delete-account
email-statusavailable, provider, hour, subscribed, state, daily, email (redacted), optionally justSentreply to the subscription messages

Rules engine, fairness protocol, verifier, analysis and worker: MIT. Server and client: AGPL-3.0-or-later.