This repository contains a modern, microservice-based chess platform. Built for high concurrency and low latency, it handles real-time matchmaking, real-time gameplay via WebSockets, scalable tournaments (Swiss and Round-Robin), and persistent user state.
graph TD
Client[Web Client / Mobile]
LB[Cloud Load Balancer]
Ingress[NGINX Ingress]
subgraph "Frontend Services"
Web[Web Frontend - React/Next.js]
WSS[WebSocket Server - Node.js]
end
subgraph "Core Backend Services"
HTTP[HTTP API Server - Node.js]
Game[Game Server - Rust / gRPC]
Matchmaker[Matchmaker - Rust / Actix]
end
subgraph "Asynchronous Workers"
CDC[CDC Pipeline - Rust]
SyncWorker[Sync Worker - Rust]
NotifWorker[Notification Worker - Node.js]
end
subgraph "Data & Messaging"
PG[(PostgreSQL)]
Redis[(Redis)]
RabbitMQ((RabbitMQ))
end
Client -->|HTTPS| LB
Client -->|WSS| LB
LB --> Ingress
Ingress -->|HTTPS| Web
Ingress -->|HTTPS| HTTP
Ingress -->|WSS| WSS
WSS -->|gRPC| Game
HTTP -->|RPC/REST| Matchmaker
HTTP -->|Read/Write| PG
CDC -->|WAL tailing| PG
CDC -->|Schedule Events| Redis
SyncWorker -->|Consume| RabbitMQ
SyncWorker -->|Consume| Redis
SyncWorker -->|Update| PG
Matchmaker <-->|State/Streams| Redis
WSS <-->|PubSub / State| Redis
Game <-->|State| Redis
HTTP -->|Publish| RabbitMQ
NotifWorker -->|Consume| RabbitMQ
graph LR
WS[WS Server] -->|Game Over| Game
Game -->|Publish Result| RMQ((RabbitMQ))
RMQ -->|Consume| Sync[Sync Worker]
Sync -->|Update Standings| PG[(PostgreSQL)]
The system is divided into focused microservices to scale different workloads independently. Synchronous user actions (login, profile updates) are handled by a traditional REST API (HTTP Server) backed by PostgreSQL. Real-time game interactions and moves occur over WebSockets (WS Server), which communicate with the authoritative Game Server via low-latency gRPC.
Asynchronous event processing is heavily decoupled. Matchmaking is handled by a dedicated Rust service using the Actor model and Redis. Tournament progression is entirely event-driven: a CDC (Change Data Capture) service tails the PostgreSQL Write-Ahead Log (WAL) to detect tournament creations, which feeds into a distributed scheduling pipeline processed by the Sync Worker. RabbitMQ guarantees the delivery of these asynchronous domain events.
Responsibility: Authoritative REST API for user authentication, profiles, tournament creation, and admin panels. How It Works: Receives standard HTTPS requests, validates payloads, and reads/writes to PostgreSQL. Communication: Exposes a REST API. Communicates synchronously with PostgreSQL. Publishes events to RabbitMQ (e.g., for notifications). Data and State: Purely stateless. Relies on PostgreSQL for authoritative state. Scaling: Scales horizontally. Stateless design allows standard L7 load balancing.
Responsibility: Manages all real-time WebSocket connections with clients for live gameplay, matchmaking updates, and spectator broadcasting.
How It Works: Clients connect via WebSockets. The server acts as a gateway, receiving moves and routing them to the authoritative Game Server for validation.
Communication: WebSockets to external clients. gRPC to the Game Server. Redis Pub/Sub for cross-pod communication. Redis Hashes for game state.
Data and State: Ephemeral socket state in memory. Live game state and spectator tracking stored in Redis (game:state:* and spectate:*).
Scaling: Scales horizontally using SocketIORedisAdapter. Redis Pub/Sub handles broadcasting events (like a move made on Pod A) to spectators connected to Pod B.
Responsibility: Evaluates player pools, pairs players of similar ELOs, and triggers match creation.
How It Works: Uses the Actix actor framework. Players enter the queue via a Redis Stream. Actors own the state of specific time controls (e.g., Bullet, Blitz). They periodically evaluate their queue, expanding the acceptable ELO gap over time until a match is found.
Communication: Reads player joins via Redis Streams. Writes matches to Redis.
Data and State: Rapidly changing matchmaking candidate state is held in actor memory (using a BTreeMap). Persistent queues are backed by Redis Sorted Sets (ZSET).
Failure and Recovery: On startup, syncs from Redis to recover queue state.
Scaling: Bounded by the number of time-control pools. Each time-control is a single Actor to prevent race conditions during matching.
Responsibility: The authoritative source of truth for chess logic, move validation, and game termination. How It Works: Receives move requests. Validates them against the current board state. Determines checkmate, draw, or timeout conditions. Communication: Exposes a gRPC interface consumed by the WebSocket Server. Reads/writes to Redis for fast state retrieval. Data and State: Stateless application layer; relies on Redis for storing active game FENs, move histories, and clocks. Scaling: Highly horizontally scalable as a gRPC service deployed behind a Kubernetes Headless Service.
Responsibility: Triggers tournament scheduling without polling the database.
How It Works: Tails the PostgreSQL Write-Ahead Log (WAL) using logical replication (pg_output). When a tournament is inserted, it creates a schedule job in Redis.
Communication: Communicates with PostgreSQL directly. Writes jobs to Redis ZSETs.
Data and State: Tracks processed LSNs (Log Sequence Numbers) in Redis (cdc:pending_lsns) to ensure it resumes correctly after a crash.
Scaling: Single instance/singleton deployment to maintain sequential WAL reading.
Responsibility: Executes scheduled tournament events, generates pairings (Swiss & Round Robin), and processes game results. How It Works: Processes distributed jobs using a two-phase Redis ZSET queue and consumes RabbitMQ events. Communication: Consumes from RabbitMQ and Redis Streams. Reads/writes to PostgreSQL and Redis. Failure and Recovery: Implements dead-letter queues (DLQ) in RabbitMQ. Redis jobs have a visibility timeout; a watchdog re-queues jobs if a worker crashes mid-processing. Scaling: Horizontally scalable. Workers compete for jobs via atomic Lua scripts and RabbitMQ consumer groups.
Responsibility: Asynchronous email and push notification delivery. Communication: Consumes strictly from RabbitMQ. Scaling: Scales horizontally based on queue depth.
In this project, Redis is used as the high-speed state layer for ephemeral and frequently accessed data, avoiding database contention.
- Matchmaking State:
QUEUE_ZSETholds pending players. - Game State:
game:state:*hashes store active game FENs, players, and clocks. - Spectator State:
spectate:*tracks active observers. - Live Games: A ZSET
live:gamesprovides a real-time feed of active matches. - Locks: Distributed locking using
SET NX PXprevents concurrent execution (e.g., tournament scheduler watchdogs, rematch handling).
The matchmaker receives players via Redis Streams (matchmaker:stream).
- Producers: The HTTP or WS server pushes a
PlayerJoinpayload. - Consumers: The matchmaker worker reads via Consumer Groups (
XREADGROUP). - Acknowledgement:
XACKis called only after the player is successfully staged in the Actor's queue and backed up to a ZSET. - Failures: If the matchmaker crashes before
XACK, the message remains in the Pending Entry List (PEL) and is reprocessed on restart.
RabbitMQ decouples heavy domain workflows (like Tournaments) from synchronous user flows.
- Exchanges & Queues: Direct and Topic exchanges route events like
MatchNotificationEventandTournamentMatchingDlqEvent. - Reliability: Consumers require explicit acknowledgements. Unprocessable messages are routed to a Dead Letter Queue (DLQ) for inspection.
Instead of polling SELECT * FROM tournaments WHERE start_time <= NOW(), the architecture uses zero-polling CDC.
graph TD
DB[(PostgreSQL)]
DB -->|WAL INSERT| CDC[CDC Pipeline]
CDC -->|Job Event| RedisZ[Redis PENDING ZSET]
RedisZ -->|Scheduled Time Reached| SyncWorker[Sync Worker]
SyncWorker -->|Start Tournament| AppState[Application State]
This ensures zero database CPU overhead for scheduling and near-instant reactivity.
gRPC is used for high-throughput, low-latency internal communication—specifically between the WS Server and the Game Server.
- Why gRPC: The WS Server receives thousands of move events per second. gRPC with HTTP/2 multiplexing significantly reduces connection overhead compared to REST.
- Service Definitions: Protobuf definitions are centralized in
packages/grpc-connection. - L4 vs L7 Considerations: Because gRPC uses long-lived HTTP/2 connections, traditional L4 Kubernetes Services (ClusterIP) result in uneven load distribution (all traffic sticks to one pod).
- Solution: A Kubernetes Headless Service (
game-server-headless) is used, allowing the client to resolve all Pod IPs and perform client-side round-robin load balancing.
The matchmaking engine is built on the Actix framework in Rust.
Each matchmaking pool (e.g., 3|0 Blitz, 10|0 Rapid) is an isolated Actor. This eliminates shared-state locks. An actor processes one player join/leave message at a time sequentially.
The actor maintains candidate players in memory using a BTreeMap structured by rating.
VecDeque is not implemented in the matchmaking pool in this repository. A BTreeMap is used instead because matchmaking requires fast range queries (e.g., finding candidates within rating - 50 to rating + 50), which BTreeMap handles efficiently (O(log N)) whereas VecDeque would require O(N) linear scanning.
- Player enters via Stream.
- The Actor inserts the player into the
BTreeMapand the Redis ZSET backup. - Every tick, the Actor checks presence (via Redis pipeline) to ensure players haven't disconnected.
- The algorithm calculates
wait_time * EXPANSION_RATEto widen the acceptable ELO gap. BTreeMap::rangeefficiently finds opponents in the expanded ELO bracket.- A match is published, and players are atomically removed from the pool.
The tournament and event scheduler uses a two-phase Redis ZSET approach.
graph TD
CDC[CDC] -->|ZADD fire_time| ZP[PENDING ZSET]
Watchdog[Watchdog] -->|Move if due| ZProc[PROCESSING ZSET]
Watchdog -->|XADD| Stream[Schedule Stream]
Worker[Sync Worker] -->|Consume & Execute| Stream
Worker -->|ZREM on Success| ZProc
- ZSET Score: Represents the Unix timestamp when the job is due.
- Failure Recovery: If a worker crashes, the job remains in
PROCESSING_ZSET. A watchdog scans for jobs with visibility timeouts that have expired and re-queues them.
Tournaments are deeply integrated into the asynchronous worker pipeline.
graph LR
Create[Tournament Creation] --> CDC[CDC Tailing]
CDC --> Sched[Scheduling]
Sched --> Init[Tournament Init]
Init --> Pair[Pairing]
Pair --> Match[Matches Played]
Match --> Res[Results / Standings]
Res --> Next[Next Round]
The repository implements a genuine Swiss pairing algorithm (Edmonds' blossom Maximum Weight Perfect Matching).
- Pairings: Players are grouped by score. The algorithm pairs players with identical scores while avoiding repeat matchups.
- Colors: Balances white/black assignments and prevents three identical colors in a row.
- Progression: The Sync Worker calculates standings, applies tiebreaks, and automatically schedules the next round.
For smaller club tournaments, the repository generates a full Round Robin matrix. All permutations are generated up-front, avoiding duplicates, and scheduled sequentially.
- Connection: Spectators connect via WebSockets to
apps/ws-server. - State: Live board state is pulled from
spectate:<game_id>in Redis. - Broadcast: When a move is made, the WS Server broadcasts it to the room. Redis Pub/Sub ensures spectators on any WS pod receive the event.
WebSocket state is strictly bound to the pod the client connected to. To solve horizontal scaling, SocketIORedisAdapter is utilized. When Pod A needs to broadcast to a room, it publishes to Redis; Pod B receives the pub/sub event and pushes it to its local connected clients.
To avoid expensive backend CPU usage, game analysis (Stockfish) is offloaded to the client using WebAssembly (WASM).
- Implementation: The
apps/webfrontend usesuseStockfish.tsto load the Stockfish WASM engine. - Execution: Analysis runs locally in the user's browser via Web Workers, entirely isolated from backend load.
Deployed via Argo CD, the infrastructure uses modern Kubernetes primitives.
- Services: Frontend and HTTP API use standard
ClusterIPservices. - Ingress: NGINX Ingress controller routes external HTTPS and WSS traffic. TLS is managed via
cert-manager. - Headless Services: Used for gRPC (
game-server-headless) to bypass kube-proxy and enable client-side load balancing. - Secrets:
SealedSecretsencrypt configuration directly in Git.
graph LR
Dev[Developer] -->|Push| Git[GitHub]
Git -->|Actions Build & Test| GHCR[Container Registry]
Argo[Argo CD] -->|Syncs Manifests| Git
Argo -->|Deploys| K8s[Kubernetes Cluster]
- CI: GitHub Actions builds Docker images, runs tests, and pushes to GHCR.
- GitOps: Argo CD continuously monitors
chess-k8s/apps. When configurations change or image tags are updated, Argo CD automatically reconciles the Kubernetes cluster state to match Git.
| Service | Talks To | Protocol | Purpose | State |
|---|---|---|---|---|
| HTTP Server | PostgreSQL | TCP (pg) | Reads/writes user data | Stateless |
| HTTP Server | RabbitMQ | AMQP | Send emails | Stateless |
| WS Server | Game Server | gRPC | Move validation | Ephemeral/Redis |
| WS Server | Redis | Redis (PubSub) | Cross-pod broadcast | Ephemeral/Redis |
| Matchmaker | Redis | Redis | Queue ingestion/evaluation | Actor memory/Redis |
| CDC | PostgreSQL | Logical Repl | Detect new tournaments | Redis LSN |
| Sync Worker | RabbitMQ | AMQP | Process results | Stateless |
| Sync Worker | PostgreSQL | TCP (pg) | Update standings | Stateless |
| Decision | Why | Trade-Off |
|---|---|---|
| Microservices | Isolates critical real-time components (Game/WS) from slow transactional ones (HTTP/Postgres). | Increased deployment complexity and debugging overhead. |
| Actor Model | Prevents race conditions during matchmaking by giving a single thread ownership of a specific player pool. | A single hot time-control (e.g., Bullet) is bound to a single thread's throughput. |
| BTreeMap vs VecDeque | Matchmaking requires finding ELO brackets (ranges). VecDeque is O(N) for this, while BTreeMap is O(log N). |
Slightly higher memory overhead and insertion cost. |
| WASM Stockfish | Offloads massive CPU computation to the client's device. | Increased initial page load time to download the WASM binary. |
| CDC WAL Tailing | Zero CPU overhead on PostgreSQL for scheduling checks. | Increased operational complexity; requires logical replication slots. |
| gRPC Headless Svc | Allows client-side load balancing across game servers. | Exposes pod IPs directly to internal clients. |
- Crash Recovery with Distributed State: The architecture strongly separates compute (Kubernetes Pods) from state (Redis/PostgreSQL). When a service crashes, no critical state is lost:
- Matchmaker Crash: On restart, the new actor instances invoke
sync_from_redisto automatically recover their matchmaking queues (fromQUEUE_ZSET) and resume pairing without dropping players. - WebSocket Server Crash: If a WS gateway crashes, client connections drop. However, clients are built to auto-reconnect to another healthy pod. Since live game states and spectator mappings are centralized in Redis (
game:state:*andspectate:*), the new pod seamlessly resumes the session. - Worker Crash: If a
sync-workercrashes mid-execution, a Redis ZSET watchdog scans for jobs with expired visibility timeouts inPROCESSING_ZSETand re-queues them. Unacknowledged RabbitMQ messages are safely redelivered to other healthy workers. - CDC Crash: The CDC worker writes its last processed LSN (Log Sequence Number) to Redis (
cdc:pending_lsns). On crash recovery, it reads this LSN to resume tailing the PostgreSQL WAL exactly where it left off.
- Matchmaker Crash: On restart, the new actor instances invoke
- Redis Crash: High impact. Matchmaking queues would empty, and active games would drop. Requires Redis persistence (AOF/RDB) or HA clustering.
- Database: Postgres is the ultimate bottleneck, but it is heavily shielded. It only handles persistent profiles and final tournament results.
- Matchmaker: Highly concurrent due to Actor isolation, but individual time controls cannot be sharded across multiple actors without coordination.
- WebSocket: Horizontally scales infinitely due to Redis Pub/Sub adapter.
.
├── apps/
│ ├── cdc/ # Rust Postgres WAL reader
│ ├── game-server/ # Rust gRPC Game logic
│ ├── http-server/ # Node.js REST API
│ ├── matchmaker/ # Rust Actix Matchmaking
│ ├── notification-worker/ # Node.js Email worker
│ ├── sync-worker/ # Rust Tournament/Scheduler worker
│ ├── web/ # Next.js Frontend
│ └── ws-server/ # Node.js WebSocket gateway
├── packages/ # Shared libraries (gRPC, DB clients, UI)
├── chess-k8s/ # Kubernetes manifests and ArgoCD configs
├── .github/workflows/ # CI/CD Pipelines
└── docker-compose.*.yml # Local development setups