Appearance
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
| rule | value |
|---|---|
| maximum message size | 64 KB (maxPayload) |
| messages per connection | token bucket: burst 120, refill 40/s. Exceeding it closes the socket with 1008 too many messages |
| opening a socket | per-address connect bucket. When empty, the socket is closed with 1013 rate limited |
| keep-alive | the server pings every 25 s and terminates a socket after 2 missed pings |
| malformed JSON, or a non-object | ignored |
unknown t | ignored |
| server shutdown | live 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
t | fields | what it does |
|---|---|---|
hello | name, 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 |
seek | matchLength, clock, vsBot, difficulty, private, rated, beavers, autoDoubleCap | posts 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 |
join | id (challenge id, up to 12 characters) | joins a specific challenge. This is what a #c=<id> link does |
cancel | withdraws your open seeks | |
rematch | offers 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 | |
lobby | asks for a lobby snapshot |
In a match
t | fields | what it does |
|---|---|---|
ack | sig | signs h0 and marks the seat ready. Play begins once both seats have acknowledged |
entropy | x (64 hex) | this player's next chain link, seeding the next game. It must hash to the previous link |
action | action, expect | a game action (below). expect is the event count the client decided on. If the match has moved on, the action is dropped silently |
sig | n (event index), sig (128 hex) | the client's signature of event hash h(n) |
abandon | forfeits the whole match now, with reason resigned. A complete record is still produced |
action objects are validated before they reach the rules engine (validateAction):
type | fields | validation |
|---|---|---|
roll | x | must be 64 lowercase hex characters |
play | hops: [{ 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 | ||
resign | level | 1, 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
t | fields | what it does |
|---|---|---|
puzzle | asks for today's puzzle card | |
puzzle-attempt | hops | answers today's puzzle. Needs a verified key. The first answer is recorded, and later ones get alreadyAnswered: true |
Accounts
t | fields | what it does |
|---|---|---|
register | username, email, jwk | creates 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-link | email | emails a sign-in link if the address has an account. Always answers login-sent |
account | asks for this key's account status | |
delete-account | confirm: '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.
t | fields | what it does |
|---|---|---|
subscribe | email | starts a double opt-in subscription for the verified key and emails a confirmation link |
email-prefs | daily (bool) or forget: true | turns the daily mail on or off, or deletes the subscription |
email-status | asks for subscription status |
Messages that send mail share a quota of 5 per hour per key and per address.
Server → client
Session and lobby
t | fields | when |
|---|---|---|
welcome | serverKey, challenge (32 hex), dice (protocol id), formats: [{ n, label, cls, clocks: [{ mode, label }] }], botEngine | immediately on connect |
identity | pub, verified, rating, note | reply to hello. note explains unrated play if the key did not verify |
lobby | seeks: [{ id, name, rating, matchLength, cls, label, clock, rated, waiting }], online, playing | after hello, on lobby, and broadcast to sessions not in a match whenever the lobby changes |
seeking | id, matchLength, label, clock, invite | your seek is posted. invite is the id for private challenges, otherwise null |
cancelled | reply to cancel | |
superseded | the same key has identified on another connection | |
rematch-offer | from | the opponent wants a rematch |
rematch-sent | your rematch offer was delivered | |
error | message | any failure |
In a match
t | fields | when |
|---|---|---|
matched | matchId, seat (0/1), opponent: { name, pub, bot, rating }, matchLength, label, clockLabel, rated | a match has been created for you |
handshake | header, h0 | sign h0 and reply ack |
need-entropy | game | reveal your next link with entropy |
event | ev (the event), hash (h(n)), state (snapshot for your seat), clocks | after every event |
opponent-left | grace (seconds) | the opponent's socket dropped. They forfeit if they do not return in time |
opponent-back | they returned | |
resume | matchId, seat, header, h0, opponent, matchLength, rated, clockLabel, lastLink, opponentHere, grace, needEntropy, hash, state, clocks | you reconnected into a live match. lastLink is the last chain link the server accepted from you |
record | record, rating: { rated, you: { before, after }, opponent } | the match finished. record is the full signed record |
aborted | reason | the 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
t | fields | when |
|---|---|---|
puzzle | puzzle, stats, streak, yours | reply to puzzle. yours (with the answers) is present only if this key has already answered |
puzzle-result | the scored attempt, plus alreadyAnswered, streak, stats | reply to puzzle-attempt |
account | registered, username, email (redacted), canRegister, optionally justSent | reply to account and register |
login-sent | reply to login-link | |
account-deleted | note | reply to delete-account |
email-status | available, provider, hour, subscribed, state, daily, email (redacted), optionally justSent | reply to the subscription messages |