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.

9 min read • View on GitHub • More from Sanjana23-is

A chessboard is split by a central server tower. A browser window pushes a knight toward the board, but the move routes through the server before it reaches the opponent side. A stopwatch, rating badge, and move ledger sit behind a locked gate, showing that the server controls time, outcomes, and persistence.
In this stack, the browser suggests moves. The server decides whether they count, how much time remains, and what gets recorded.
Key Takeaways

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.

A single move packet travels through a sequence of handoffs. A cursor drags a bishop, the socket packet leaves the browser, passes through a queue door marked GameManager, enters a sealed chamber marked Game, then emerges as a row in a ledger and a broadcast line to the opponent. The flow is visibly sequential and controlled.
One move is not just sent. It is routed, checked, persisted, and only then echoed to the other player.

One move, six handoffs

This is the whole system in one line: a move starts as input, becomes verified server state, and ends as shared reality.

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.

ConcernClient-heavy chessProject Chess
Authority modelBrowser decides too muchServer owns legality, clocks, and outcomes
Timer ownershipSplit or localServer-side and authoritative
Move validationOften duplicated in UIValidated in `Game.ts` with shared rules
PersistenceOptional or shallowMoves and FEN history are stored
ScopeCan drift into UI-only playFocused on competitive integrity
Best use caseCasual prototypesFair 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.

LayerToolingWhy it fits
FrontendReact 19 + Vite + Tailwind v4Fast UI iteration without framework drag
Realtime transportwsDirect WebSocket control with low overhead
BackendExpress 5 + TypeScriptSimple routing and explicit server logic
PersistencePrisma + PostgreSQL/SQLiteClear schema and portable storage
Game ruleschess.jsShared 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.