openvibers/openvibe.games AGENTS.md
AGENTS.md
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 throughopenvibe-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
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/serverandpackages/persistenceknow 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) wrappingovhost deploy games(strategypnpm-build:
pnpm install --frozen-lockfile,pnpm build, restore trackeddist-types/, restart).--wait-idleholds the
restart until nobody plays;DRY_RUN=1printsovhost plan games; rollbacksudo 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 likeusr_…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
thegamesclient, 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 perWORLD_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_mirrorsqueue
copies them to thegamestenant. There is no screenshot/blueprint upload. - Mods: manifests follow
[email protected]validated against the installed Contracts (pin drift is a
defect);games-content@2packs merge definitions at boot, the client sends its definition digest inhelloand 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@1runs only withGAMES_SCRIPT_MODS=1in a per-mod QuickJS sandbox (defaultscpuMs2/tick,
memoryMb8,stackKb128; 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/platformreports 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 intopackages/*;
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_PLACEonly
within one process,GAMES_CREATOR_WORLDS_MAXcaps 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 is127.0.0.1, and nginx overwrites the headers from
$remote_addr. Never readX-Forwarded-Fordirectly.- 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/readyis 200 only while the database answers a real round trip. - Test pointers: after adding/moving a
*.test.ts, runnode test/run.js --index; a missing/stale flat pointer
fails the suite. Vitest reads the real colocated file. - Build: pnpm's
onlyBuiltDependenciesisesbuild; Corepack downloads pnpm with Node's fetch in CI (the image
bakes it).pnpm install --frozen-lockfileand a cleanpnpm buildare the deploy gate. - nginx keeps
/api/readyoff the public vhosts;/metricsanswers loopback only. Secrets
(OV_OAUTH_CLIENT_SECRET,GAMES_EVENTS_SECRET,OV_NETWORK_AUTH_URL) live in/etc/openvibe/games.envby name
only. - Play's old pages 301 here: do not re-add
apps/serverplayer 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).