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:
npm ci
export PGHOST=127.0.0.1
export PGUSER=darkwind
export PGDATABASE=darkwind
export PGPASSWORD='your-database-password'
npm run dev:persistentAlternatively 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:
createdb darkwind_floating
PGDATABASE=darkwind_floating DW_PERSISTENT_ROOM=driftwood-test DW_ROOM_TEMPLATE=driftwood npm run dev:persistent -- --rebuildIf 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):
createdb darkwind_storage
PGDATABASE=darkwind_storage DW_PERSISTENT_ROOM=storage-test npm run dev:persistent -- --rebuildIf 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.
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:
npm run build npm run room:upgrade:demo -- recovery-world.jsonStop 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:
npm run room:upgrade -- preview recovery-world.json recovery-chest recovery-plan.jsonReview 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.
Apply that exact file, then restart the room:
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 examplehttps://play.example.com. Multiple allowed origins are comma-separated; include scheme and optional port, without a path.DW_PERSISTENT_ROOM: optional, defaults tolantern-house.DW_ROOM_TEMPLATE: setdriftwoodfor floating props,raftfor Cedar crossing with a steerable shared raft,drawbridgefor the hinged Alder gate,causewayfor linked decks, orcargofor the Lantern passage playtest; chooseDW_ROOM_AUTHORITY=cooperativeortrusted.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/datavolume 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
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-floatingThe 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:
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 -- --rebuildIf 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.