You need: multiplayer and auth composed.
Add it
The example games are what to copy, Using Multiplayer is the API, sessions and lifecycle is the arc a game runs through, and Durable Objects is what holds it.
pithy add multiplayer
pithy migrateadd writes the Durable Object binding and its class-migration tag into every environment, and puts the session class export in your Worker entry. Both halves matter — a binding without the tag deploys a Worker whose class does not exist.
Define the game
multiplayer({
games: [
{ key: "tictactoe", kind: "connect-n", rules: { rows: 3, cols: 3, connect: 3 } },
],
})That is the whole game. connect-n is a shipped model, so you are configuring one, not writing one.
Same model, different games:
{ key: "connect4", kind: "connect-n", rules: { rows: 7, cols: 6, connect: 4 } }
{ key: "gomoku", kind: "connect-n", rules: { rows: 15, cols: 15, connect: 5 } }Play it
POST /multiplayer/games/tictactoe
→ { "sessionId": "…" }The creator is the first member. The second player joins:
POST /multiplayer/sessions/<id>/joinThe roster filling is what moves the session from open to active.
Then take a turn:
POST /multiplayer/sessions/<id>/action
{ "row": 1, "col": 1 }The body is whatever the model defines, forwarded untouched. For connect-n that is a cell.
Read the board
GET /multiplayer/sessions/<id>Your redacted view. For a fully-open game like this one, everybody sees the same thing — which is what a game with nothing to hide looks like through the redaction seam.
Once terminal:
GET /multiplayer/sessions/<id>/resultLive play
GET /multiplayer/sessions/<id>/socketA hibernation-safe WebSocket. A session waiting on a player’s turn bills no duration, which is the whole reason turn-based play is the quadrant this serves.
Add a deadline
{ key: "tictactoe", kind: "connect-n", rules: { … }, turnTimeoutMs: 60_000 }Alarm-enforced, never a timer. A move that does not land in time moves the session to abandoned.
What you did not have to build
Membership bound to an authenticated user. A lifecycle. The hidden-state boundary. The deadline. A durable result row in your own D1, joinable beside your own tables. A one-way leaderboard publish when you want it.
That is the same for every game. Only the rules differ.
Next
Publish the result to a board — see publish results to a leaderboard.
Get players in without sharing session ids by hand — see get players into a session.
Write a game the three models do not cover — see write your own game model.