Chickens Must Die.Technical docs / Infrastructure

Authoritative Multiplayer Server Starter

Infrastructure & Operations

How the services fit together: Compose topology, validated configuration, PostgreSQL, Redis, RabbitMQ, HTTP, logging, map compilation, and the boundaries to consider before production.

Environment topology

Docker Compose runs the following services:

Service Role Development access
map-compiler Compiles Tiled JSON into the server map One-shot container
game-server Colyseus + Express on a single port Internal port 2567
postgres Persistent application data 127.0.0.1:5432
redis Rate limiting, Presence, and Driver 127.0.0.1:6379
rabbitmq Integration events AMQP 5672, management UI 15672
nginx Public HTTP/WebSocket reverse proxy Default port 8080

Nginx preserves proxy headers, supports WebSocket upgrades, disables buffering, and applies long timeouts for persistent game connections.

Configuration

Configuration is validated with Zod as the application is assembled. A missing required URL or invalid relationship between values stops startup with a clear error.

General & gameplay

Variable Default Purpose
NODE_ENV development development, test, production
LOG_LEVEL info Pino log level
MOVEMENT_MODE free free or grid
GRID_SIZE 16 16, 32, or 64
NGINX_PORT 8080 Public proxy port

PostgreSQL

Variable Default Purpose
DATABASE_URL Required postgres: / postgresql: URL
POSTGRES_POOL_MIN 0 Minimum clients in the pool
POSTGRES_POOL_MAX 20 Maximum clients in the pool
POSTGRES_IDLE_TIMEOUT_MS 30000 Client idle timeout
POSTGRES_CONNECTION_TIMEOUT_MS 3000 Connection acquisition timeout
POSTGRES_QUERY_TIMEOUT_MS 10000 Client-side query timeout
POSTGRES_STATEMENT_TIMEOUT_MS 10000 Statement timeout
POSTGRES_MAX_LIFETIME_SECONDS 3600 Maximum client lifetime
POSTGRES_STARTUP_MAX_ATTEMPTS 10 Startup attempts
POSTGRES_STARTUP_RETRY_DELAY_MS 1000 Delay between startup attempts
POSTGRES_RECONNECT_INITIAL_DELAY_MS 1000 Initial reconnect backoff
POSTGRES_RECONNECT_MAX_DELAY_MS 30000 Maximum reconnect backoff
POSTGRES_SSL false TLS with certificate verification

PostgresDataSource is a process-wide singleton backed by one pg pool. It exposes generic queries and transactions. Operation logs include the operation name, duration, row count, and pool statistics, but not query parameters.

A network failure triggers reconnection with exponential backoff. Failed write queries are not retried automatically because their outcome may be ambiguous. Domain queries belong in separate data-access/<feature> classes, not in the datasource.

Redis

Variable Default Purpose
REDIS_URL Required redis: or rediss: URL
REDIS_CONNECT_TIMEOUT_MS 3000 Connection timeout
REDIS_COMMAND_TIMEOUT_MS 2000 Command timeout
REDIS_MAX_RETRIES_PER_REQUEST 2 Attempts for one command
REDIS_STARTUP_MAX_ATTEMPTS 10 Startup attempts
REDIS_STARTUP_RETRY_DELAY_MS 1000 Delay between startup attempts
REDIS_RECONNECT_INITIAL_DELAY_MS 500 Initial reconnect backoff
REDIS_RECONNECT_MAX_DELAY_MS 10000 Maximum reconnect backoff

There are three logical connections:

  • Application RedisDataSource for commands and rate limiting.
  • Colyseus Presence client.
  • Colyseus Driver client.

This separation prevents application-level command load from interfering with Colyseus communication. The application client uses lazy connect, a ready check, bounded retries, an offline queue, and reconnect logic. Presence and Driver are provided directly to defineServer().

RabbitMQ

Variable Default Purpose
RABBITMQ_URL Required amqp: / amqps: URL
RABBITMQ_EXCHANGE game.events Durable topic exchange
RABBITMQ_QUEUE game.events Durable queue bound with #
RABBITMQ_CONNECT_TIMEOUT_MS 5000 Connection timeout
RABBITMQ_HEARTBEAT_SECONDS 30 AMQP heartbeat
RABBITMQ_STARTUP_MAX_ATTEMPTS 10 Startup attempts
RABBITMQ_STARTUP_RETRY_DELAY_MS 1000 Delay between startup attempts
RABBITMQ_RECONNECT_INITIAL_DELAY_MS 1000 Initial reconnect backoff
RABBITMQ_RECONNECT_MAX_DELAY_MS 30000 Maximum reconnect backoff
RABBITMQ_PUBLISH_CONFIRM_TIMEOUT_MS 5000 Publisher-confirm timeout
RABBITMQ_MAX_EVENT_BYTES 262144 Maximum JSON event size

RabbitModule exposes only EventPublisher to higher layers. Each event contains a UUID, a type used as its routing key, an ISO timestamp, a version, and a JSON payload.

Messages are persistent and mandatory, and the broker confirms them through a confirm channel. An unroutable message is treated as an error.

The connection recovers automatically after a failure. Publishing may wait for reconnection, but has one shared timeout. If it times out, the publish outcome is marked as unknown; the library does not automatically resend the event.

Example:

await eventPublisher.publish("player.created", {
  playerId: "player-1",
});

HTTP & health

Express is attached through the express callback in defineServer().

GET /api/health

The endpoint checks these dependencies in parallel:

  • Map readiness in GameRuntime.
  • SELECT 1 in PostgreSQL.
  • PING in Redis.
  • RabbitMQ connection state.

Example of a successful response:

{
  "status": "ok",
  "ready": true,
  "services": {
    "gameRuntime": { "ready": true },
    "postgres": { "ready": true },
    "redis": { "ready": true },
    "rabbitMq": { "ready": true }
  }
}

HTTP 200 means everything is ready; 503 means at least one dependency is unavailable. The health endpoint is excluded from request logging and rate limiting.

/monitor and Playground are available only outside production.

Rate limiting

Boundary Default limit Implementation
HTTP 60 / 60 s Atomic Redis counter per IP
Room auth 10 / 60 s Atomic Redis counter per IP
Movement 90 / 1 s In-memory sliding window per client
Chat 5 / 10 s + 750 ms cooldown In-memory sliding window per client

The identity used in Redis keys is hashed with SHA-256. HTTP replies include RateLimit-Limit and RateLimit-Remaining; blocked requests also include Retry-After.

If Redis fails, limits fail closed: HTTP returns 503, and room auth rejects the connection as rate_limited. Movement and chat limits do not require a Redis round trip.

Proxy configuration: RATE_LIMIT_TRUST_PROXY_HOPS must match the actual number of trusted proxies. A wrong value may cause the limiter to identify the wrong IP address.

Logging

AppLogger wraps Pino and creates child loggers with a component field. Logs use JSON, ISO timestamps, and include the service name and environment.

The HTTP middleware:

  • Accepts or generates x-request-id.
  • Returns the ID in the response header.
  • Logs method, path, URL, IP, status, and duration.
  • Chooses info, warn, or error based on response status.
HTTP payloads, passwords, and SQL parameters are not logged by the middleware.

Map compiler

Source: map-compiler/tiled-source/map.json.

Output: server/src/shared/map/map.server.json, or the /maps volume in Compose.

The compiler requires:

  • An orthogonal, non-infinite Tiled map.
  • Positive map dimensions and tile size.
  • Exactly one object layer named Collision.
  • No layer offset.
  • Only unrotated rectangles.
  • Objects fully contained within the map.

In free mode it preserves geometry. In grid mode it snaps collision edges to the grid and requires world dimensions to be divisible by GRID_SIZE.

The server validates the compiled result again with Zod, caps the collision count at 100,000, and memoizes asynchronous loading. The pipeline does not read a map when a module is imported.

Persistent data & migrations

PostgreSQL is connected, but the current gameplay does not yet persist players or rooms. The pg/migrations/init.sql file is an initial schema draft; there is no versioned migration runner yet.

Before production deployment: Choose a deliberate migration strategy and verify the PostgreSQL initialization mount in Compose.

Known limitations before production

The infrastructure provides a working foundation, but the following items are not yet complete or need deployment-specific attention:

  • No user/token authentication — current auth is limited to room nicknames.
  • No PostgreSQL domain-level persistence.
  • No RabbitMQ consumer or dead-letter topology.
  • No transactional outbox between PostgreSQL and RabbitMQ.
  • Realtime rate limiters are local to each instance.
  • The player-eating algorithm has quadratic complexity in player count.
  • EventBus and ShootHandler are inactive.
  • Playground and monitor must remain disabled in production.
  • Remove temporary configuration debug logs, and never log URLs containing passwords.
Scope reminder: Prepared ports and adapters are extension points, not evidence that domain repositories, consumers, or a complete persistence workflow are already implemented.

↑ Back to top