Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

chess-war backend

A real-time multiplayer chess backend written in Rust, split into four services that communicate over RabbitMQ, Redis, and Postgres. "RPC" here means an internal request/reply protocol used to keep the public-facing edge service stateless and the business logic isolated.

Architecture

                 HTTP / WebSocket
   Client  ───────────────────────▶  api
                                      │
                                      │  AMQP request/reply ("rpc_queue")
                                      ▼
                                  rpc_server  ───────▶  Postgres  (games, users)
                                      │
                                      │  publish game_channel_{id}
                                      ▼
                                    Redis  ◀───────────  api (subscribes, relays to WS clients)

   worker  ───────────────────────▶  Postgres  (independent background reaper, no MQ)
  • api never touches game/auth business logic directly. It's a thin, stateless Axum HTTP + WebSocket gateway: it validates JWTs, forwards every game/auth command to rpc_server over RabbitMQ, and relays live game updates from Redis pub/sub to WebSocket clients.
  • rpc_server owns all business logic and is the only service that writes to Postgres. It consumes commands from RabbitMQ, applies them (auth hashing, move validation via the chess engine, optimistic-concurrency game updates), and publishes the resulting game state to Redis so subscribed WebSocket clients get pushed updates.
  • worker is an independent cron-like daemon with no MQ dependency. It runs two interval loops directly against Postgres: reaping games that never got a second player, and force-finalizing games where a player's clock ran out.
  • core (chess-core) is the shared library all three binaries depend on: a from-scratch bitboard chess engine, Postgres DTOs/queries (sqlx), a Redis pub/sub wrapper, and the RabbitMQ-based RPC client/server framework.

This separation means the public HTTP surface (api) can be scaled/restarted independently of the trusted logic tier (rpc_server), and neither is affected by the maintenance tier (worker).

Workspace layout

.
├── Cargo.toml               workspace root
├── justfile                 dev task runner (see below)
└── crates/
    ├── core/                chess_core — chess engine, DB layer, Redis client, RPC framework
    ├── api/                 api          — Axum HTTP/WebSocket gateway
    ├── rpc_server/          rpc_server   — business logic / RPC command handlers
    └── worker/              worker       — background reaper (stale games, timeouts)

Crates

core (chess_core)

Shared library, depended on by all three binaries.

  • engine — a hand-written bitboard chess engine implementing orthodox FIDE rules: FEN parsing/serialization, legal move generation, check/checkmate/stalemate detection, insufficient material, threefold repetition (via Zobrist hashing), and the fifty-move rule. Precomputed attack tables for knights/pawns/sliding pieces live under engine/attacks/; game state and rules live under engine/game/.
  • db — Postgres access via sqlx. connect.rs exposes the DB pool type; queries/ holds raw SQL (games, users); dto/ holds the API-facing response types converted from query rows. Notable: update_game uses an optimistic-concurrency check (old_fen) so concurrent writes to the same game fail as a conflict rather than clobbering each other; update_results auto-loses a player whose clock ran out, in bulk.
  • message_client — a thin Redis pub/sub wrapper. Games are broadcast on a game_channel_{game_id} topic; the API's WebSocket handler subscribes per game.
  • rpc — a custom request/reply protocol over RabbitMQ (lapin). Defines the AuthCommand/GameCommand/RpcCommand message types, RpcError/RpcErrorKind (mapped to HTTP status codes in api), an RpcClient (used by api) that publishes to a well-known queue with a reply-to + correlation ID and a 10s timeout, and an RpcServer (used by rpc_server) that consumes and dispatches those commands.

api

Axum HTTP + WebSocket gateway (binary: api). Holds a DB handle only for the CurrentUser extractor (looking up the authenticated user by JWT subject); all game and auth mutations are forwarded to rpc_server.

  • AuthPOST /api/v1/auth/register, /login, GET /refresh. Issues a short-lived (15 min) JWT access token in the JSON response and a 7-day refresh token as an httpOnly, Secure, SameSite=Strict cookie; /refresh rotates both from that cookie.
  • GamePOST /api/v1/game/create, GET /{game_id}, GET /token/{token} (share-link access), GET /share-token/{game_id} (creator-only, generates a 15-min sharing JWT), POST /select-side-by-id/{game_id}, POST /select-side-by-token/{token}, POST /{game_id}/move, POST /check-result, and GET /socket (WebSocket).
  • WebSocket (router/game/socket.rs) — clients send {"event": "subscribe" | "unsubscribe", "game_id": ...} and receive GameResponse JSON pushed whenever rpc_server updates that game.
  • Config (env-driven, see below) — DB/Redis/RabbitMQ URIs, HTTP port, JWT secrets (access/refresh/share), and the default per-side chess clock.

rpc_server

The application/business-logic tier (binary: rpc_server). No HTTP surface — it's a pure AMQP consumer that dispatches every RpcCommand to a handler.

  • Auth — registers users with Argon2 password hashing (unique-email conflicts are translated to RpcError::conflict); login verifies the Argon2 hash.
  • Game — creation (seeds both clocks from API_SIDE_TIME_LIMIT), side selection (by game id for the creator, by share token for an invited opponent), move handling (rejects finished/unstarted games, auto-loses the mover if their clock has expired, validates turn order, applies the move through chess_core::engine::Game, recomputes FEN/result, and persists with the optimistic-concurrency check), and a check-result endpoint for polling a timeout loss without making a move. Every mutation is followed by a broadcast of the new GameResponse over Redis so the API's WebSocket clients stay live.

worker

Background maintenance daemon (binary: worker), talking to Postgres only — no MQ dependency. Runs two independent interval loops:

  1. Stale-game reaper — deletes games that never got a second player within a configurable threshold.
  2. Result updater — a batch/DB-level companion to the live timeout check in rpc_server: auto-assigns a loss to whichever side's clock ran out on any unfinished, fully-seated game.

Infrastructure dependencies

Three services must be reachable, none are bundled — no docker-compose.yml is provided, so bring your own Postgres/Redis/RabbitMQ (or run them locally):

Service Used for
Postgres game/user persistence (sqlx, migrated via sqlx-cli)
Redis pub/sub only, for live game-update broadcast — no persistence
RabbitMQ internal request/reply bus between api and rpc_server

Configuration

Copy .env-example to .env and fill in the values:

DATABASE_URL=                          # postgres connection string
REDIS_URL=                             # redis connection string
RABBITMQ_URL=                          # amqp connection string
RUST_LOG=debug

API_PORT=3000
API_JWT_ACCESS_SECRET=                 # HS256 secret, access token (15 min)
API_JWT_REFRESH_SECRET=                # HS256 secret, refresh token (7 days, cookie)
API_JWT_SHARE_SECRET=                  # HS256 secret, game share-link token (15 min)
API_SIDE_TIME_LIMIT=30000              # per-side chess clock, in seconds

WORKER_STALE_GAMES_INTERVAL_SECS=60       # how often the reaper loop runs
WORKER_RESULTS_INTERVAL_SECS=600          # how often the timeout-sweep loop runs
WORKER_STALE_GAMES_THRESHOLD_SECS=900     # age before an unseated game is deleted

Getting started

Requires Rust, just, and sqlx-cli (cargo install sqlx-cli), plus a running Postgres/Redis/RabbitMQ pointed to by .env.

just migrate      # run pending sqlx migrations

just api          # terminal 1 — HTTP/WebSocket gateway on API_PORT
just rpc          # terminal 2 — business-logic RPC consumer
just worker       # terminal 3 — background reaper

Other useful recipes (see justfile):

just watch-api / watch-rpc / watch-worker   # cargo-watch variants of the above
just build / just build-release             # compile the workspace
just test                                   # cargo test
just check / just clippy / just fmt         # check / lint / format
just migrate-revert                         # revert the last migration
just migration-new <name>                   # scaffold a new migration

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages