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.
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)
apinever touches game/auth business logic directly. It's a thin, stateless Axum HTTP + WebSocket gateway: it validates JWTs, forwards every game/auth command torpc_serverover RabbitMQ, and relays live game updates from Redis pub/sub to WebSocket clients.rpc_serverowns 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.workeris 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).
.
├── 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)
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 underengine/attacks/; game state and rules live underengine/game/.db— Postgres access viasqlx.connect.rsexposes theDBpool type;queries/holds raw SQL (games,users);dto/holds the API-facing response types converted from query rows. Notable:update_gameuses an optimistic-concurrency check (old_fen) so concurrent writes to the same game fail as a conflict rather than clobbering each other;update_resultsauto-loses a player whose clock ran out, in bulk.message_client— a thin Redis pub/sub wrapper. Games are broadcast on agame_channel_{game_id}topic; the API's WebSocket handler subscribes per game.rpc— a custom request/reply protocol over RabbitMQ (lapin). Defines theAuthCommand/GameCommand/RpcCommandmessage types,RpcError/RpcErrorKind(mapped to HTTP status codes inapi), anRpcClient(used byapi) that publishes to a well-known queue with a reply-to + correlation ID and a 10s timeout, and anRpcServer(used byrpc_server) that consumes and dispatches those commands.
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.
- Auth —
POST /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 anhttpOnly,Secure,SameSite=Strictcookie;/refreshrotates both from that cookie. - Game —
POST /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, andGET /socket(WebSocket). - WebSocket (
router/game/socket.rs) — clients send{"event": "subscribe" | "unsubscribe", "game_id": ...}and receiveGameResponseJSON pushed wheneverrpc_serverupdates 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.
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 throughchess_core::engine::Game, recomputes FEN/result, and persists with the optimistic-concurrency check), and acheck-resultendpoint for polling a timeout loss without making a move. Every mutation is followed by a broadcast of the newGameResponseover Redis so the API's WebSocket clients stay live.
Background maintenance daemon (binary: worker), talking to Postgres only — no MQ
dependency. Runs two independent interval loops:
- Stale-game reaper — deletes games that never got a second player within a configurable threshold.
- 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.
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 |
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
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 reaperOther 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