openvibers/openvibe.games Commits
AGENTS.md: the context an agent starts from (#379)
Changes
nnew file mode 100644@@@ -0,0 +1,169 @@++# AGENTS.md — OpenVibe.Games++++## Project Overview++++OpenVibe.Games hosts the browser games at `openvibe.games` and `play.openvibe.games`: **Scraplandia** (a persistent++multiplayer physics sandbox with the scored "Haul" mode), **Salvage Rush** (on-foot timed collection),++**creator worlds** (map editor + `/worlds`, sandboxed), the casual line (plan T22, starting with **Vibe Heroes**),++server-run AI draft matches/Bench scoring, and the moved **OpenVibe.Play player pages** (`/achievements`,++`/leaderboards`, `/developers`) which read Play's public API server-side.++++- **Domain:** `openvibe.games`, `play.openvibe.games` · **Port:** 8000 (server; Vite dev client 5173) ·++ **Service id:** `games` · **Env prefix:** `GAMES`/`OV_`++- **Stack:** TypeScript everywhere, pnpm 10 monorepo, Babylon.js (WebGPU/WebGL) client, Rapier++ `@dimforge/rapier3d-deterministic-compat` (one pinned build headless on server and in client prediction), WS++ protocol, PostgreSQL 18 + Valkey through `openvibe-sdk`, QuickJS for script mods.++- **Read first:** [README.md](README.md), [ARCHITECTURE.md](ARCHITECTURE.md), [ROADMAP.md](ROADMAP.md),++ [docs/games.md](docs/games.md), [docs/casual.md](docs/casual.md), [docs/mods.md](docs/mods.md),++ [docs/instances.md](docs/instances.md), [docs/ai-matches.md](docs/ai-matches.md), [ADR-0007](docs/adr/), [ADR-0013](docs/adr/).++++**Network context:** players are keyed by Network `subject_id` (`usr_…`/`gst_…`); SSO/JWKS/identity/mod grants are++OpenVibe.Network. Events are OpenVibe.Events (transactional outbox). Achievements/scores go to OpenVibe.Play's API;++six built-in achievements grant Live items through OpenVibe.Inventory. Map-editor assets are also copied to++OpenVibe.Media objects. Mod grants' authority is Network (`mods.grant.manage`); Games runs its own bounded++`games-quickjs@1` scripts. The platform boundary is `apps/server/src/platform` and `apps/server/src/mods`; the++simulation packages do not know the platform exists.++++## Commands++++```bash++corepack enable && pnpm install # Node ≥22, pnpm 10.6.5++pnpm dev # server + client dev in parallel++pnpm dev:server # authoritative server on :8000 (tsx watch)++pnpm dev:client # client hot reload on :5173 (proxies /ws to :8000)++pnpm test # all vitest files through test/run.js++pnpm typecheck # strict TypeScript across every package++pnpm lint # ESLint (no-explicit-any)++pnpm format # Prettier write (format:check in CI)++pnpm build # all packages + client bundle++node test/run.js --index # regenerate the flat test/*.test.js pointers after adding/moving a test++tsx apps/server/scripts/sliceTest.ts # end-to-end vertical slice: real server, protocol clients, restart++pnpm test:editor | test:collab | test:console # e2e Playwright checks (editor, collaboration, console)++pnpm audit:editor # editor architecture audit (vitest config)++```++++Production: `pnpm build`, then `DATABASE_URL=… DATABASE_DIRECT_URL=… STATIC_DIR=apps/client/dist PORT=8000 pnpm++--filter @openvibe/server start`. `DATABASE_URL` serves through PgBouncer; migrations run at boot on++`DATABASE_DIRECT_URL` (owner, direct); without both and outside production, PGlite under `DATABASE_DIR`.++++## Architecture at a Glance++++- **Server entry:** `apps/server/src/main.ts`; authoritative simulation, networking, persistence, platform boundary++ ([apps/server/src/platform](apps/server/src/platform)), mods ([apps/server/src/mods](apps/server/src/mods)),++ maps, worlds, casual, AI matches, Steam lists, observability.++- **Client:** [apps/client/](apps/client/) — rendering, prediction, input, HUD, the map editor (`src/editor/`), e2e++ scripts; Babylon is imported **only** here.++- **Packages:** [packages/shared/](packages/shared/) (math, ids, events, logging, fixed timestep),++ [packages/protocol/](packages/protocol/) (versioned wire messages, zod-validated inbound),++ [packages/content/](packages/content/) (data-driven items/recipes/worlds + validation registry),++ [packages/gameplay/](packages/gameplay/) (pure inventory/crafting/movement/zones/entities),++ [packages/physics/](packages/physics/) (abstraction + Rapier adapter),++ [packages/persistence/](packages/persistence/) (DTOs, repositories, PostgreSQL, migrations),++ [packages/casual/](packages/casual/) (casual rules/auto-battle/Vibe Heroes, engine-free).++- **Dependency graph (ARCHITECTURE.md):** shared → protocol/content/physics → gameplay → persistence → apps; only++ `apps/server` and `packages/persistence` know PostgreSQL; `packages/*` never import browser-only APIs.++- **Simulation:** 30 Hz fixed tick, 15 Hz snapshots; kinematic Source/Quake controller, client prediction + rewind +++ interpolation; physics sleep is first-class (settled props cost nothing on the wire).++- **Data:** [packages/persistence/migrations/](packages/persistence/migrations/) (23 migrations: characters, world,++ props, instances/leases, progress, hauls, inventory rewards, casual, Play sync, telemetry, creator worlds/scripts/++ teams/assets, surf, AI matches, sharing, outbox). A write-behind flusher means the tick never awaits I/O; SIGTERM++ drains it.++- **Pages:** `/` (portal; on play.openvibe.games `/` is the game), `/play`, `/editor`, `/worlds`,++ `/achievements`, `/leaderboards`, `/developers`, `/play/vibe-heroes`, `/healthz`, `/api/ready`, `/metrics`.++- **Deploy:** [deploy/openvibe-games.service](deploy/openvibe-games.service) (PORT 8000, HOST 127.0.0.1,++ `/etc/openvibe/games.env`, `GAMES_MIGRATIONS_DIR`, `MAP_PATH`, `STATIC_DIR`, `TRUST_PROXY=127.0.0.1`,++ `TimeoutStopSec=30`), [deploy/nginx-openvibe-games.conf](deploy/nginx-openvibe-games.conf),++ [deploy/scripts/deploy.sh](deploy/scripts/deploy.sh) wrapping `ovhost deploy games` (strategy `pnpm-build`:++ `pnpm install --frozen-lockfile`, `pnpm build`, restore tracked `dist-types/`, restart). `--wait-idle` holds the++ restart until nobody plays; `DRY_RUN=1` prints `ovhost plan games`; rollback `sudo deploy/scripts/deploy.sh++ --rollback` (rebuilds the previous commit).++++## Ownership and Boundaries++++Games owns the authoritative simulation (world, characters, inventories, skills, blueprints, props), the++write-behind persistence schema, the map editor and its assets, the mod registry/content/script sandbox and its audit,++the `games.*` events, the WS protocol and the portal/play/editor pages, the casual kit and its server economy, creator++worlds, AI draft matches, and the moved Play player pages. It does **not** own identity (Network subjects), files++beyond the local copy (Media mirrors), event delivery (Events), Play's service/API (Play), Inventory items (Inventory),++or general-purpose executable mods (Host Stage C; Games' QuickJS scripts are bounded and approved per install).++++## Platform Integration (the boundary)++++- **Identity:** a signed-in player is the token's `subject_id`; a token without one cannot name an account, and a++ guest token that looks like `usr_…` is refused. Never key a row on a display name.++- **Service principal:** calls to Events/Media/Network identity/Inventory/Play carry a client-credentials token for++ the `games` client, one per audience. No shared keys. SSO calls for a player use the player's own token.++- **Events:** `games.player.joined|left`, `games.skill.leveled`, `games.blueprint.unlocked`, `games.world.saved`++ (≤1 per `WORLD_SAVED_EVENT_MINUTES`, plus shutdown), `games.mod.installed|enabled|disabled|grants_changed|revoked`.++ Achievements are **not** events: an unlock is stored in Games and sent to Play; the six inventory-bearing++ achievements grant through Inventory and retry on failure.++- **Play:** `GAMES_PLAY_URL` (unset = scores/unlocks stay local); Play is the network-wide read surface.++- **Media:** only map-editor assets (textures, paint masks, glb) are uploaded; a restart-safe `media_mirrors` queue++ copies them to the `games` tenant. There is no screenshot/blueprint upload.++- **Mods:** manifests follow `[email protected]` validated against the installed Contracts (pin drift is a++ defect); `games-content@2` packs merge definitions at boot, the client sends its definition digest in `hello` and the++ server rejects a mismatch; each install stores the approved subset of requested capabilities, every runtime binding++ checks it, revocation stops affecting the world on the next tick, and install/grant/use/deny/revoke are audited.++ `games-quickjs@1` runs only with `GAMES_SCRIPT_MODS=1` in a per-mod QuickJS sandbox (defaults `cpuMs` 2/tick,++ `memoryMb` 8, `stackKb` 128; over budget disables the mod for the instance).++- **Frame:** the portal/play/editor load the OpenVibe Frame from this server's own `/shared/` copy, so it keeps its++ frame while Network is down. `GET /api/v1/platform` reports events/Media-mirror state and backlog without secrets.++++## Capabilities and Grants++++Implemented: `games.mod.manage`, `games.mod.read` (mods API), `games.world.announce`, `games.prop.place`,++`games.def.define` (declarative packs), and for script packs `games.script.run`, `games.players.read`,++`games.player.message`, `games.world.emit`, `games.storage.kv`. Script bindings are absent unless approved for that++install. The `games` principal needs `inventory.item.grant`, `events.event.publish|subscription.manage`,++`media.object.upload` (namespace `games`), `identity.subject.resolve`, `network.modules.write` on++`games.progress.summary`, and Network's `mods.grant.manage`.++++## Conventions and Pitfalls++++- **The server is the game.** Every gameplay-relevant decision is server-simulated/validated; no client field is++ trusted; inbound messages are zod-validated and flood-controlled.++- **Do not import Babylon into `apps/server`** (the architecture audit fails) or platform modules into `packages/*`;++ keep gameplay engine-free and deterministic (same Rapier build on both sides).++- **Data-driven content:** add definitions in `@openvibe/content`, validate at startup; stable ids, display names are++ cosmetic. A new built-in pack must not bypass the capability check.++- **Migrations** are `packages/persistence/migrations/NNNN_*.sql`, applied at boot with the owner URL; never edit one++ that has run. The tick never awaits I/O — write-behind, then drain on SIGTERM (30 s stop timeout).++- **Instances/leases:** PostgreSQL records each room and a renewable fenced lease; `GAMES_INSTANCES_PER_PLACE` only++ within one process, `GAMES_CREATOR_WORLDS_MAX` caps creator worlds, empty ones stop after the idle timeout. A lease++ that is lost must stop the place (old failure: five places lost leases in one night).++- **`TRUST_PROXY`:** unset means the TCP peer is the client and all forwarded headers are ignored (otherwise a caller++ mints fresh rate-limit buckets); behind nginx it is `127.0.0.1`, and nginx overwrites the headers from++ `$remote_addr`. Never read `X-Forwarded-For` directly.++- **Static serving:** an unknown path is a 404 (HTML page, `{"error":"not_found"}` under `/api`, plain text for a++ missing asset), never the portal with 200. `/api/ready` is 200 only while the database answers a real round trip.++- **Test pointers:** after adding/moving a `*.test.ts`, run `node test/run.js --index`; a missing/stale flat pointer++ fails the suite. Vitest reads the real colocated file.++- **Build:** pnpm's `onlyBuiltDependencies` is `esbuild`; Corepack downloads pnpm with Node's fetch in CI (the image++ bakes it). `pnpm install --frozen-lockfile` and a clean `pnpm build` are the deploy gate.++- **nginx** keeps `/api/ready` off the public vhosts; `/metrics` answers loopback only. Secrets++ (`OV_OAUTH_CLIENT_SECRET`, `GAMES_EVENTS_SECRET`, `OV_NETWORK_AUTH_URL`) live in `/etc/openvibe/games.env` by name++ only.++- **Play's old pages 301 here:** do not re-add `apps/server` player pages under play.openvibe.network; the service and++ API stay there.++- Phone first for casual/portal pages; no Font Awesome; "Calm reduces, only Off removes".++++## Testing++++`pnpm test` runs every vitest file through [test/run.js](test/run.js) (package and server units incl.++`apps/server/src/game/platformIntegration.test.ts`, which boots the real server twice on one database, and the++architecture audit). `pnpm typecheck`/`pnpm lint` keep strict TS and no `any`; `tsx apps/server/scripts/sliceTest.ts`++drives protocol clients across a restart; `pnpm test:editor|test:collab|test:console` are Playwright e2e checks. Run++the narrowest file first, then the suite.++++## Documentation++++- [README.md](README.md) — purpose, owns, does-not-own, depends on, layout, development, commands, production,++ platform integration, capabilities, tests, security.++- [ARCHITECTURE.md](ARCHITECTURE.md) — principles, package graph, simulation, networking, server systems, persistence,++ platform boundary, zones, entities, observability, security posture.++- [ROADMAP.md](ROADMAP.md), [docs/games.md](docs/games.md), [docs/casual.md](docs/casual.md),++ [docs/MAP_EDITOR.md](docs/MAP_EDITOR.md), [docs/EDITOR_COMPLETION_STATUS.md](docs/EDITOR_COMPLETION_STATUS.md),++ [docs/instances.md](docs/instances.md), [docs/mods.md](docs/mods.md), [docs/ai-matches.md](docs/ai-matches.md),++ [docs/platform-direction.md](docs/platform-direction.md), [docs/adr/](docs/adr/),++ [docs/capabilities-proposal/](docs/capabilities-proposal/), [docs/service-manifest-proposal.json](docs/service-manifest-proposal.json).++- [SECURITY.md](SECURITY.md), [.env.example](.env.example), [CHANGELOG.md](CHANGELOG.md).nnew file mode 100644@@@ -0,0 +1 @@
new file mode 100644
new file mode 100644
@@ -0,0 +1,169 @@
@@ -0,0 +1,169 @@
+# AGENTS.md — OpenVibe.Games
+
+## Project Overview
+
+OpenVibe.Games hosts the browser games at `openvibe.games` and `play.openvibe.games`: **Scraplandia** (a persistent
+multiplayer physics sandbox with the scored "Haul" mode), **Salvage Rush** (on-foot timed collection),
+**creator worlds** (map editor + `/worlds`, sandboxed), the casual line (plan T22, starting with **Vibe Heroes**),
+server-run AI draft matches/Bench scoring, and the moved **OpenVibe.Play player pages** (`/achievements`,
+`/leaderboards`, `/developers`) which read Play's public API server-side.
+
+- **Domain:** `openvibe.games`, `play.openvibe.games` · **Port:** 8000 (server; Vite dev client 5173) ·
+ **Service id:** `games` · **Env prefix:** `GAMES`/`OV_`
+- **Stack:** TypeScript everywhere, pnpm 10 monorepo, Babylon.js (WebGPU/WebGL) client, Rapier
+ `@dimforge/rapier3d-deterministic-compat` (one pinned build headless on server and in client prediction), WS
+ protocol, PostgreSQL 18 + Valkey through `openvibe-sdk`, QuickJS for script mods.
+- **Read first:** [README.md](README.md), [ARCHITECTURE.md](ARCHITECTURE.md), [ROADMAP.md](ROADMAP.md),
+ [docs/games.md](docs/games.md), [docs/casual.md](docs/casual.md), [docs/mods.md](docs/mods.md),
+ [docs/instances.md](docs/instances.md), [docs/ai-matches.md](docs/ai-matches.md), [ADR-0007](docs/adr/), [ADR-0013](docs/adr/).
+
+**Network context:** players are keyed by Network `subject_id` (`usr_…`/`gst_…`); SSO/JWKS/identity/mod grants are
+OpenVibe.Network. Events are OpenVibe.Events (transactional outbox). Achievements/scores go to OpenVibe.Play's API;
+six built-in achievements grant Live items through OpenVibe.Inventory. Map-editor assets are also copied to
+OpenVibe.Media objects. Mod grants' authority is Network (`mods.grant.manage`); Games runs its own bounded
+`games-quickjs@1` scripts. The platform boundary is `apps/server/src/platform` and `apps/server/src/mods`; the
+simulation packages do not know the platform exists.
+
+## Commands
+
+```bash
+corepack enable && pnpm install # Node ≥22, pnpm 10.6.5
+pnpm dev # server + client dev in parallel
+pnpm dev:server # authoritative server on :8000 (tsx watch)
+pnpm dev:client # client hot reload on :5173 (proxies /ws to :8000)
+pnpm test # all vitest files through test/run.js
+pnpm typecheck # strict TypeScript across every package
+pnpm lint # ESLint (no-explicit-any)
+pnpm format # Prettier write (format:check in CI)
+pnpm build # all packages + client bundle
+node test/run.js --index # regenerate the flat test/*.test.js pointers after adding/moving a test
+tsx apps/server/scripts/sliceTest.ts # end-to-end vertical slice: real server, protocol clients, restart
+pnpm test:editor | test:collab | test:console # e2e Playwright checks (editor, collaboration, console)
+pnpm audit:editor # editor architecture audit (vitest config)
+```
+
+Production: `pnpm build`, then `DATABASE_URL=… DATABASE_DIRECT_URL=… STATIC_DIR=apps/client/dist PORT=8000 pnpm
+--filter @openvibe/server start`. `DATABASE_URL` serves through PgBouncer; migrations run at boot on
+`DATABASE_DIRECT_URL` (owner, direct); without both and outside production, PGlite under `DATABASE_DIR`.
+
+## Architecture at a Glance
+
+- **Server entry:** `apps/server/src/main.ts`; authoritative simulation, networking, persistence, platform boundary
+ ([apps/server/src/platform](apps/server/src/platform)), mods ([apps/server/src/mods](apps/server/src/mods)),
+ maps, worlds, casual, AI matches, Steam lists, observability.
+- **Client:** [apps/client/](apps/client/) — rendering, prediction, input, HUD, the map editor (`src/editor/`), e2e
+ scripts; Babylon is imported **only** here.
+- **Packages:** [packages/shared/](packages/shared/) (math, ids, events, logging, fixed timestep),
+ [packages/protocol/](packages/protocol/) (versioned wire messages, zod-validated inbound),
+ [packages/content/](packages/content/) (data-driven items/recipes/worlds + validation registry),
+ [packages/gameplay/](packages/gameplay/) (pure inventory/crafting/movement/zones/entities),
+ [packages/physics/](packages/physics/) (abstraction + Rapier adapter),
+ [packages/persistence/](packages/persistence/) (DTOs, repositories, PostgreSQL, migrations),
+ [packages/casual/](packages/casual/) (casual rules/auto-battle/Vibe Heroes, engine-free).
+- **Dependency graph (ARCHITECTURE.md):** shared → protocol/content/physics → gameplay → persistence → apps; only
+ `apps/server` and `packages/persistence` know PostgreSQL; `packages/*` never import browser-only APIs.
+- **Simulation:** 30 Hz fixed tick, 15 Hz snapshots; kinematic Source/Quake controller, client prediction + rewind +
+ interpolation; physics sleep is first-class (settled props cost nothing on the wire).
+- **Data:** [packages/persistence/migrations/](packages/persistence/migrations/) (23 migrations: characters, world,
+ props, instances/leases, progress, hauls, inventory rewards, casual, Play sync, telemetry, creator worlds/scripts/
+ teams/assets, surf, AI matches, sharing, outbox). A write-behind flusher means the tick never awaits I/O; SIGTERM
+ drains it.
+- **Pages:** `/` (portal; on play.openvibe.games `/` is the game), `/play`, `/editor`, `/worlds`,
+ `/achievements`, `/leaderboards`, `/developers`, `/play/vibe-heroes`, `/healthz`, `/api/ready`, `/metrics`.
+- **Deploy:** [deploy/openvibe-games.service](deploy/openvibe-games.service) (PORT 8000, HOST 127.0.0.1,
+ `/etc/openvibe/games.env`, `GAMES_MIGRATIONS_DIR`, `MAP_PATH`, `STATIC_DIR`, `TRUST_PROXY=127.0.0.1`,
+ `TimeoutStopSec=30`), [deploy/nginx-openvibe-games.conf](deploy/nginx-openvibe-games.conf),
+ [deploy/scripts/deploy.sh](deploy/scripts/deploy.sh) wrapping `ovhost deploy games` (strategy `pnpm-build`:
+ `pnpm install --frozen-lockfile`, `pnpm build`, restore tracked `dist-types/`, restart). `--wait-idle` holds the
+ restart until nobody plays; `DRY_RUN=1` prints `ovhost plan games`; rollback `sudo deploy/scripts/deploy.sh
+ --rollback` (rebuilds the previous commit).
+
+## Ownership and Boundaries
+
+Games owns the authoritative simulation (world, characters, inventories, skills, blueprints, props), the
+write-behind persistence schema, the map editor and its assets, the mod registry/content/script sandbox and its audit,
+the `games.*` events, the WS protocol and the portal/play/editor pages, the casual kit and its server economy, creator
+worlds, AI draft matches, and the moved Play player pages. It does **not** own identity (Network subjects), files
+beyond the local copy (Media mirrors), event delivery (Events), Play's service/API (Play), Inventory items (Inventory),
+or general-purpose executable mods (Host Stage C; Games' QuickJS scripts are bounded and approved per install).
+
+## Platform Integration (the boundary)
+
+- **Identity:** a signed-in player is the token's `subject_id`; a token without one cannot name an account, and a
+ guest token that looks like `usr_…` is refused. Never key a row on a display name.
+- **Service principal:** calls to Events/Media/Network identity/Inventory/Play carry a client-credentials token for
+ the `games` client, one per audience. No shared keys. SSO calls for a player use the player's own token.
+- **Events:** `games.player.joined|left`, `games.skill.leveled`, `games.blueprint.unlocked`, `games.world.saved`
+ (≤1 per `WORLD_SAVED_EVENT_MINUTES`, plus shutdown), `games.mod.installed|enabled|disabled|grants_changed|revoked`.
+ Achievements are **not** events: an unlock is stored in Games and sent to Play; the six inventory-bearing
+ achievements grant through Inventory and retry on failure.
+- **Play:** `GAMES_PLAY_URL` (unset = scores/unlocks stay local); Play is the network-wide read surface.
+- **Media:** only map-editor assets (textures, paint masks, glb) are uploaded; a restart-safe `media_mirrors` queue
+ copies them to the `games` tenant. There is no screenshot/blueprint upload.
+- **Mods:** manifests follow `[email protected]` validated against the installed Contracts (pin drift is a
+ defect); `games-content@2` packs merge definitions at boot, the client sends its definition digest in `hello` and the
+ server rejects a mismatch; each install stores the approved subset of requested capabilities, every runtime binding
+ checks it, revocation stops affecting the world on the next tick, and install/grant/use/deny/revoke are audited.
+ `games-quickjs@1` runs only with `GAMES_SCRIPT_MODS=1` in a per-mod QuickJS sandbox (defaults `cpuMs` 2/tick,
+ `memoryMb` 8, `stackKb` 128; over budget disables the mod for the instance).
+- **Frame:** the portal/play/editor load the OpenVibe Frame from this server's own `/shared/` copy, so it keeps its
+ frame while Network is down. `GET /api/v1/platform` reports events/Media-mirror state and backlog without secrets.
+
+## Capabilities and Grants
+
+Implemented: `games.mod.manage`, `games.mod.read` (mods API), `games.world.announce`, `games.prop.place`,
+`games.def.define` (declarative packs), and for script packs `games.script.run`, `games.players.read`,
+`games.player.message`, `games.world.emit`, `games.storage.kv`. Script bindings are absent unless approved for that
+install. The `games` principal needs `inventory.item.grant`, `events.event.publish|subscription.manage`,
+`media.object.upload` (namespace `games`), `identity.subject.resolve`, `network.modules.write` on
+`games.progress.summary`, and Network's `mods.grant.manage`.
+
+## Conventions and Pitfalls
+
+- **The server is the game.** Every gameplay-relevant decision is server-simulated/validated; no client field is
+ trusted; inbound messages are zod-validated and flood-controlled.
+- **Do not import Babylon into `apps/server`** (the architecture audit fails) or platform modules into `packages/*`;
+ keep gameplay engine-free and deterministic (same Rapier build on both sides).
+- **Data-driven content:** add definitions in `@openvibe/content`, validate at startup; stable ids, display names are
+ cosmetic. A new built-in pack must not bypass the capability check.
+- **Migrations** are `packages/persistence/migrations/NNNN_*.sql`, applied at boot with the owner URL; never edit one
+ that has run. The tick never awaits I/O — write-behind, then drain on SIGTERM (30 s stop timeout).
+- **Instances/leases:** PostgreSQL records each room and a renewable fenced lease; `GAMES_INSTANCES_PER_PLACE` only
+ within one process, `GAMES_CREATOR_WORLDS_MAX` caps creator worlds, empty ones stop after the idle timeout. A lease
+ that is lost must stop the place (old failure: five places lost leases in one night).
+- **`TRUST_PROXY`:** unset means the TCP peer is the client and all forwarded headers are ignored (otherwise a caller
+ mints fresh rate-limit buckets); behind nginx it is `127.0.0.1`, and nginx overwrites the headers from
+ `$remote_addr`. Never read `X-Forwarded-For` directly.
+- **Static serving:** an unknown path is a 404 (HTML page, `{"error":"not_found"}` under `/api`, plain text for a
+ missing asset), never the portal with 200. `/api/ready` is 200 only while the database answers a real round trip.
+- **Test pointers:** after adding/moving a `*.test.ts`, run `node test/run.js --index`; a missing/stale flat pointer
+ fails the suite. Vitest reads the real colocated file.
+- **Build:** pnpm's `onlyBuiltDependencies` is `esbuild`; Corepack downloads pnpm with Node's fetch in CI (the image
+ bakes it). `pnpm install --frozen-lockfile` and a clean `pnpm build` are the deploy gate.
+- **nginx** keeps `/api/ready` off the public vhosts; `/metrics` answers loopback only. Secrets
+ (`OV_OAUTH_CLIENT_SECRET`, `GAMES_EVENTS_SECRET`, `OV_NETWORK_AUTH_URL`) live in `/etc/openvibe/games.env` by name
+ only.
+- **Play's old pages 301 here:** do not re-add `apps/server` player pages under play.openvibe.network; the service and
+ API stay there.
+- Phone first for casual/portal pages; no Font Awesome; "Calm reduces, only Off removes".
+
+## Testing
+
+`pnpm test` runs every vitest file through [test/run.js](test/run.js) (package and server units incl.
+`apps/server/src/game/platformIntegration.test.ts`, which boots the real server twice on one database, and the
+architecture audit). `pnpm typecheck`/`pnpm lint` keep strict TS and no `any`; `tsx apps/server/scripts/sliceTest.ts`
+drives protocol clients across a restart; `pnpm test:editor|test:collab|test:console` are Playwright e2e checks. Run
+the narrowest file first, then the suite.
+
+## Documentation
+
+- [README.md](README.md) — purpose, owns, does-not-own, depends on, layout, development, commands, production,
+ platform integration, capabilities, tests, security.
+- [ARCHITECTURE.md](ARCHITECTURE.md) — principles, package graph, simulation, networking, server systems, persistence,
+ platform boundary, zones, entities, observability, security posture.
+- [ROADMAP.md](ROADMAP.md), [docs/games.md](docs/games.md), [docs/casual.md](docs/casual.md),
+ [docs/MAP_EDITOR.md](docs/MAP_EDITOR.md), [docs/EDITOR_COMPLETION_STATUS.md](docs/EDITOR_COMPLETION_STATUS.md),
+ [docs/instances.md](docs/instances.md), [docs/mods.md](docs/mods.md), [docs/ai-matches.md](docs/ai-matches.md),
+ [docs/platform-direction.md](docs/platform-direction.md), [docs/adr/](docs/adr/),
+ [docs/capabilities-proposal/](docs/capabilities-proposal/), [docs/service-manifest-proposal.json](docs/service-manifest-proposal.json).
+- [SECURITY.md](SECURITY.md), [.env.example](.env.example), [CHANGELOG.md](CHANGELOG.md).
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@