HuntPageComponent never called WorldStore.load(), so opening /hunt directly (bookmark/hard refresh) without first visiting /world left currentLocation() at null forever, stranding the page and the context panel on their empty states with no recovery. Add ngOnInit that calls worldStore.load() only when no location is present yet, mirroring WorldPageComponent's existing call and avoiding a duplicate request.
Ashen Realms
A dark-fantasy browser RPG built as an npm-workspace modular monolith:
apps/web— Angular 22 single-page applicationapps/api— NestJS 11 + TypeORM + PostgreSQL APIpackages/*— reserved shared boundaries (currently unused)
This README documents the first visible vertical slice: a server-authoritative world/travel loop between two locations (Südtor von Graufurt and Verbrannte Straße) for one demo character, with no login required.
Prerequisites
-
Node.js and npm (workspace-aware npm, matching the versions used by CI/your toolchain).
-
A reachable PostgreSQL server (PostgreSQL 13 or newer — the schema uses the built-in
gen_random_uuid(), which needs no extensions) with a database namedashen_realms. Create it once, e.g.:psql -h localhost -U postgres -c "CREATE DATABASE ashen_realms;" psql -h localhost -U postgres -c "CREATE USER ashen WITH PASSWORD 'ashen';" psql -h localhost -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE ashen_realms TO ashen;"Adjust user/password/host to match your own local PostgreSQL instance; just keep
DATABASE_URL(below) pointed at it.
Local setup
Run from the repository root:
npm install
Copy-Item .env.example .env
npm run db:migrate
npm run db:seed
npm run dev:api
npm run dev:web
Run dev:api and dev:web in separate terminals — both watch and keep
running. Once both are up:
- API:
http://localhost:3000/api/...(see routes below) - Web:
http://localhost:4200, which proxies/api/*requests tohttp://localhost:3000in development (seeapps/web/proxy.conf.json)
Open http://localhost:4200/world to see Aric Duskwalker at the Südtor von
Graufurt and travel to the Verbrannte Straße and back.
Configuration (.env)
.env.example (repo root) documents every variable. Copy it to .env at the
repo root — .env is git-ignored and must never be committed. The important
one for local setup is:
DATABASE_URL=postgresql://ashen:ashen@localhost:5432/ashen_realms
This is the default local PostgreSQL connection string: database name
ashen_realms, default port 5432. Edit it if your local PostgreSQL uses a
different user, password, host, or port. All API workspace scripts
(db:migrate, db:seed, dev:api, and apps/api's own test:e2e) resolve
this .env from the repository root regardless of which workspace directory
npm runs the underlying command in.
synchronize is always false; schema changes only happen through the
checked-in migration at
apps/api/src/database/migrations/1787072400000-CreateVisibleVerticalSlice.ts.
The seed (apps/api/src/database/seeds/vertical-slice.seed.ts) is idempotent:
running npm run db:seed multiple times upserts the same two locations, two
connections, and one demo character without creating duplicates or resetting
the character's current (live) location.
Ports
| Service | Port | Notes |
|---|---|---|
| API (NestJS) | 3000 |
All routes are under /api (e.g. /api/health). |
| Web (Angular dev server) | 4200 |
Proxies /api/* to the API in development. |
Available routes (first slice)
GET /api/health
GET /api/characters/me
GET /api/world/current-location
GET /api/travel/current
POST /api/travel { "targetLocationId": "<uuid>" }
POST /api/travel accepts exactly targetLocationId; the global validation
pipe rejects any other property (e.g. a client-supplied arrivesAt) with
400 Bad Request, since arrivesAt is always server-derived.
Assets
apps/web/public/images holds only the derivative image files the Angular
build actually serves (resized runtime/*-128.png HUD icons, runtime/*-960.jpg
and runtime/*-1440.jpg background variants, and the small set of PNG
fallbacks referenced directly by source/styles). Everything under apps/web/public
is copied verbatim into the browser build, so keep that folder limited to
files a component, template, or stylesheet actually references.
Original/unresized source art (enemy, NPC, and combat-status artwork, HUD icon
originals, difficulty badges, etc.) that isn't loaded by the app lives in
art/ at the repository root instead, mirroring the same subfolder layout
(e.g. art/enemies, art/npc, art/hud). It is not part of any build output.
Scripts
Run these from the repository root unless noted otherwise.
| Script | Description |
|---|---|
npm run dev:web |
Start the Angular dev server (port 4200). |
npm run dev:api |
Start the NestJS API in watch mode (port 3000). |
npm run build:web |
Production build of the Angular app. |
npm run build:api |
Production build of the NestJS app. |
npm run build |
Both builds. |
npm test |
Unit tests for every workspace. |
npm run test:e2e |
API end-to-end/smoke tests (see below). |
npm run db:migrate |
Run pending TypeORM migrations against DATABASE_URL. |
npm run db:revert |
Revert the last migration. |
npm run db:seed |
Run the idempotent demo-content seed. |
Testing
Unit tests never require a database:
npm test --workspace=@ashen-realms/api -- --runInBand
The API end-to-end smoke test (apps/api/test/visible-slice.e2e-spec.ts)
always asserts GET /api/health. When DATABASE_URL is set and reachable,
it additionally boots the real AppModule (real database, entities, and
controllers) and asserts the seeded character/world responses and the
arrivesAt validation rejection. Those database-backed assertions are
skipped — not failed — when no database is configured, so npm run test:e2e
is safe to run without any local PostgreSQL setup too:
npm run test:e2e --workspace=@ashen-realms/api -- --runInBand
Both builds:
npm run build:api
npm run build:web
Known limitations of this first vertical slice
This slice deliberately excludes (per
docs/superpowers/specs/2026-08-18-first-visible-vertical-slice-design.md):
- Authentication, user accounts, JWTs, or any login/registration flow — there is exactly one hardcoded demo character (Aric Duskwalker).
- Hunting, combat, loot, inventory, equipment, quests, merchants, and currencies.
- Realtime features: WebSockets, chat, guilds, CMS, object storage, or a multi-service/microservice architecture.
- Ambush resolution:
ambushChanceis stored on each connection and surfaced only as a coarseLOW/HIGHdanger rating; it is never rolled. - Production containerization or NestJS static hosting.
- Client-side clock skew can shift how the travel countdown displays, but
arrival is always confirmed by the server (
GET /api/travel/current/GET /api/world/current-location), never inferred locally.
Only two locations and one directed pair of connections exist
(south-gate ⇄ burned-road), each with a fixed 10-second travel duration.