EyeOnChess is a monorepo with 6 workspace packages + 3 tooling configs, deployed as multiple Docker services behind an Nginx reverse proxy.
┌─────────┐
│ Nginx │ :80/:443
└────┬────┘
┌─────────┼─────────┐
▼ ▼ ▼
┌──────────┐ ┌──────┐ ┌──────────┐
│ Web │ │ API │ │ Admin │
│ :3000 │ │:3001 │ │ :3002 │
└──────────┘ └──┬───┘ └──────────┘
┌──┴───┐
│ │
┌──────▼──┐ ┌─▼─────┐
│PgBouncer│ │ Redis │
│ :6432 │ │ │
└────┬────┘ └───┬────┘
┌────▼────┐ │
│Postgres │ │
│ :5432 │ │
└─────────┘ │
│
┌────────▼────────┐
│ Worker │
│ (Stockfish) │
└─────────────────┘
| Service |
Technology |
Purpose |
| Nginx |
Nginx Alpine |
Reverse proxy — routes subdomains to Web, Admin, Grafana; /api/v1 to API |
| Web |
Next.js 14 (App Router) |
Player-facing frontend — server-side rendering, client-side navigation |
| Admin |
Next.js 14 (App Router) |
Admin panel — separate subdomain (admin.{domain}), shared API |
| API |
Fastify + Socket.io |
REST API (versioned /api/v1/) + real-time WebSocket events |
| Migrate |
Node.js + Prisma |
Database migrations + seeds — init container, runs once then exits |
| Worker |
Node.js + Stockfish 15 |
Background analysis — polls Redis queue, runs Stockfish on positions |
| Postgres |
PostgreSQL 16 Alpine |
Primary database — users, games, moves, analysis, settings |
| PgBouncer |
edoburu/pgbouncer |
Connection pooler — transaction mode, 20 pool size, 200 max clients |
| Redis |
Redis 7 Alpine |
Ephemeral data — online presence, game clocks, analysis queue, caching |
| Layer |
Technology |
| Frontend |
Next.js 14, TypeScript, Tailwind CSS, Zustand |
| Board UI |
Chessground (lichess board library) |
| Chess Logic |
chess.js |
| Backend |
Fastify, TypeScript, Zod |
| ORM |
Prisma 6 |
| Connection Pool |
PgBouncer (transaction mode) |
| Real-time |
Socket.io |
| Analysis |
Stockfish 15 (depth 18) |
| Auth |
Custom JWT (access + refresh token rotation) |
| Database |
PostgreSQL |
| Cache |
Redis |
| Proxy |
Nginx |
| Deployment |
Docker Compose |
| Package Manager |
pnpm (workspaces) |
| Monorepo |
Turborepo |
- User submits credentials → API validates → returns JWT access token (15min) + sets httpOnly refresh cookie (7 days)
- Access token stored in memory (Zustand) — never localStorage
- On 401, Axios interceptor auto-refreshes via cookie → retries request
- Refresh tokens are SHA-256 hashed in DB, rotated on each use
- Player challenges friend → API creates WAITING game → Socket emits
challenge:incoming
- Friend accepts → API sets ACTIVE → clocks initialized in Redis
- Each move: client emits
game:move → API validates with chess.js → persists to DB → updates Redis clocks → broadcasts to room
- On checkmate/stalemate/timeout/resignation → Elo ratings updated →
game:over emitted
- User clicks "Analyze" → API queues gameId in Redis list
- Worker polls queue → spawns Stockfish → evaluates every position at depth 18
- Each move classified by centipawn loss → GameAnalysis + MoveFeedback saved to DB
- Frontend polls status → displays interactive analysis board
- Socket connects with JWT → userId set in Redis with 30s TTL
- Client sends heartbeat every 20s → refreshes TTL
- Friends list queries Redis for bulk online status