Reference13 min read11 sections

Persistent cooperative rooms

docs/PERSISTENT_ROOMS.mdSource updated 2026-10-28
Markdown
On this page 11 sections

This opt-in runtime connects the Lantern House, Rain Garden and Old Sanctuary to PostgreSQL. Travelers retain inventory, shared chest contents, doors, beacon flags and their last committed position across reloads and server restarts. The existing beta and editor commands retain their current storage profiles.

The browser still owns movement. This is a cooperative room for trusted playtesters, not the roadmap's completed authoritative simulation or account system. Durable interactions are reduced on the server and acknowledged only after PostgreSQL commits. Pose recovery is sampled every four seconds while the client runs; this is not a measured five-second recovery guarantee under every failure/load condition.

#Local run

Use Node 22.23.4 (engine bundles pin the exact Node version) and an existing PostgreSQL database:

bash
npm ci
export PGHOST=127.0.0.1
export PGUSER=darkwind
export PGDATABASE=darkwind
export PGPASSWORD='your-database-password'
npm run dev:persistent

Alternatively set DW_DATABASE_URL. Open http://localhost:8793/; it redirects to the configured room. This uses different ports and data from npm run dev:editor. A second browser profile creates a second traveler. Opening the same traveler in another tab closes the previous connection to prevent two tabs controlling one identity.

The browser stores only a private return credential in local storage for this room. Keep it to return to the same traveler. It expires after 30 days without admission; the stored journey remains in PostgreSQL. Account recovery and credential management UI are not implemented. Clearing site data loses browser access to that traveler. Movement/inventory restoration does not depend on the legacy local save.

For several persistent rooms behind one public address, use the regional doorway and room pool. It adds compatible-instance selection, capacity reservations, private supervised hosts and short-lived join tickets. The standalone command above remains available.

Use DW_PERSISTENT_ROOM to choose another room ID. New rooms use the current built engine. npm run dev:persistent -- --rebuild packages current source; an existing room deliberately continues using its retained engine and immutable world. Choose a new room ID to test a new build. The current build includes database migrations through version 9: when testing alongside an earlier persistent build, use a new dedicated database. Startup refuses to apply a new schema to a database with retained rooms, because their older engines reject unknown migrations. Content upgrades within this schema now use the explicit preview/apply workflow below. Cross-schema upgrades remain unsupported; do not bypass the startup guard.

#Persistent floating-world playtest

Use a fresh dedicated database with schema 9 and a new room ID. With the connection variables above set, start the built-in cove:

bash
createdb darkwind_floating
PGDATABASE=darkwind_floating DW_PERSISTENT_ROOM=driftwood-test DW_ROOM_TEMPLATE=driftwood npm run dev:persistent -- --rebuild

If DW_DATABASE_URL is set, change that URL to the new database instead. Open http://localhost:8793/ in two browser profiles. Walk onto the jetty, approach the timber and press E at Gently push. Both travelers see the same motion. Stop and restart the same command: the moved timber and traveler credentials survive. Floating simulation runs while the server is active, including with no travelers; a stopped server resumes its committed clock without offline catch-up.

To use your own cove, create or edit Driftwood cove in the private editor, Save and Export the authored world document. For a new room replace DW_ROOM_TEMPLATE=driftwood with DW_WORLD_DOCUMENT=/absolute/path/to/dusk-draft-N.json. The document retains authored entity identities and must pass checksum and semantic validation. Choose one source option. Existing rooms always use their pinned database world; changing these variables cannot overwrite a retained room. Use the explicit stopped-room upgrade workflow for subsequent revisions.

The server owns floating bodies and validates push reach/obstruction; cooperative character movement remains browser-reported. Push motion, player revision, receipt and outbox commit atomically. Physics-only checkpoints advance a separate revision before broadcast, without consuming gameplay receipts. Lease fencing prevents an expired host from committing. Backups include the native checkpoint and physics revision in the same database snapshot as player state.

Stopped-room upgrades preserve surviving body motion and reject stale previews. Removing a body or transferring it to another place currently rejects because there is no explicit body recovery mapping. Authored position changes reposition surviving bodies, and overlapping collision revisions reject. The upgrade command requires a recovery-container ID; add a container to the candidate world if using the cove, which has no chest by default. For server-owned character movement with these bodies, use the trusted floating profile. See floating-world acceptance.

#Shared chest playtest

New Lantern House rooms include an oak and brass chest beside the keeper’s writing desk. Take the lantern, approach the chest and press E. Use Leave in chest, then have another traveler use Take into satchel. The panel updates for both travelers. Transfers wait for a database receipt; reconnecting disables its controls. A full chest refuses another deposit. The item disappears from its original pickup spot while carried or stored.

For an earlier persistent installation, keep its database and use a fresh one for this test (with your existing PostgreSQL connection variables):

bash
createdb darkwind_storage
PGDATABASE=darkwind_storage DW_PERSISTENT_ROOM=storage-test npm run dev:persistent -- --rebuild

If you use DW_DATABASE_URL, point that URL at the new database instead; it takes precedence over PGDATABASE. On Coolify, create a separate Compose resource with its own database volume. Do not remove the earlier resource’s volumes to get a fresh build.

Close both browsers or restart the server with an item in the chest, then reconnect: it stays in the chest until someone takes it. The current static profile has no time-based decay or empty-room simulation. Depositing equipped items stows them atomically. Concurrent withdrawals award the item once; a lost response can be retried with the same operation ID without applying twice.

Storage is authored as a format-10 container interaction with a unique ID, world-space position, reach radius and capacity from 1 to 64 items. Contents are public to the room. Removing containers from a persistent release requires the staged recovery workflow below. Cross-room transfers remain separate roadmap work.

#Upgrade a persistent room

This is an operator-run content upgrade with a short maintenance stop. It preserves traveler identities and item ownership, moves items from deleted containers into an explicitly selected recovery chest, and retains names/equipment definitions when a pickup is removed. It does not migrate an older database schema or keep the room playable during activation.

Use the same PostgreSQL connection, DW_PERSISTENT_ROOM, DW_DATA_DIR and exact Node version as the room. Back up the database and retained-engine directory first. Source and target engines must support the current schema (6) and upgrade policy (1). If your room predates this schema, use a new dedicated test database; startup intentionally refuses to alter its retained engine's schema.

  1. Build the candidate engine and write or export its proposed world JSON. For a new Lantern House test room, this sample removes the original chest and lantern pickup and adds a recovery chest across the room:

    bash
    npm run build
    npm run room:upgrade:demo -- recovery-world.json
  2. Stop the room's original process (Ctrl+C locally, or stop its service). Allow its lease to expire if it was killed abruptly. Previewing while it is running is allowed, but subsequent activity makes that snapshot stale. With the room stopped, create a fresh plan:

    bash
    npm run room:upgrade -- preview recovery-world.json recovery-chest recovery-plan.json
  3. Review the printed summary and the saved JSON: exact source/target release, every player's before/after state, recovered items, retained definitions, retired flags/doors and stowed equipment. Every traveler returns to the candidate entry, which must pass a native collision/floor check. Owned items remain owned. Loose items with a surviving pickup move to that pickup; loose items without one move to recovery. A capacity reduction moves the alphabetically last excess item IDs to recovery. Insufficient recovery capacity rejects the whole plan.

  4. Apply that exact file, then restart the room:

    bash
    npm run room:upgrade -- apply recovery-plan.json
    npm run dev:persistent

The apply command refuses a live room lease, changed snapshot, edited plan checksum, incompatible engine or unsafe arrival. It commits the release, player/item revisions, recovered contents, audit plan, receipt and outbox event in one transaction. Repeating the identical apply command returns its original receipt. Plan files are written privately and never overwritten; choose a new filename for a new preview. The compiled engine also includes runtime/upgrade.mjs for the same commands without source tooling.

Connected travelers see “A new chapter.” after the upgraded server returns. Continue your journey reloads the retained release with the same private traveler credential. In the demo, press E at the recovery chest, take the lantern into your satchel, then press I to inspect it. The lantern remains usable even though its original pickup no longer exists. Inventory-only definitions use world format 11 and compiler inventory-world.11.

Prior releases, transactions and the full upgrade plan remain retained. Removed flags/doors are recorded in the plan's before-state; they are not active in the new world. Gear is stowed, never deleted. Reverting content requires another reviewed forward upgrade; switching code alone must not undo committed ownership. Floating body state is carried through compatible upgrades as described above. Cross-schema migrations, general dynamic-state recovery mappings, live activation and down-migrations remain future work.

#Coolify / Docker Compose

Create a separate Docker Compose resource from this repository and select compose.persistent.yaml. Set:

  • DW_POSTGRES_PASSWORD: a private database password.
  • DW_ROOM_PASSWORD: the passphrase testers enter when joining.
  • DW_ALLOWED_ORIGINS: the exact public origin, for example https://play.example.com. Multiple allowed origins are comma-separated; include scheme and optional port, without a path.
  • DW_PERSISTENT_ROOM: optional, defaults to lantern-house.
  • DW_ROOM_TEMPLATE: set driftwood for floating props, raft for Cedar crossing with a steerable shared raft, drawbridge for the hinged Alder gate, causeway for linked decks, or cargo for the Lantern passage playtest; choose DW_ROOM_AUTHORITY=cooperative or trusted.
  • DW_WORLD_DOCUMENT: optional alternative to the template; an absolute path to an exported document inside the room container. Make that file available through an operator-managed mount or the /data volume before startup.

Assign the public HTTPS domain to room, port 8793. PostgreSQL has no published host port. Keep the existing beta/editor deployment separate while testing this profile.

Compose explicitly creates two named volumes:

Volume Container mount Contents
darkwind-persistent-db PostgreSQL /var/lib/postgresql Player state, floating checkpoints, item ownership, room flags, credentials, receipts and release records
darkwind-persistent-engines Room /data Verified immutable client/runtime bundles required by retained room releases

Use the persistent backup and restore commands to capture the database together with its exact retained engines. Back up both volumes. Preserve their names and resource identity across redeploys. Removing volumes destroys persistence; rebuilding an image does not migrate an existing room to the new engine. A missing or corrupted retained engine prevents startup instead of silently substituting current code. The initial deployment must use an empty dedicated database or a compatible control schema. Do not run a second owner against the same room; expiring database leases fence ownership.

The built bootstrap and retained runtime are exercised with native PostgreSQL by browser acceptance. Container execution must additionally be checked on a Docker-capable host; passing native tests is not evidence of a successful Coolify deployment.

#Acceptance commands

bash
npm run check
npm test
npm run test:control
npx playwright install chromium --only-shell
npm run test:browser:persistent
npm run test:browser:persistent -- --portals
npm run test:browser:upgrades
npm run test:browser:persistent-floating

The control and browser commands create and remove isolated PostgreSQL clusters using initdb and pg_ctl. Browser acceptance uses port 5175 and requires Playwright Chromium; set PLAYWRIGHT_EXECUTABLE_PATH only when using a preinstalled Playwright-compatible browser. It builds a fresh production engine, retains it, seeds the production world with a nearby test spawn, and starts the compiled production bootstrap. No user world or editor server is modified.

#Current boundaries

The persistent profile supports static places, shared containers, doors, pickup/equipment, dialogue outcomes, beacon flags, authored gateways and returning to the entry, plus server-owned floating boxes in cooperative rooms. Patrols, platforms, scripted sliding props and object behavior programs are rejected at startup until their hot state and persistence have a coherent recovery contract. The upgrade browser journey additionally stops/restarts the compiled production runtime and invokes its packaged upgrade CLI; its test-only controller uses port 5176.

The persistent runtime does not yet expose creator edits or seamless upgrades to a running room, cross-room item transfer, accounts, public admission management or item-drop UI. The lower-level ledger's drop command is not exposed through this player protocol.

The prototype retains up to 128 admitted identities and 100,000 transactions per room, and up to 16 engine builds per data directory, and 32 retained upgrades per room. Compaction, retention operations and long-running capacity qualification remain plan work. Reaching a transaction budget refuses further commits instead of discarding history. Existing private editor invitations and old relay save files are not migrated into this profile.

The current complete browser acceptance uses Playwright-matched Chromium 141. The corrected software launch also renders on Chromium 155 in initial-entry/pixel checks, but its second-traveler shader compilation timeout remains unresolved. See render placement qualification; this is a headless test configuration, not a recommendation to downgrade a normal browsing installation.

#Opt-in server-owned movement

For a new room, the trusted profile replaces browser-reported poses with server simulation and local prediction. Use npm run dev:trusted -- --rebuild with a fresh room ID, or set DW_ROOM_AUTHORITY=trusted on the Compose room service. Existing rooms retain their authority and engine. This profile admits static worlds and bounded floating boxes; see its runbook for the trust boundary and remaining gates.

Normal process shutdown now drains admitted work and saves final positions before releasing the room. See host shutdown and recovery for deadlines, reconnect behavior and supervisor requirements.

#Owned physics in persistent and trusted rooms

DW_PHYSICS_BACKEND=owned now selects the owned solver for the persistent host, trusted player simulations, durable cargo validation, and subprocess/network place owners. It defaults to rapier. PostgreSQL schema 8 stores the choice in dw_rooms.physics_backend; a room cannot change backends on restart or accept an incompatible floating snapshot. Content upgrades retain that choice. The existing retained-engine/schema guard still applies: use a fresh dedicated database when testing this new schema alongside an older build.

With PostgreSQL connection variables configured, a separate local test is:

Terminal
createdb darkwind_owned
PGDATABASE=darkwind_owned DW_PHYSICS_BACKEND=owned \
DW_PERSISTENT_ROOM=owned-trusted DW_RELAY_PORT=8795 \
DW_ALLOWED_ORIGINS=http://localhost:8795,http://127.0.0.1:8795 \
npm run dev:trusted -- --rebuild

If using DW_DATABASE_URL, point it at the new database instead. Open http://localhost:8795/ in two profiles; the host supplies the matching physics=owned URL. Use the same variables on restart. Port 8795 keeps this separate from the ordinary editor and npm run dev:owned relay. For shared floating props add DW_ROOM_TEMPLATE=driftwood when creating the new room, or supply an exported authored world through DW_WORLD_DOCUMENT.

compose.persistent.yaml, compose.cargo.yaml and compose.rooms.yaml pass DW_PHYSICS_BACKEND to their host/coordinator. The regional pool pins each new room to its selected backend, starts retained rooms with their stored choice, and returns matching player URLs. HTTP admission and WebSocket hello both reject mixed backends. Physics services receive the backend in authenticated owner initialization; they do not need a separate global solver setting.

Focused verification: owned cooperative, trusted, process-owner and network-owner hosts admit clients and recover committed physics after restart; PostgreSQL rejects mixed saves and atomically transfers owned cargo with a rider. Existing persistent/upgrade suites pass (18 checks in that focused run). A retained-runtime routing check covers owned admission, tickets and coordinator restart; two related routing checks pass. The packaged two-player trusted browser journey passes shared lantern ownership, server-saved walking and reload recovery. Its restored view was reviewed in test-results/owned-persistent-restored.png. Type/package checks pass.

Review later: owned content-upgrade/backup round trips, injected failures across all distributed cargo handoff stages, sustained latency and worker load, vehicle and ragdoll browser journeys under trusted authority, and multi-device deployment. These are qualification follow-ups; this increment does not switch the default solver or approve the existing character/environment art.

#Authored responses and sensor puzzles

Installed touch-toggle, custom material and 2D sensor responses now run on the persistent room owner, including trusted movement rooms. Save and Export an authored world from the editor, then set DW_WORLD_DOCUMENT to that document when starting a new room with npm run dev:persistent -- --rebuild or npm run dev:trusted -- --rebuild. Use the fresh dedicated schema-9 database required above. Existing rooms continue using their pinned release and engine.

Private script state stays in PostgreSQL; travelers receive only public results. Sensor state/output and body checkpoints commit together. Sensor-only worlds use admitted traveler poses; trusted rooms use simulated poses. The room's lease fences every commit. Sensor events join the ordered durable outbox without changing player revisions. Release previews retain script state and check it against the candidate configuration. Backup manifest schema 6 includes the new private column and sensor-event table; older backup manifests remain unsupported by this build's exact-schema restore guard.

Both ordinary action collection and the control ledger's alternate pickup command respect response/sensor locks. A failed touch response retains its fault state in a rejected receipt, so retries cannot silently reset a parked script. See TILEMAPS.md for focused acceptance and remaining qualification.

#Lost browser credentials

The owner can now recover a saved traveler with npm run player:recover. Recovery requires a stopped room, preserves the original player/inventory, revokes old credentials and produces a private link that the traveler confirms in the updated player. Global accounts and self-service recovery remain open.

End of Persistent rooms