chess-multiplayer: Project Chess: Where the Server Keeps the Clock, the Rules, and the Truth
A real-time multiplayer chess stack that treats every move, timer tick, and rating change as server-owned state, not client opinion.
- Project Chess is built around one hard idea: the server is the arbiter of legality, time, and outcomes, so the browser can never rewrite the game.
- The strongest design choice is not the chess UI, but the move pipeline that turns a drag event into validated state, persisted history, and opponent broadcast.
- The database is doing archival work, not just storage, because FEN snapshots let the system reconstruct games instead of merely remembering them.
- This repo stays lean by putting complexity in protocol and authority rather than in framework sprawl.
The browser is not trusted
In chess, the clock is part of the game. If the client owns it, the client can lie. That is why the most interesting thing in Sanjana23-is/chess-multiplayer is not the board. It is the server-side referee that decides whether a move is legal, whether the timer really expired, and whether the result should be written down.
That choice changes the whole shape of the product. The browser becomes a proposal layer. The server becomes the source of truth. In a real-time game, that is the difference between an app and a system.
One move, six handoffs
The cleanest way to read the repo is as a packet journey. The frontend emits intent through `useSocket`. The connection lands in `GameManager`, which finds a room or spins one up. Then `Game` validates the move, updates clocks, resolves endings, and writes the result through Prisma before broadcasting the new state to the opponent.
type ClientEvent =
| { type: 'AUTH'; token: string }
| { type: 'FIND_MATCH'; timeControl: number }
| { type: 'CREATE_ROOM' }
| { type: 'MOVE'; roomId: string; from: string; to: string; promotion?: string };
// The important part is not the syntax.
// It is that every move is a message the server can refuse.
Game.ts is the referee
`Game.ts` is where this project stops being a chatty socket demo and becomes a real arbiter. It wraps `chess.js` so move legality is decided server-side, not negotiated by the client. If a move is illegal, it does not become a debate. It simply dies there.
That same file also owns server-side time. Instead of trusting the browser clock, it measures elapsed time from the server and decrements the active player’s remaining time there. In chess, that is not a detail. It is the game.
The strongest proof that the backend owns outcomes is the rating update path. When a game ends, the Elo change is handled as an atomic backend action, not a UI flourish. Moves, clocks, and ratings all belong to the same trust boundary.
GameManager.ts is the traffic cop
`GameManager.ts` handles the parts that make multiplayer feel alive. It keeps track of matchmaking queues, private room codes, live sockets, and cleanup when a connection disappears. That sounds mundane until you realize it is what prevents ghost players and orphaned games.
This is the difference between a chess engine and a chess service. One knows the rules. The other knows which human is attached to which board, and what to do when the cable gets yanked.
| Concern | Client-heavy chess | Project Chess |
|---|---|---|
| Authority model | Browser decides too much | Server owns legality, clocks, and outcomes |
| Timer ownership | Split or local | Server-side and authoritative |
| Move validation | Often duplicated in UI | Validated in `Game.ts` with shared rules |
| Persistence | Optional or shallow | Moves and FEN history are stored |
| Scope | Can drift into UI-only play | Focused on competitive integrity |
| Best use case | Casual prototypes | Fair real-time multiplayer |
The database is a replay system, not just storage
The Prisma schema matters because it remembers enough to reconstruct a match, not just to note that a match happened. Storing move history with FEN snapshots turns the database into a replay system. That gives you auditability, rematches, history views, and a clean way to rebuild state if a client drops out.
That is a quiet but powerful design choice. Many apps save outcomes. This one saves the path that produced the outcome.
A modern stack with disciplined scope
The stack is current without being ornate: React 19, Vite, Tailwind v4, Express 5, `ws`, Prisma, and PostgreSQL or SQLite. Nothing here is trying to impress you with framework maximalism. The complexity lives where it should, in the protocol and the state machine.
| Layer | Tooling | Why it fits |
|---|---|---|
| Frontend | React 19 + Vite + Tailwind v4 | Fast UI iteration without framework drag |
| Realtime transport | ws | Direct WebSocket control with low overhead |
| Backend | Express 5 + TypeScript | Simple routing and explicit server logic |
| Persistence | Prisma + PostgreSQL/SQLite | Clear schema and portable storage |
| Game rules | chess.js | Shared legality checks across client and server |
That restraint is the point. A real-time game benefits from sharp boundaries, not a giant framework orbit.
Why this is harder than it looks
Multiplayer chess sits at an awkward intersection. Latency matters, but fairness matters more. Persistence matters, but only if it can reconstruct state accurately. And once ratings enter the picture, every state change becomes part of the record.
That is why this repository is more interesting than a generic chess app. It treats chess as a trust problem disguised as a UI problem, and it answers with a server that refuses to let the browser improvise.