Add it
pithy add multiplayer
pithy migrateThis is Pithy’s first Durable Object. add wires the binding and its class-migration tag into wrangler.jsonc for every environment, and writes the MultiplayerSession export into your Worker entry. migrate creates the result table.
You write games as config, and the example games are the ones to copy from.
Define a game
A game runs inside a session, and shipping a turn-based game walks the whole arc — define, seat, play, resolve. The reference has every field.
multiplayer({
games: [
{
key: "tictactoe",
kind: "connect-n",
rules: { rows: 3, cols: 3, connect: 3 },
},
],
})| Field | |
|---|---|
key | A URL path segment |
kind | The model’s discriminator, resolved from the registry |
mode | match (default) or table |
players | Match: the exact roster. Table: the maximum seats |
rules | The model-specific block. Validated by the model at assembly |
turnTimeoutMs | Alarm-enforced. Null to wait indefinitely |
leaderboard | Optional publish target |
Play
POST /multiplayer/games/:game create — you are its first member
POST /multiplayer/sessions/:id/join
POST /multiplayer/sessions/:id/action
POST /multiplayer/sessions/:id/leave table mode
POST /multiplayer/sessions/:id/close table mode
GET /multiplayer/sessions/:id your redacted view
GET /multiplayer/sessions/:id/result once terminal
GET /multiplayer/sessions/:id/socket live play, hibernation-safeAn action’s body is whatever the game’s model defines — { offense, defense } for battle, { row, col } for connect-n, { kind: "bet", … } for craps — and the route forwards its JSON untouched.
Every route requires authentication
Membership binds to the authenticated user id from the auth seam, never a client-supplied one. Add the auth capability.
Register your own game
registerGameModel(myGame);In your Worker entry, at module load. The Worker and the Durable Object share one isolate, so a model registered on import is present by the time the object handles a request.
Re-registering a kind replaces it, which is how you override a built-in.
Composing with the neighbors
| Capability | What it adds |
|---|---|
| Auth | Required. Membership has nothing to bind to without it |
| Ledger | Required for any wagering game. Settles the effects a model declares |
| Leaderboard | An optional, one-way publish of results |
| Matchmaking | The four ways players reach a session in the first place |
Read the cost page before production
A session waiting on a player’s turn hibernates and bills no duration — that is the quadrant this serves.
The two facts that make a naive estimate wrong: WebSocket messages bill at 20:1, and duration bills the full 128 MB regardless of how little the session uses. As of 2026-07-16; Cloudflare’s pricing page is the authority.
What this is not
Not rooms, chat, presence or broadcast — use PartyServer or a raw Durable Object.
Not real-time action netcode — no rollback, prediction, lag compensation or fixed high-rate ticks. A fixed-rate loop never hibernates, which is the exact cost this design avoids. Use Photon.
Not matchmaking or skill rating — different capabilities.