The first multiplayer game anybody writes trusts the client. It has to — the client is where the game logic already is — and it works until two people care about the outcome.
@pithy-sh/multiplayer is the other arrangement: the server holds the game state no client can be trusted with, resolves each move, and writes a durable result to your own D1. A client sends an action and is told what happened. It is never told the parts of the state it should not know, and it is never asked what the outcome was.
This is Pithy’s first Durable Object, and the wiring is why it is a capability rather than a snippet: a Durable Object is a binding and a class migration tag and an export from your Worker’s entry, across every environment, and the CLI writes all three.
Turn-based and asynchronous, not real-time
This is the boundary, and it is worth being blunt about.
It is for a game where a turn is a decision: a board game, a card game, a strategy game, a wagering table, a simultaneous-reveal battle. A session can last a minute or a week, and a player can close the app between turns.
It is not netcode. No tick rate, no interpolation, no lag compensation, no authoritative movement simulation. For a game where position updates sixty times a second, Cloudflare’s own PartyServer is the thing you want, and this package will not pretend otherwise.
Games are pluggable
Three example games ship, each built on a reusable pattern helper rather than as a special case:
| Example | Pattern |
|---|---|
connect-n | Turn-based grid — tic-tac-toe and Connect Four are the same model with different rules |
battle | Simultaneous — both players choose, then both reveal |
craps | A wagering table, with persistent buy-in between rounds |
Each helper is the layer your own game sits on. Writing a GameModel of your own is the supported path, not a fork.
It supports N players, not just two.
Hidden state is a first-class idea
A card game where every player can read every hand is not a card game. The model distinguishes what the session knows from what each player is told, so a projection to one player omits what that player should not see.
That is not something you can add later to a design that assumed shared state, which is why it is here rather than in a feature list.
Wagering, done properly
The wagering stack is provably-fair dice, persistent tables where players buy in and cash out between rounds, and **bets settled through **ledger.
That last part is what makes it safe. A stake is a hold the moment the bet is placed — reserved, unavailable, and either released or captured when the hand resolves. Without holds, a player can bet, spend the same chips elsewhere while the hand runs, and be unable to pay when they lose.
What it deliberately does not do
No rooms, chat, or presence. Getting players into a session is matchmaking — four ways in, one session id — and chat is not here at all.
No spectating, no replay UI, no tournament brackets.
It does not rate players. A resolved session is an outcome you record with rating, or publish to a leaderboard. Both are one call from the result this produces.
It does not decide your game’s rules. It runs the model you give it, authoritatively.
When you would reach for it
A turn-based or asynchronous game where the outcome matters — because there is a rating attached, or money, or just because players will find any way to cheat that you leave open.
Not for a single-player game with a score, and not for anything that needs sixty updates a second.
What it needs
No required peers.
auth is optional and effectively required: sessions bind to an authenticated user, so without it every route denies.
leaderboard and ledger are optional and are what a resolved session usually flows into — a published standing, or a settled wager.
Read What a waiting queue costs before production if you also compose matchmaking. A Durable Object waiting on nothing is cheap; a busy queue’s alarm cadence is the dial that decides how cheap.