Building a Production-Ready Multiplayer Chess App

This blog explains the exact data and infrastructure stack used in this chess project:
Redis for real-time game state
PostgreSQL for relational and durable core data
Cassandra/Astra DB for high-volume event-style data (chat + move logs)
Docker for consistent local and production deployments
The examples below align with the current codebase (React + Bun + Socket.io + Prisma + Redis + Astra Data API).
1) Redis: What It Is, How It Works, and Why It Fits Chess
What is Redis?
Redis is an in-memory key-value data store. It is extremely fast (sub-millisecond operations in most cases) and supports rich data structures like:
Strings
Hashes
Sets
Lists
Sorted sets
Because multiplayer chess needs very low-latency updates, Redis is a strong choice for volatile game/session state.
How Redis Works (Conceptually)
Redis keeps data in RAM, so reads/writes are very fast. Your app sends commands like:
HSET/HGETALLfor game objectsSADD/SCARDfor spectatorsEXPIREfor automatic cleanup
In this project:
Room game state is stored as a Redis hash (
game:<roomCode>)Players are mapped in a hash (
players:<roomCode>)Spectators are tracked in a set (
spectators:<roomCode>)Socket-to-room mapping uses a string (
socket:<socketId>)TTL is 6 hours (
21600seconds)
How Redis Is Integrated in This Chess App
server/src/config/redis.ts creates a shared ioredis client, sets retry behavior, and key helpers.
server/src/services/RoomService.ts uses Redis as the real-time authority for:
Room creation and initial FEN/time settings
Join/rejoin/disconnect flows
Spectator add/remove/count
Fast room lookup by socket ID
TTL refresh (
touchKeys) to keep active games alive
Redis Data Flow Diagram
flowchart LR
C1[Player A Client] -->|Socket Event| API[Bun + Socket.io Server]
C2[Player B Client] -->|Socket Event| API
C3[Spectator Client] -->|Socket Event| API
API -->|HSET/HGETALL game:room| R[(Redis)]
API -->|HSET/HGETALL players:room| R
API -->|SADD/SCARD spectators:room| R
API -->|SET socket:socketId| R
API -->|EXPIRE 21600s| R
API -->|Broadcast state| C1
API -->|Broadcast state| C2
API -->|Broadcast state| C3
2) PostgreSQL: What It Is, How It Works, and Why It Fits Chess
What is PostgreSQL?
PostgreSQL is an advanced relational database (SQL, ACID transactions, strong consistency). It is ideal for structured and durable business data.
How PostgreSQL Works (Conceptually)
Data is stored in tables with relations and constraints. You query/update rows using SQL (or ORM abstractions such as Prisma).
In a chess app, relational data typically includes:
Users and ratings
Official game records
Match results and PGN metadata
How PostgreSQL Is Integrated in This Chess App
server/src/config/db.ts initializes Prisma client and manages connect/disconnect lifecycle.
Business services use Prisma:
server/src/services/UserService.tsCreate/find users
Update ELO
Increment win/loss/draw stats
server/src/services/GameService.tsCreate game records
Find an active game by room
Persist result/winner/PGN and mark end time
This design separates:
Redis: live/ephemeral game state
PostgreSQL: durable source of truth for user/game records
PostgreSQL Data Flow Diagram
flowchart TB
API[Bun API Layer] --> PRISMA[Prisma Client]
PRISMA --> PG[(PostgreSQL)]
API -->|create/update user| PRISMA
API -->|create game record| PRISMA
API -->|save result + PGN| PRISMA
Common PostgreSQL commands (CLI reference)
These are typical operations when setting up or adjusting a database outside Prisma migrations. Connect as a superuser (e.g. postgres) with psql or run docker compose exec postgres psql -U postgres.
Create a database
CREATE DATABASE chess_app;
Create a user (role) and set a password
CREATE USER 'db_name_user' WITH PASSWORD 'choose_a_strong_password';
-- Or use a role that can log in:
CREATE ROLE 'db_name_user' LOGIN PASSWORD 'choose_a_strong_password';
Grant access to the database and schema defaults
GRANT CONNECT ON DATABASE 'db_name' TO 'db_name_user';
GRANT USAGE ON SCHEMA public TO 'db_name_user';
GRANT CREATE ON SCHEMA public TO 'db_name_user'; -- optional: allow creating tables
Create a table
CREATE TABLE players (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
elo_rating INTEGER NOT NULL DEFAULT 1500,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
Insert and update rows
INSERT INTO players (username, elo_rating) VALUES ('alice', 1600);
UPDATE players
SET elo_rating = 1650
WHERE username = 'alice';
Modify table structure (ALTER)
-- Add a column
ALTER TABLE players ADD COLUMN games_played INTEGER NOT NULL DEFAULT 0;
-- Change a column type (example: widen text)
ALTER TABLE players ALTER COLUMN username TYPE VARCHAR(64);
-- Rename a column
ALTER TABLE players RENAME COLUMN elo_rating TO rating;
-- Add a constraint
ALTER TABLE players ADD CONSTRAINT rating_non_negative CHECK (rating >= 0)
Handy psql commands
- `\l` — list databases
- `\c chess_app` — connect to a database
- `\dt` — list tables
- `\d players` — describe table `players`
- `\q` — quit
PostgreSQL cheat sheet (this project: chess_db + Docker)
The root docker-compose.yml uses container name chess_postgres, database chess_db, and user chess_user (password from POSTGRES_PASSWORD in your env).
1. Connect
- Docker (into the running container):
docker exec -it chess_postgres psql -U chess_user -d chess_db
- Local host (when Postgres is exposed, e.g. docker-compose.dev.yml maps 5432:5432):
psql -U chess_user -d chess_db
If you use a non-default port: psql -U chess_user -d chess_db -p 5433 (adjust to match your DATABASE_URL).
2. List all databases
\l
3. Connect to a database (from inside psql)
\c chess_db
4. List all tables
\dt
5. Table structure (replace users with your table name; Prisma often uses plural model names)
\d users
6. View data
SELECT * FROM users;
7. List database roles (users)
\du
8. Current session user
SELECT current_user;
9. Delete data
- Specific row:
DELETE FROM users WHERE id = 1;
- All rows (table and schema stay):
DELETE FROM users;
- Faster clear (often used in dev); keeps table:
TRUNCATE TABLE users;
11. Drop a database
Exit the DB first \q), then connect as a role that may drop databases (often the superuser created by the image). With the official Postgres image, it POSTGRES_USER is chess_user, that user is typically a superuser and can drop chess_db:
\q
psql -U chess_user -d postgres
DROP DATABASE chess_db;
If your user cannot drop the database, connect as postgres (or the image’s superuser) and run DROP DATABASE chess_db;.
12. Drop a role (user)
You cannot be connected as the role you are dropping. Connect as another superuser, then:
DROP USER chess_user;
Shortcuts
| Command | Meaning |
|---------|----------------|
| `\dt` | List tables |
| `\l` | List databases |
| `\du` | List roles |
| `\q` | Quit `psql` |
Common errors
- relation "users" does not exist — List tables with \dt and use the exact table name from your Prisma schema (could be User mapped to "users" or another name).
- permission denied — Grant privileges (run as superuser or owner), for example:
GRANT ALL PRIVILEGES ON DATABASE chess_db TO chess_user;
Also, grant on schema/tables if needed after migrations.
Docker + Prisma tip
Avoid manually dropping tables whenever you want a clean dev database aligned with migrations. From server/:
npx prisma migrate reset
That reapplies migrations and optional seed; it is safer than ad-hoc DROP TABLE When the app is driven by Prisma.
Prisma vs SQL (quick mapping)
| Goal | Prisma (CLI) | Raw SQL / `psql` |
|------|----------------|------------------|
| Apply migrations | `npx prisma migrate deploy` | Run migration `.sql` files by hand |
| Dev: reset DB | `npx prisma migrate reset` | `DROP` / `TRUNCATE` tables yourself |
| Inspect schema | `npx prisma studio` or open `schema.prisma` | `\d table_name` |
| Create migration after model change | `npx prisma migrate dev --name ...` | `CREATE TABLE` / `ALTER TABLE` yourself |
3) Cassandra / Astra DB: What It Is, How It Works, and Why It Fits Chess Logs
What is Cassandra / Astra DB?
Apache Cassandra is a distributed NoSQL database optimized for high write throughput, horizontal scaling, and fault tolerance.
DataStax Astra DB is a managed cloud platform for Cassandra. In this project, the Astra Data API is used for persistence.
How Cassandra Works (Conceptually)
Instead of relational joins, Cassandra models data around query patterns and partitions. It is excellent for time-series/event-like workloads where writes are frequent and volume is high.
For chess, that fits:
Chat messages
Move-by-move logs
How Astra Is Integrated in This Chess App
server/src/config/cassandra.ts:
Reads
ASTRA_DB_ENDPOINTandASTRA_DB_TOKENNormalizes endpoint to selected keyspace path
Enables/disables Astra persistence gracefully based on env vars
server/src/services/CassandraService.ts:
Sends HTTP POST requests to Astra Data API collections
Writes to:
chat_messagesmove_log
Reads ordered messages/moves for room replay/history
Handles failures with non-fatal warnings (keeps the game server resilient)
Cassandra/Astra Data Flow Diagram
flowchart LR
API[Bun + Socket Handlers] --> CS[CassandraService]
CS -->|POST insertOne/find| ASTRA[(Astra Data API)]
API -->|saveMessage| CS
API -->|saveMove| CS
API -->|getMessages/getMoves| CS
4) Docker: What It Is, How It Works, and How Client + Server Are Built
What is Docker?
Docker packages applications and dependencies into containers so they run consistently across machines (local, CI, VPS, cloud).
How Docker Works (Conceptually)
Dockerfiledefines how to build an image.docker composedefines multiple services and networking.Containers communicate via internal service names (DNS), e.g.
postgres,redis,app.
How Docker Is Used in This Project (Current Setup)
Root docker-compose.yml runs:
postgres(postgres:16-alpine)redis(redis:7-alpine)app(built fromserver/Dockerfile)nginx(reverse proxy + TLS + WebSocket upgrade)
server/Dockerfile is multi-stage:
Base stage with Bun + OpenSSL
Development stage (
bun run dev)Builder stage (
prisma generate, TypeScript build)Production stage (copy only runtime artifacts +
bun run start)
Production Container Architecture Diagram
flowchart TB
U[Browser / Client App] -->|HTTPS + Socket.io| N[Nginx Container]
N -->|Proxy /api + /socket.io| A[App Container: Bun Server]
A -->|Prisma| P[(PostgreSQL Container)]
A -->|ioredis| R[(Redis Container)]
A -->|HTTPS Data API| X[(Astra DB Cloud)]
How to Containerize Both Client and Server (Optional Full-Container Deployment)
Right now, the client is designed for Vercel deployment, while the backend is containerized.
If you want both client and server in Docker:
Add a
client/Dockerfilethat builds Vite assets and serves them with Nginx.Add
clientservice in compose.Route
/apiand/socket.iotoapp, and/toclient.
Example client/Dockerfile:
FROM oven/bun:1.2.8 AS builder
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install
COPY . .
ARG VITE_SERVER_URL
ENV VITE_SERVER_URL=${VITE_SERVER_URL}
RUN bun run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Example compose idea:
services:
client:
build:
context: ./client
args:
VITE_SERVER_URL: https://yourdomain.com
container_name: chess_client
restart: unless-stopped
networks: [chess_network]
Then Nginx can reverse proxy:
/->client:80/api+/socket.io->app:4000
Client + Server Container Diagram
flowchart LR
Browser --> NginxEdge[Nginx Reverse Proxy]
NginxEdge -->|/| Client[Client Container - static build]
NginxEdge -->|/api, /socket.io| Server[Server Container - Bun API]
Server --> Postgres[(PostgreSQL)]
Server --> Redis[(Redis)]
Server --> Astra[(Astra DB)]
Why This Hybrid Storage Design Works for Multiplayer Chess
Redis gives low-latency real-time room state.
PostgreSQL keeps durable, relational entities and outcomes.
Cassandra/Astra handles event-style history at scale.
Docker gives reproducible deploys and clean service boundaries.
This separation keeps gameplay responsive while preserving long-term data correctly.
Quick Integration Checklist
Redis:
Define key naming convention and TTL strategy
Store only hot/ephemeral room/session data
PostgreSQL:
Keep users, ratings, and final game metadata
Use Prisma migrations + schema discipline
Astra/Cassandra:
Persist high-write chat/move history collections
Model for query paths (by room + order)
Docker:
Use service DNS names (
postgres,redis,app)Add health checks and
depends_onconditionsKeep multi-stage images for a smaller production footprint



