A 2D cooperative multiplayer browser-based RPG built with pixel art and tile-based rendering. Features deep NPC systems, crafting, and real-time combat in a persistent world.
- Client: Phaser 3 (rendering and input)
- Server: Colyseus (multiplayer game server)
- API: Express (authentication and REST endpoints)
- Database: PostgreSQL (persistent storage)
- Language: TypeScript (strict mode, full ESM)
- Build Tools: pnpm workspaces (monorepo), Vite (client bundler)
- Infrastructure: Docker (containerized Postgres + optional server)
- Node.js 20 LTS (Download)
- pnpm - Install globally:
npm install -g pnpm - Docker & Docker Compose - For PostgreSQL (Download)
- PostgreSQL client tools (optional) - For manual database management
git clone <repository-url>
cd ruinpnpm installcp .env.example .envEdit .env if needed. The defaults work for local development with Docker.
docker compose up postgres -dThis starts PostgreSQL on port 5432 with user ruin and password ruin.
Option 1: Using Docker Compose PostgreSQL
# You may be prompted for the password: ruin
createdb -h localhost -U ruin ruin
createdb -h localhost -U ruin ruin_testOption 2: Using pgAdmin or another PostgreSQL GUI
Connect to localhost:5432 with user ruin, password ruin, and create two databases:
ruin(development)ruin_test(testing)
Option 3: Using psql
psql -h localhost -U ruin -c "CREATE DATABASE ruin;"
psql -h localhost -U ruin -c "CREATE DATABASE ruin_test;"pnpm db:migrateThis applies all pending migrations to the development database. Migrations run automatically on server startup as well.
pnpm devThis starts:
- Server (Colyseus + Express) on port
2567 - Client (Vite dev server) on port
3009
Navigate to http://localhost:3009 in your browser.
You'll see a green 10x10 tilemap. The connection will fail with "Connection failed — see console for details" because there's no login UI yet. This is expected for Phase 0b.
ruin/
├── packages/
│ ├── shared/ # Shared TypeScript types and constants
│ ├── server/ # Colyseus game server + Express API
│ └── client/ # Phaser 3 browser client
├── docker-compose.yml # Docker services (Postgres + optional server)
├── Dockerfile.server # Multi-stage build for server deployment
└── .env.example # Environment variable template
TypeScript types and constants shared between client and server. No runtime code — only type definitions and constant values. No dependencies.
Example exports:
IPlayer,IWorldSave,INpc(TypeScript interfaces)TICK_RATE,MAX_PARTY_SIZE,TILE_SIZE(constants)
Authoritative game server. All game logic runs here. The server validates inputs, simulates the world, and sends state updates to clients.
Includes:
- Colyseus WorldRoom (multiplayer room logic)
- Express API (authentication, registration)
- PostgreSQL persistence layer
- Database migrations
- Structured logging (Pino)
Phaser 3 browser client. Dumb renderer and input sender. The client displays the game world and sends player inputs to the server, but contains no game logic.
Includes:
- Phaser scenes (BootScene, WorldScene)
- Colyseus client SDK integration
- Network client wrapper
| Command | Description |
|---|---|
pnpm dev |
Start server (port 2567) and client (port 3009) concurrently |
pnpm build |
Build @ruin/shared and @ruin/server (production) |
pnpm test |
Run all tests (requires Postgres with ruin_test database) |
pnpm db:migrate |
Run database migrations on development database |
pnpm lint |
Lint all packages with ESLint |
pnpm format |
Format all files with Prettier |
pnpm -F @ruin/client build |
Build client for production (static files) |
pnpm -F @ruin/server dev |
Start only the server (useful for debugging) |
pnpm -F @ruin/client dev |
Start only the client (useful for debugging) |
Note: pnpm build does NOT build the client. The client is built separately for production deployment using pnpm -F @ruin/client build, which generates static files for hosting.
| Variable | Description | Default |
|---|---|---|
NODE_ENV |
Node environment (development, production) |
development |
PORT |
Server port | 2567 |
DATABASE_URL |
PostgreSQL connection string | postgresql://ruin:ruin@localhost:5432/ruin |
JWT_SECRET |
Secret for signing JWT tokens | change-me-in-production |
LOG_LEVEL |
Logging level (debug, info, warn, error) |
info |
ADMIN_PASSWORD |
Password for Colyseus monitoring dashboard (dev-only) | admin |
JWT_SECRET and ADMIN_PASSWORD before deploying to production!
For local development, run only the PostgreSQL service:
docker compose up postgres -dThen run the server and client directly with pnpm dev.
To test the full containerized deployment:
docker compose upThis starts:
- postgres service on port
5432 - server service on port
2567
The server container:
- Builds the server from source using a multi-stage Dockerfile
- Connects to the
postgresservice - Runs migrations automatically on startup
- Serves the Colyseus game server and Express API
Note: The client is not containerized. For production, build the client (pnpm -F @ruin/client build) and serve the static files from packages/client/dist/ using a CDN, Nginx, or static hosting service.
The server is authoritative — all game logic and simulation run server-side. Clients send inputs (e.g., movement commands), and the server validates, simulates, and broadcasts state updates.
Client responsibilities:
- Render the current game state
- Send input commands to the server
- Interpolate/smooth visual updates
Server responsibilities:
- Validate all client inputs
- Simulate game world (movement, combat, NPCs, etc.)
- Persist game state to database
- Broadcast state updates to clients
Each WorldRoom corresponds to a single persistent world save, not a lobby or match. This distinction is important for future persistence logic:
- A world save is owned by a specific player (the "host")
- The world persists across sessions
- When all players leave, the room is disposed, but the world save remains in the database
- When the host rejoins, a new WorldRoom is created and loads the saved world state
The @ruin/shared package contains no runtime code — only TypeScript type definitions and constants. This keeps the shared package lightweight and ensures types are consistent across client and server.
The database includes tables for future systems (NPCs, game events, crafting recipes) even though they're currently unused. This is intentional — the schema is designed for the full game architecture, not just Phase 0.
Current tables:
accounts— User accounts (email, password hash)world_saves— Persistent world instancescharacters— Player characters (linked to accounts)npcs— Non-player characters (future use)game_events— World events and quests (future use)
- PostgreSQL must be running
- Test database
ruin_testmust exist (see Getting Started)
pnpm testExpected output:
Test Files 2 passed (2)
Tests 11 passed (11)
Test breakdown:
auth.test.ts— 7 integration tests for authentication (register, login, validation)schema.test.ts— 4 unit tests for Colyseus state schemas
If tests fail with Cannot find module 'bcrypt_lib.node', rebuild the bcrypt native module:
pnpm rebuild bcryptEnsure PostgreSQL is running and the ruin_test database exists:
docker compose up postgres -d
createdb -h localhost -U ruin ruin_test- Start Docker Compose PostgreSQL:
docker compose up postgres -d - Start dev servers:
pnpm dev - Make changes to code (client or server)
- Server auto-restarts on changes (via
tsx watch) - Client hot-reloads on changes (via Vite HMR)
- Run tests:
pnpm test - Lint and format:
pnpm lint && pnpm format
- Create a new
.sqlfile inpackages/server/src/db/migrations/ - Name it with an incrementing number (e.g.,
002_add_items_table.sql) - Add
-- UPand-- DOWNsections:
-- UP
CREATE TABLE items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
...
);
-- DOWN
DROP TABLE items;- Run migrations:
pnpm db:migrate
- Build the Docker image:
docker build -f Dockerfile.server -t ruin-server .- Run the container with environment variables:
docker run -p 2567:2567 \
-e DATABASE_URL="postgresql://user:pass@host:5432/ruin" \
-e JWT_SECRET="your-secure-secret" \
-e NODE_ENV="production" \
ruin-serverOr use docker compose up for full stack deployment.
- Build the client:
pnpm -F @ruin/client build- Deploy static files from
packages/client/dist/to:- CDN (Cloudflare, AWS CloudFront)
- Static hosting (Vercel, Netlify, GitHub Pages)
- Nginx or Apache
Example Nginx config:
server {
listen 80;
server_name yourdomain.com;
root /var/www/ruin/client/dist;
location / {
try_files $uri $uri/ /index.html;
}
}If you have PostgreSQL already installed locally, Docker may fail to bind to port 5432.
Symptoms:
docker compose up postgresfails with port binding errorcreatedbconnects to wrong PostgreSQL instance
Solution:
-
Edit
docker-compose.ymlto remap the port:postgres: ports: - "5433:5432" # Change from 5432:5432
-
Update
DATABASE_URLin.env:DATABASE_URL=postgresql://ruin:ruin@localhost:5433/ruin -
Recreate databases using the new port:
createdb -h localhost -p 5433 -U ruin ruin createdb -h localhost -p 5433 -U ruin ruin_test
If tests or the server fail with Cannot find module 'bcrypt_lib.node' or similar errors:
Solution:
pnpm rebuild bcryptDocker/Alpine Linux:
If building in a Docker container based on Alpine Linux, ensure native build tools are installed before pnpm install:
RUN apk add --no-cache python3 make g++
RUN pnpm install --frozen-lockfile
RUN pnpm rebuild bcryptThis project is in early development. Contributions are welcome once core systems stabilize.
[Your license here]