Skip to main content

Command Palette

Search for a command to run...

Building a Production-Ready Multiplayer Chess App

Published
11 min read
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 / HGETALL for game objects

  • SADD / SCARD for spectators

  • EXPIRE for 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 (21600 seconds)

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.ts

    • Create/find users

    • Update ELO

    • Increment win/loss/draw stats

  • server/src/services/GameService.ts

    • Create 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_ENDPOINT and ASTRA_DB_TOKEN

  • Normalizes 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_messages

    • move_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)

  • Dockerfile defines how to build an image.

  • docker compose defines 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 from server/Dockerfile)

  • nginx (reverse proxy + TLS + WebSocket upgrade)

server/Dockerfile is multi-stage:

  1. Base stage with Bun + OpenSSL

  2. Development stage (bun run dev)

  3. Builder stage (prisma generate, TypeScript build)

  4. 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:

  1. Add a client/Dockerfile that builds Vite assets and serves them with Nginx.

  2. Add client service in compose.

  3. Route /api and /socket.io to app, and / to client.

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_on conditions

    • Keep multi-stage images for a smaller production footprint