Back to projects

Play Chess

A real-time multiplayer chess platform with Elo rankings, friend challenges, and Pro memberships.

Play Chess

I've always found online chess either too serious or too throwaway - heavyweight platforms that demand accounts and downloads, or bare-bones boards with no memory of who you are. So I built something in between, and used it as an excuse to learn how real systems actually scale.

Play Chess is a real-time multiplayer platform where players queue into matches within seconds, challenge friends directly, and track their progress through a competitive Elo rating system. Under the hood, it follows production-standard patterns: a TypeScript monorepo with clear service boundaries, server-authoritative game state, and modular infrastructure that's meant to grow.


The Problem

Most online chess platforms are either too heavy or too bare-bones. I wanted something in between - fast, real-time, and social - while also using it as a vehicle to practice building a serious full-stack TypeScript monorepo.

The real constraint wasn't chess. Chess is a solved problem. The constraint was synchronization - two browsers, one game, no disagreements about whose move it is.


Core Features I Built

Competitive Gameplay

  • Real-time multiplayer - Low-latency board synchronization using Socket.IO
  • Responsive interface - Desktop and mobile-optimized chessboard with move validation
  • Sound design - Audio feedback for moves and game events

Progression & Retention

  • Elo rating system - Competitive rating updates based on game outcomes
  • Standardized Elo rating algorithm - Uses a proven skill-based matchmaking and ranking formula to accurately measure player performance over time
  • Player statistics - Comprehensive match history and performance metrics
  • Game replays - Full move sequences enabling game review and analysis

Social Engagement

  • Friend system - Player connections and social features
  • Direct challenges - Ability to invite specific players to matches
  • Premium gating - Pro membership features with Stripe integration

Monetization

  • Stripe integration - Subscription management and payment processing
  • Membership tiers - Pro membership with exclusive features
  • Billing interface - User-friendly upgrade and downgrade flows

Architecture

Architecture Diagram

Real-Time Multiplayer System

Socket.IO does the hard work of keeping two browsers in sync - moves travel between players in milliseconds, and the connection survives brief network hiccups without dropping the game.

  • Structured events - Move transmission, resignation, time control, and game completion
  • State consistency - Server-authoritative game state with client-side prediction
  • Latency optimization - Reduced round-trip delays through optimized serialization

The key insight here: the server is the source of truth, but the client doesn't wait for permission. It moves first, then the server confirms. If the server disagrees, it corrects quietly. That's what makes it feel responsive.

Monorepo Organization

The codebase follows a Turborepo structure with clear separation between applications and shared packages:

ComponentResponsibility
Chess LogicMove validation, rules enforcement, game utilities, Elo calculations
AuthenticationOAuth flows, session management, secure user verification
DatabasePrisma schema definition and type-safe client generation
UI SystemShared components, design tokens, responsive layouts
PaymentsStripe integration, subscription management, billing logic

This modular approach ensures code reusability, reduces duplication, and simplifies maintenance across the system.

Repository Layout

The project is a Turborepo monorepo with two main apps and a suite of shared packages.

Apps

AppDescription
apps/webNext.js 16 frontend with Turbopack and authentication
apps/apiExpress server handling Socket.IO real-time events
apps/emailReact Email templates with hot-reload preview
apps/studioPrisma Studio for database management

Shared Packages

PackageRole
@workspace/authBetter Auth - session management and OAuth
@workspace/chessGame logic and move validation utilities
@workspace/dbPrisma schema and PostgreSQL client
@workspace/uiShared shadcn/ui + Tailwind component library
@workspace/paymentsStripe integration for Pro memberships
@workspace/emailReact Email templates + Resend integration
@workspace/rate-limitAPI rate limiting utilities

Production Considerations

Scalability

The Socket.IO + Redis architecture scales horizontally:

  • Redis - Pub/sub layer that synchronizes real-time events across server instances; Socket.IO rooms group players (e.g., per match).
  • Database - PostgreSQL handles all persistent data with connection pooling

Redis sounds like overkill for a chess app. It isn't. The moment you want two servers talking to each other about the same game, you need a shared brain - and Redis is the cheapest one.

Reliability

  • Automatic reconnection - Clients reconnect on connection loss with state recovery
  • Server persistence - Game state survives temporary server restarts
  • Error recovery - Graceful degradation if subsystems become unavailable

Security

  • Authentication - Sessions managed by Better Auth with encryption
  • Move validation - Server authorizes all game state changes
  • Payment security - PCI compliance through Stripe; application never touches payment details
  • Rate limiting - API endpoints protected against abuse
  • SQL injection prevention - Prisma parameterizes all database queries

How It Works

Gameplay Flow

Player connects - Authentication with Better Auth and session establishment

Queue entry - Redis stores matchmaking state; players wait in configured queue

Match creation - Server pairs compatible players and initializes game state

Real-time play - Socket.IO synchronizes moves between players with latency compensation

Game conclusion - Elo ratings updated; game recorded in database for history and replays

Statistics update - Player stats reflect new rating and match count

Client-Server Synchronization

The client predicts moves locally for responsive feel; the server validates and broadcasts authoritative moves to all players:

  • Optimistic updates - Client executes moves immediately for perceived latency
  • Server validation - Moves verified against rules before database commit
  • Broadcast - Validated moves sent to opponent, ensuring state consistency
  • Conflict resolution - Invalid moves are rolled back with user notification

Tech Stack

  • Next.js 16 with Turbopack
  • TypeScript throughout
  • shadcn/ui + Tailwind CSS for UI components
  • Better Auth for session management and OAuth
  • Express REST API
  • Socket.IO for real-time bidirectional communication
  • Redis for cache, and game state management with Socket.IO adapter
  • Prisma ORM with PostgreSQL
  • Stripe for payments
  • Resend + React Email for transactional emails
  • Turborepo for monorepo build orchestration
  • pnpm workspaces
  • Docker + Docker Compose for local and production environments
  • GitHub Actions for CI/CD
  • ESLint, Prettier, Husky for code quality

Key Technical Decisions

Why a monorepo? Sharing types, UI components, and business logic - like chess rules - across the frontend and backend without duplication was the main driver. Running the same move-validation logic on both sides wasn't a clever trick; it was a necessity. Turborepo's build caching made the whole thing fast enough to not hate.

Why Socket.IO over WebSockets directly? Socket.IO's automatic reconnection, room management, and fallback support made it a practical choice for a game where a dropped connection means a lost match. The abstraction cost is worth it.

Why Better Auth over NextAuth? Better Auth offered more flexibility for the session model I needed - especially for mixing OAuth with custom credential flows without fighting the library's assumptions at every turn.


Challenges

Synchronizing game state - For a while, the game felt broken. Moves would land on one screen slightly before the other, and occasionally the board states would diverge. The fix was counterintuitive: run the chess engine on the client too, accept that it might be wrong, and let the server correct it quietly. Optimistic updates plus server authority. Once I had that mental model, the rest fell into place.

Monorepo tooling from scratch - Setting up shared tsconfig, eslint, and workspace dependencies had a real learning curve. But the payoff was catching integration bugs early - the kind that only show up when two apps share a type and one of them changes it.


What I Learned

Real-time systems aren't about speed - they're about trust. The client has to believe the server is right, even when it's showing you a move that hasn't been confirmed yet. Getting that trust loop right is the whole job.

Monorepo discipline pays off - Shared packages prevent duplication and enforce consistent patterns. The friction upfront is worth it.

Type safety catches bugs early - TypeScript strict mode across frontend and backend stops integration errors before they reach runtime.

Infrastructure as code reduces friction - Docker and declarative configuration mean the environment is never a variable.


Summary

Play Chess is what happens when you use a familiar problem to learn unfamiliar infrastructure. Chess gave me a bounded domain with clear rules; the real challenge was everything around it - keeping two clients in sync, scaling across servers, handling payments, building a monorepo from scratch.

The patterns here - optimistic updates, server authority, shared logic packages, real-time pub/sub - aren't unique to chess. They show up in collaborative tools, live dashboards, multiplayer games, anything where multiple users need to agree on the same state at the same time.

If I were starting over, I'd reach for this stack again. Not because it's perfect, but because every tradeoff was legible - and legible tradeoffs are how you actually learn.