Repositories openvibers openvibe.games official
openvibers/openvibe.games
Games and creator worlds you play in the browser, with achievements on OpenVibe.Play.
Get it
Get this repository
A Git token is made for this copy and shown once. It can read and write the repositories you own, and expires in an hour.
git clone https://git.openvibe.codes/openvibers/openvibe.games.git
OpenVibe Link asks you on your computer. The folder is inside the root you chose in Link, and the request ends after two minutes.
Clone to my computerA Codes session clones from our disk: no network, no GitHub quota. When this repository has a GitHub twin, Codes pushes there too.
Open in CodesREADME.md
View fileOpenVibe.Games
Browser games and creator worlds — openvibe.games. Play in the browser, with achievements on OpenVibe.Play.
Live site: https://openvibe.games · API docs: https://openvibe.services · For AI: https://openvibe.games/llms.txt
Run it: pnpm install && pnpm dev (Node 22 or newer) · Tests: npm test
Part of the OpenVibe network; help and the developer API.
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](docs/games.md) for both games and local progression.
Server-run Vibe Heroes AI draft matches and Bench scoring are described in [docs/ai-matches.md](docs/ai-matches.md).
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](docs/games.md#creator-worlds).
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](docs/casual.md).
The player pages of OpenVibe.Play (the game services behind Games) live here too: /achievements, /leaderboards
and the API guide for game makers at /developers. They read Play's public API server-side; the service and its API
stay at play.openvibe.network, whose old pages 301 here.
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), the
games-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.summary
user module, mod grants), OpenVibe.Events (outbox relay, subscriptions), OpenVibe.Media (asset mirror) openvibe-contractsv0.77.0,openvibe-sdkv0.22.0 andopenvibe-sharedv4.33.0 (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](ARCHITECTURE.md) for boundaries and data flow,
[ROADMAP.md](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](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](.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](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](docs/adr/0006-canonical-subjects-and-platform-boundary.md).
- 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 thegamesOAuth 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, tableevent_outbox):games.player.joined|left,
games.skill.leveled,games.blueprint.unlocked,games.world.saved
(at most one checkpoint perWORLD_SAVED_EVENT_MINUTES, plus shutdown) and
games.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(budgets
cpuMs2 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 fetch
GET /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.ts
boots 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](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.
<!-- versions:start -->
- openvibe-contracts: v0.422.0
- openvibe-sdk: v0.161.0
- openvibe-shared: v4.235.0
- openvibe-publishing: v1.7.0
<!-- versions:end -->