- TypeScript 94.8%
- CSS 2.1%
- HTML 2%
- JavaScript 1%
- Shell 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .github/workflows | ||
| apps | ||
| deploy | ||
| docs | ||
| packages | ||
| test | ||
| .env.example | ||
| .gitignore | ||
| .gitleaks.toml | ||
| .npmrc | ||
| .prettierrc.json | ||
| ARCHITECTURE.md | ||
| CHANGELOG.md | ||
| eslint.config.js | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| ROADMAP.md | ||
| SECURITY.md | ||
| STATUS.json | ||
| tsconfig.base.json | ||
| tsconfig.solution.json | ||
| vitest.audit.config.ts | ||
| vitest.config.ts | ||
OpenVibe.Games
Purpose
OpenVibe.Games hosts Scraplandia at https://openvibe.games and https://play.openvibe.games.
Scraplandia's extraction events double as its scored "Haul" mode: securing at-risk valuables at a
recovery bird earns haul points, with its own all-time/this-week leaderboard and achievements.
The same Games process also hosts Salvage Rush, an on-foot timed collection game with its own
place, world, rooms and round system. Play it at /play?game=salvage-rush on the same host.
See docs/games.md for both games and local progression.
Players build their own creator worlds in the map editor and publish them at /worlds for anyone to join
(sandboxed: characters are not saved there); see docs/games.md.
It also hosts the casual line (plan T22): small phone-first browser games built from one kit, starting with
Vibe Heroes, an idle hero collector at /play/vibe-heroes. See docs/casual.md.
A persistent multiplayer physics sandbox for the browser: Source-style movement,
physgun-driven building from crafted physical objects, gathering, crafting, and
extraction risk — running against an authoritative dedicated server.
Stack: TypeScript everywhere · Babylon.js (WebGPU with WebGL fallback) ·
Rapier physics (@dimforge/rapier3d-deterministic-compat, one pinned build
headless on the server and in client prediction) ·
WebSocket protocol · PostgreSQL 18 + Valkey persistence (openvibe-sdk) · pnpm monorepo.
Owns
- the game: the authoritative simulation, the world, characters, inventories, skills, blueprints and
props, in PostgreSQL (ADR-0007 decision 5: schema from scratch, migrations under
packages/persistence/migrations, a write-behind flusher — the tick never awaits I/O) - the map editor and its assets, the mod registry (
games-content@2packs, grants, audit), thegames-quickjs@1server script mods (docs/mods.md) and thegames.*events - the game WebSocket protocol and the portal,
/playand/editorpages - the casual kit (
packages/casual), its server economy (saves, ledger, idempotent actions) and the/play/<game>pages
Does not own
- identity (OpenVibe.Network: players are keyed by Network subjects), file storage beyond the local copy (OpenVibe.Media mirrors map-editor assets), events delivery (OpenVibe.Events)
- mod grants' authority (Network keeps them,
mods.grant.manage) and general-purpose executable mods (OpenVibe.Host Stage C); Games runs its own boundedgames-quickjs@1scripts
Depends on
- OpenVibe.Network (SSO, JWKS, client-credentials tokens, identity resolve, the
games.progress.summaryuser module, mod grants), OpenVibe.Events (outbox relay, subscriptions), OpenVibe.Media (asset mirror) openvibe-contractsv0.77.0,openvibe-sdkv0.22.0 andopenvibe-sharedv4.15.2 (pinned by release tarball inapps/server/package.json), Babylon.js, Rapier, PostgreSQL (pg, or embedded PGlite in development), Valkey (iovalkey, optional)
Repository layout
apps/
client/ Browser client: rendering, prediction, input, HUD
server/ Dedicated authoritative server: simulation, networking, persistence
packages/
shared/ Math, ids, events, logging facade, fixed-timestep primitives
protocol/ Versioned wire messages + codec (zod-validated inbound)
content/ Data-driven item/recipe/world definitions + validation registry
gameplay/ Pure domain logic: inventory, crafting, movement sim, zones, entities
physics/ PhysicsWorld abstraction + Rapier adapter (@openvibe/physics/rapier)
persistence/ DTOs, repository interfaces, PostgreSQL implementation, migrations
casual/ The casual kit: rules, auto-battle and the Vibe Heroes pack (engine-free)
docs/adr/ Architecture decision records
See ARCHITECTURE.md for boundaries and data flow, ROADMAP.md for what exists and what is planned.
Prerequisites
- Node.js >= 22
- pnpm 10 (
corepack enable)
Development
pnpm install
# Terminal 1 — authoritative server on :8000 (tsx watch)
pnpm dev:server
# Terminal 2 — client with hot reload on :5173 (proxies /ws to :8000)
pnpm dev:client
Open http://localhost:5173 in two browser windows to see multiplayer.
Controls: WASD move · Space jump (hold to bunnyhop) · Shift sprint · 1-6 hotbar · Tab menu (inventory / crafting / skills / players) · G drop held item · E gather / pick up props. Physgun (GMod-style): hold LMB grab (grabbing a frozen prop unfreezes it) · release to let go · RMB freeze · wheel push/pull · hold E rotate like a globe (Shift+E snaps) · hold Shift for grid-lock. Building: craft pieces → drop them (G or drag out of the inventory) → position with the physgun → freeze. Dropping IS placement; E picks any prop back up into your inventory.
The loop: spawn in Scrap City (safe city — no PvP, no building, no physgun), gather branches/stones/scrap outside the gates, craft tools, harvest the forest (axe/woodcutting) and quarry (pickaxe/mining), craft building pieces, place them in the wilds, position with the physgun and freeze. Props are protected: only you and players you trust can move them.
Commands
| Command | Purpose |
|---|---|
pnpm dev |
Server + client dev processes in parallel |
pnpm test |
All unit tests (vitest, through test/run.js) |
pnpm typecheck |
Strict TypeScript across every package |
pnpm lint |
ESLint (typescript-eslint, no-explicit-any) |
pnpm format |
Prettier write |
pnpm build |
Production build of all packages + client bundle |
tsx apps/server/scripts/sliceTest.ts |
End-to-end vertical-slice test (boots a real server, drives protocol clients, restarts, asserts persistence) |
Production
pnpm build
DATABASE_URL=postgres://… DATABASE_DIRECT_URL=postgres://… STATIC_DIR=apps/client/dist PORT=8000 pnpm --filter @openvibe/server start
DATABASE_URL (pooled, through PgBouncer) serves; migrations run at boot on DATABASE_DIRECT_URL
(the owner role, a direct session). With neither set and NODE_ENV not production, the server uses
an embedded PGlite database under DATABASE_DIR (default data/) — development only, one process.
GAMES_PLACE_ID (default scraplandia) selects the default place for older clients and the
edited map. Both built-in places have separate world rows and instance leases. VALKEY_URL
(optional) makes the per-actor limits and the WebSocket upgrade tickets shared across instances
(issue is SET … PX … NX, consume is GETDEL, so a ticket is single-use cluster-wide).
GAMES_INSTANCES_PER_PLACE (default 1) enables several room simulations for a place in this process;
MAX_PLAYERS is per-room capacity. PostgreSQL records each room and its renewable fenced lease.
GAMES_INTEREST_CELL (default 16) and GAMES_INTEREST_RADIUS (default 80) tune spatial replication.
See docs/instances.md for transfer, lease expiry and the one-process limit.
TRUST_PROXY (optional) says which proxies may set the client-address headers: unset means the TCP
peer is the client and X-Forwarded-For/X-Real-IP/CF-Connecting-IP are ignored (they are
client-supplied and would otherwise let a caller mint fresh rate-limit buckets); true means the
server is only reachable through the proxy; otherwise a comma-separated list of trusted proxy
IPs/CIDRs (behind nginx, which sets X-Real-IP from $remote_addr). See .env.example.
The server serves the built client, /healthz (liveness), /api/ready, /metrics, and the game
WebSocket on one port. Pages are / (portal), /play and /editor; on play.openvibe.games / is
the game and /play redirects there. Any other path that is neither a file of the build nor a route
answers 404 (an HTML page with links to those pages; {"error":"not_found"} under /api; plain text
for a missing asset), never the portal with a 200. /api/ready is 200 only while the database answers a real round trip
(db.ready()) and the simulation
has ticked within the last 5 s, otherwise 503 naming the failed check (status
ready/not_ready, checks.db, checks.tick); nginx keeps it off the public vhosts. Environment: PORT, HOST, STATIC_DIR,
MAX_PLAYERS, LOG_LEVEL, GAMES_PLACE_ID, the database variables above (platform variables below).
Deployed at https://openvibe.games (nginx TLS termination → server on 127.0.0.1:8000,
systemd unit openvibe-games.service from deploy/openvibe-games.service,
env file /etc/openvibe/games.env (which holds DATABASE_URL, DATABASE_DIRECT_URL and VALKEY_URL);
play.openvibe.games Host-routes to the same process).
Deploy on the host with sudo /opt/openvibe.games/deploy/scripts/deploy.sh, which runs
ovhost deploy games (OpenVibe.Host, strategy pnpm-build; roadmap WS-N task 11): a fast-forward pull as
the checkout owner, pnpm install --frozen-lockfile, every workspace package's dependencies checked, pnpm build,
the restart (the SDK migrator applies any new migration as the owner before serving), /api/ready, and on failure the checkout
restored, reinstalled and rebuilt and the server restarted again. Players online are reported and
reconnect; --wait-idle holds the restart until nobody plays, --rollback runs ovhost rollback games,
DRY_RUN=1 prints ovhost plan games. Do not pull by hand first (ovhost would find nothing new; pass
--restart if you did). The wrapper exits with an error if ovhost is missing.
Rollback: sudo deploy/scripts/deploy.sh --rollback (ovhost rollback games), which rebuilds the previous
commit. Migrations are additive and applied at boot, so an older build starts on the newer schema; the SDK
migrator refuses a migration file that was edited after it ran (write a new one instead).
Platform integration (OpenVibe network)
Games integrates with the rest of OpenVibe at its service boundary only
(apps/server/src/platform, apps/server/src/mods); the simulation packages
do not know the platform exists. See ADR-0006.
- Identity. Signed-in players are keyed by their canonical
openvibe.network subject (
usr_…/gst_…, the token'ssubject_id); a token without one cannot name an account. Guest tokens that look like account keys (usr_…) are refused. - Service principal. Calls to Events, Media and Network identity carry a
client-credentials token of the
gamesOAuth client (same client id and secret as SSO), one per audience. There are no shared keys. The SSO calls made for a player (/api/auth/me, code and FedCM exchanges) keep using the player's own token. - Events (OpenVibe.Events, through a transactional outbox in
PostgreSQL, table
event_outbox):games.player.joined|left,games.skill.leveled,games.blueprint.unlocked,games.world.saved(at most one checkpoint perWORLD_SAVED_EVENT_MINUTES, plus shutdown) andgames.mod.installed|enabled|disabled|grants_changed|revoked. Achievements are not published as events: an unlock is stored in Games (its achievement rules read those tables) and sent to OpenVibe.Play over Play's API (GAMES_PLAY_URL), which is the network-wide read surface for titles, scores and unlocks. Progression events come from what a save actually wrote, in the same transaction. - Inventory. Six built-in achievements give Live items: Scraplandia First Haul (Toxic Name), Big Haul (Golden Name), Haul Week (Cowboy Hat); Salvage Rush First Haul (Neon Name), Big Haul (Flames), Arena Winner (Stars). Games grants them after unlock or guest adoption using its own OAuth client; failed grants retry. Inventory items are free, never sold or traded.
- Media. Map-editor assets (textures, paint masks, glb models; the only
uploaded files Games has) are still served from local disk and are also
copied into Media objects (
kind: asset, namespaceMEDIA_NAMESPACE), with a restart-safe queue (media_mirrors). There are no screenshots or blueprint files to upload: blueprints are per-player recipe unlocks. - Mods (ADR-013 in OpenVibe.Contracts). Manifests follow
[email protected](Games validates against the schema in the installedopenvibe-contractspackage — pin drift is a defect, ADR-0007 decision 12). The content runtime isgames-content@2: declarative data packs checked against@openvibe/content(announcements; inert, mod-owned props; definitions — items, recipes, crops, NPC archetypes — merged into the content registry at boot, so they take effect on the next restart). The client sends its definition digest inhello; the server checks it before joining and echoes its digest inwelcome. A mismatch is rejected so installed definitions cannot silently desync clients. Scraplandia's default blank-slate map is supplied by the built-in pack. Each install stores the approved subset of its requested capabilities; every runtime binding checks it at call time, a revoked install or capability stops affecting the world on the next tick, and install/grant/use/deny/revoke are audited (mod_audit). Trust tiers are metadata only. Staff can installgames-quickjs@1script packs through the mods API; built-in packs can carry scripts too. Scripts are pinned by the def-set digest and run in a per-mod QuickJS sandbox only whereGAMES_SCRIPT_MODS=1(budgetscpuMs2 per tick,memoryMb8,stackKb128 by default; over budget disables the mod for the instance; host API and limits in docs/mods.md). API:GET /api/v1/mods,GET /api/v1/mods/:id(public);POST /api/v1/mods,POST /api/v1/mods/:id/enable|disable|revoke|grants|reload,DELETE /api/v1/mods/:id/grants/:capability,GET /api/v1/mods/:id/audit(owner/admin session, or a principal token withgames.mod.manage). Fresh clients fetchGET /api/v1/contentbefore the digest handshake so live script changes can be joined. - Status:
GET /api/v1/platformreports whether events and the Media mirror are on, the outbox backlog and mirror counts (no secrets). - In production (2026-09-23, release
f11e21c): events and the Media mirror are on.games.world.savedevents reach OpenVibe.Events; no mod is installed, no grant exists and no asset has been mirrored to Media yet. - OpenVibe Frame. The portal,
/playand/editorload the OpenVibe Frame (navbar, footer, theme loader) from this server's own pinned copy at/shared/(apps/server/src/net/sharedAssets.ts), so they keep their frame while Network is down. - Persistence proof.
apps/server/src/game/platformIntegration.test.tsboots the real server twice on one database and checks characters, world props, the world clock and the mod registry survive the restart;sliceTest.tscovers the gameplay state end to end.
Platform environment (all optional; service calls need OV_OAUTH_CLIENT_SECRET):
| Variable | Purpose |
|---|---|
OV_OAUTH_CLIENT_ID |
OAuth client / principal id (default games) |
OV_OAUTH_CLIENT_SECRET |
Its secret: enables SSO and every service call |
OV_NETWORK_INTERNAL_URL |
Network base for /oauth/token, identity and the JWKS (e.g. http://127.0.0.1:4000) |
EVENTS_URL |
OpenVibe.Events base; enables the outbox (EVENTS_PUBLISH=off disables) |
MEDIA_URL |
OpenVibe.Media base; enables the asset mirror (MEDIA_MIRROR=off disables) |
GAMES_INVENTORY_URL |
Inventory base (default https://inventory.openvibe.network; production can use loopback) |
GAMES_ITEMS |
on (default) or off; off also when the OAuth client secret is unset |
GAMES_PLAY_URL |
OpenVibe.Play base (e.g. http://127.0.0.1:5040); unset = scores and unlocks stay local |
MEDIA_NAMESPACE |
Media tenant for the copies (default games) |
WORLD_SAVED_EVENT_MINUTES |
Checkpoint event interval (default 15) |
GAMES_CREATOR_WORLDS_MAX |
Creator worlds running at once (default 2; 0 stops new ones starting) |
GAMES_CREATOR_WORLDS_PER_PERSON |
Worlds one account keeps (default 5) |
GAMES_CREATOR_WORLD_IDLE_SECONDS |
An empty creator world stops after this long (default 180) |
GAMES_CREATOR_WORLD_PLAYERS |
Players per creator world (default 16) |
GAMES_CREATOR_SCRIPTS |
1 runs creator worlds' own scripts in their rooms (QuickJS sandbox; off by default) |
Capabilities
Implemented here (the service manifest's capabilities): games.mod.manage and games.mod.read (the
mods API, for a principal token or an owner/admin session); games.world.announce,
games.prop.place, and games.def.define for declarative packs; and
games.script.run, games.players.read, games.player.message,
games.world.emit, and games.storage.kv for script packs. Script bindings
are absent unless approved for that install.
Service grants the games principal needs:
inventory.item.grant (openvibe.inventory), events.event.publish and events.subscription.manage (openvibe.events: the outbox, and the account,
revocation and mod-grant subscriptions), media.object.upload
(openvibe.media, namespace games), identity.subject.resolve
(openvibe.network), network.modules.write on games.progress.summary, and mods.grant.manage
(Network's mod grants, roadmap WS-M task 3).
Tests
pnpm test runs every vitest file (unit tests of the packages and the server, including
apps/server/src/game/platformIntegration.test.ts, which boots the real server twice on one database);
it goes through test/run.js, which maps the flat test/*.test.js pointers back to the colocated
TypeScript tests so ov test can select and shard them (vitest reads the real files, so their
@vitest-environment docblocks and imports are untouched). After adding or moving a *.test.ts,
run node test/run.js --index to regenerate the pointers — a missing or stale pointer fails the run.
pnpm typecheck and pnpm lint keep strict TypeScript and no any;
tsx apps/server/scripts/sliceTest.ts drives protocol clients against a real server across a restart.
Security
Reporting a vulnerability: SECURITY.md. The server is authoritative: inbound protocol
messages are validated (zod), the WebSocket has per-socket flood control, and HTTP writes that cross a
capability boundary (editor saves and uploads, the mods API) have per-actor limits (openvibe-sdk/limits).
Players are keyed by Network subjects; guest tokens that look like account keys are refused.
Script mods run in isolated QuickJS sandboxes with only their approved bindings.
nginx keeps /api/ready off the public vhosts and /metrics answers direct loopback callers only. OV_OAUTH_CLIENT_SECRET, GAMES_EVENTS_SECRET (signed
Events deliveries) and OV_NETWORK_AUTH_URL live in /etc/openvibe/games.env, by name only; there are no shared keys
between services.
- openvibe-contracts: v0.217.0
- openvibe-sdk: v0.65.0
- openvibe-shared: v4.15.2
- openvibe-publishing: v1.4.0