Using Multiplayer

Add it

pithy add multiplayer
pithy migrate

This 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
keyA URL path segment
kindThe model’s discriminator, resolved from the registry
modematch (default) or table
playersMatch: the exact roster. Table: the maximum seats
rulesThe model-specific block. Validated by the model at assembly
turnTimeoutMsAlarm-enforced. Null to wait indefinitely
leaderboardOptional 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-safe

An 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

CapabilityWhat it adds
AuthRequired. Membership has nothing to bind to without it
LedgerRequired for any wagering game. Settles the effects a model declares
LeaderboardAn optional, one-way publish of results
MatchmakingThe 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.

ESC