# 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**

```plaintext
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**

```plaintext
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**

```sql
CREATE DATABASE chess_app;
```

**Create a user (role) and set a password**

```sql
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**

```sql
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**

```sql
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**

```sql
INSERT INTO players (username, elo_rating) VALUES ('alice', 1600);

UPDATE players
SET elo_rating = 1650
WHERE username = 'alice';
```

**Modify table structure (**`ALTER`**)**

```sql
-- 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**

```sql
- `\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):**

```shell
docker exec -it chess_postgres psql -U chess_user -d chess_db
```

\- **Local host** (when Postgres is exposed, e.g. [`docker-compose.dev`](http://docker-compose.dev)`.yml` maps `5432:5432`):

```shell
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**

```shell
\l
```

**3\. Connect to a database** (from inside `psql`)

```shell
\c chess_db
```

**4\. List all tables**

```shell
\dt
```

**5\. Table structure** (replace `users` with your table name; Prisma often uses plural model names)

```shell
\d users
```

**6\. View data**

```sql
SELECT * FROM users;
```

**7\. List database roles (users)**

```sql
\du
```

**8\. Current session user**

```sql
SELECT current_user;
```

**9\. Delete data**

\- Specific row:

```sql
DELETE FROM users WHERE id = 1;
```

\- All rows (table and schema stay):

```sql
DELETE FROM users;
```

\- Faster clear (often used in dev); keeps table:

```sql
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`:

```sql
\q
```

```shell
psql -U chess_user -d postgres
```

```sql
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:

```sql
DROP USER chess_user;
```

**Shortcuts**

```plaintext
| 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:

```sql
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/`:

```shell
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)**

```plaintext
| 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**

```plaintext
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

```mermaid
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`:

```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:

```yaml
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

```mermaid
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
