The Lost Realms

Vault Administrator's Guide · Setup · Keys · Sign-in · Worlds

How to stand up a Server Vault, hand it your provider keys, put a login in front of it, and keep it running — written for the person who has to answer for the bill and the secrets.

Start HereWhat a vault is

The Lost Realms runs perfectly well as a single HTML file opened from your own disk. In that arrangement — Direct mode — the provider keys live in the browser, encrypted at rest, and every call to Anthropic or an image service is made by the page itself. Nothing else is involved, and for one person playing on one machine nothing else is needed.

A vault is what you run when that stops being true. It is a small Node server that hosts the game and proxies its calls, so that the keys sit on the server and never reach a browser at all. The client asks for “a GM turn” or “an image from this provider id”; the vault owns the endpoint, injects the key, and answers. The page never names a URL, which is the whole of the SSRF boundary: a proxy that cannot be told where to go cannot be turned into an open relay.

The one-line testIf more than one person will play, or the keys must not be on the machine doing the playing, or you want the art to stop living inside every save file — run a vault. Otherwise do not; Direct mode is less to go wrong.

What it gives you

Keys server-side

One Anthropic key and whatever image, audio, video and 3D keys you hold, AES-encrypted on disk and never returned to a client.

An admin page

/admin — keys, providers, usage, media, worlds and settings, in the game's own palette.

A media store

Generated art written once, content-addressed, and referenced by URL — instead of a megabyte of base64 in every save.

Hosted worlds

Publish a realm from the editor, serve it to players, and — if you choose — list it on a registry so strangers can find it.

A sign-in

Auth0 in front of the whole game and the admin page, with separate lists for who may administer and who may play.

A bill you can read

Per-provider usage and cost, tallied server-side, where one browser's ledger could never see the whole of it.

The vault is optional and detected, never configured. The app probes GET /vault/config on load: served by a vault it switches to Vault mode, opened any other way it stays in Direct mode. There is no setting to get wrong on the client.

Five minutes to a vault

Two variables and you are playing. Everything else in this guide is something you add to that.

cd Server
npm install

VAULT_ACCESS_TOKEN="$(openssl rand -hex 24)" \
VAULT_ANTHROPIC_KEY="sk-ant-..." \
npm start
# → open the printed http://localhost:8787/ and play. The key stays on the server.

On localhost the browser is handed the access token automatically, so there is nothing to paste. From anywhere else the token goes only to a signed-in player — see Who may play.

Next three thingsAdd VAULT_MASTER_KEY so the admin page can store keys rather than read them from the environment (Keys). Turn on HTTPS. Then put a login in front of it, in that order — each one is useful without the next.

A .env file instead

Everything here can live in Server/.env rather than on the command line; the vault loads it at boot. VAULT_ENV_FILE points somewhere else if you keep it outside the repo, which is the better habit on a real server.

Where everything lives

One directory holds everything a running vault writes. Knowing which file is which is most of knowing what to back up and what must never be committed.

PathWhat it isSecret?
Server/data/vault-keys.jsonThe encrypted key store. AES-256-GCM ciphertext; the master key is not in it.Yes
Server/data/vault-access.jsonWho may administer and who may play, as edited on the admin page.Personal data
Server/data/vault-settings.jsonServer name and description, provider slot overrides, model allow-lists.No
Server/data/vault-providers.jsonCustom provider descriptors an admin has authored.No
Server/data/vault-usage.json
Server/data/vault-usage-history.json
Aggregate call counts, tokens and cost. No prompts, no per-user rows.No
Server/data/worlds/<uid>/world.jsonPublished worlds, one pretty-printed file each. Relocatable — see Worlds in a git repo.No
Server/data/media/<shard>/<sha>.extThe content-addressed media store, plus its index.No
Server/data/generations/The last few raw generation artefacts, for diagnosis. Bounded by VAULT_GENERATIONS_KEEP.No
Server/server.key + server.certThe TLS pair, when you generate one locally. Gitignored.Yes (the key)
Server/.envWhatever you put in it — which is usually every secret you have.Yes

Each of those paths is an environment variable away from being somewhere else; Appendix A names them all. The one that moves for a real reason rather than tidiness is VAULT_WORLDS_DIR.

Part I · Standing It Up

Part IThe environment

A vault is configured entirely by environment variables. They are listed in full in Appendix A; this section is the shape of them — which ones you must set, which ones you set once you have decided something, and which ones exist only because you are debugging.

The two that are not optional

VariableWhy it is required
VAULT_ACCESS_TOKENGates the GM proxy. The server refuses to start without one (16 characters or more), because a vault with an open /vault/gm is somebody else's Anthropic bill. Generate a random one; never reuse a provider key.
VAULT_ANTHROPIC_KEYThe key the vault injects into GM calls — or ANTHROPIC_API_KEY, which it falls back to. Strictly speaking the server starts without it and the game then cannot narrate, which is not a server you would keep.

Once VAULT_MASTER_KEY is set and you have entered keys on the admin page, the stored key wins and the environment key becomes the bootstrap for a fresh install rather than the live setting.

Deciding where the vault is

Three variables answer three different questions, and conflating them is the most common way a remote vault half-works.

VariableThe question it answers
VAULT_HOST / VAULT_PORTWhat does the process bind? Loopback by default. Binding anything else requires TLS — the vault will not serve the world in clear text by accident.
VAULT_PUBLIC_URLWhere does the world reach this vault? The address that goes into a registry listing and the origin Auth0 returns to. It must be an https address a stranger can reach: loopback, LAN and plain http are refused for a listing.
VAULT_LOGIN_URLWhere should Auth0 send the browser back? Only needed when that is somewhere else — the vault on your own machine, published through a tunnel, whose operator signs in at http://localhost:8787 while the realm is listed at the tunnel address.
The trap in that third rowPoint VAULT_LOGIN_URL at a loopback address on a vault that gates play behind Auth0 and it lists perfectly and turns every arriving player away — their browser is sent to a callback on their machine. The vault warns at boot when all three conditions hold. A vault with Auth0 off has open play and no such problem.

Keys & the encrypted store

A vault can hold a key two ways. From the environment, which needs nothing else and means editing a file and restarting to change one. Or in the store, which is a file of AES-256-GCM ciphertext that the admin page can write to while the server runs.

The store is off until you set VAULT_MASTER_KEY — 32 bytes, as 64 hex characters or base64. It encrypts the store and is never written beside it: a backup of the data directory alone is not a backup of your keys, and that is deliberate.

VAULT_ACCESS_TOKEN=... VAULT_MASTER_KEY="$(openssl rand -hex 32)" npm start
# → http://localhost:8787/admin — add keys there instead of in the environment.
Write-only, by designThe vault will tell you a key is configured and show you its last four characters. It will never show you the key. There is no admin route that returns one, so a compromised admin session cannot walk away with your provider credentials — only spend them, which the usage tab will show.

The providers it knows

Each row on the Keys tab is one provider, and its name is a link to the page where that key is created — the one step the admin page cannot take for you. Two are marked required, which is a claim about the game being playable rather than about the server booting: the desktop launcher gates its Play button on exactly those two.

ProviderWhat it buys
anthropicThe Game Master, and every narration in the gameRequired
nanobananaImages, gallery variations, portraits, icons, weathering, reactionsRequired
pollinationsImages and portraits — the one provider with a keyless tierOptional
openai · fal · higgsfieldFurther image providers, each selectable per generation slotOptional
elevenlabsSound effects and music — two endpoints, one keyOptional
runwareVideoOptional
tripo · worldlabs3D item models, and 3D room environmentsOptional
vectorizerTracing a raster picture into vector glyphsOptional

An admin can also author a provider the vault has never heard of, on the Providers tab, as a validated descriptor: an endpoint, an auth style, a request template and a response rule. It is data, not code — the vault runs every descriptor through one trusted executor with the host pinned, so adding a provider can never mean running something new on the server.

HTTPS

The vault speaks HTTPS the moment it has a key and a certificate, and it requires them to bind anything but loopback. For local work, generate a self-signed pair:

./make-cert.sh                 # localhost + 127.0.0.1 + ::1
./make-cert.sh 192.168.1.20    # ...plus another address you reach the vault by

On Windows make-cert.ps1 is the same script, using openssl when it is on PATH and .NET's own APIs when it is not — that fallback needs PowerShell 7, and says so if you are on 5.1.

Both files are written beside server.js, both are gitignored, and the vault picks them up with no configuration. Set one without the other and it is a fatal misconfiguration, not a quiet fall back to http: a server that has been told to be encrypted and silently is not is worse than one that refuses to start.

A self-signed certificate is not trustedBrowsers warn once per profile and again whenever you replace it. The desktop app refuses one outright — TLR_ALLOW_INSECURE_CERT=1 accepts it for the vault's own host during development and must never ship. The cure for both is a local root CA: one root installed into the machine's trust store, and a certificate signed by it. The full recipe is on the admin page under Settings › Server, because that is the screen that raised the question — and its openssl half is executed by a test rather than proofread.

A certificate must carry a subjectAltName; browsers stopped honouring the Common Name years ago. For anything genuinely public, terminate TLS at a reverse proxy with a real certificate and let the vault stay on loopback behind it.

Sign-in with Auth0

Set four variables together and the whole game requires a sign-in, not just the admin page. An unauthenticated request to any app page is redirected to Auth0's Universal Login; only a successful login gets the game. The same session gates /admin, which additionally requires an email on the admin list.

VAULT_ACCESS_TOKEN=...  VAULT_MASTER_KEY="$(openssl rand -hex 32)" \
AUTH0_ISSUER_BASE_URL=https://auth.example.com \
AUTH0_CLIENT_ID=... \
AUTH0_SESSION_SECRET="$(openssl rand -hex 32)" \
VAULT_PUBLIC_URL=https://vault.example.com \
VAULT_ADMIN_EMAILS=you@example.com \
npm start

A Native application, not a Regular Web Application

The vault is a public client signing in with Authorization Code + PKCE. Create the Auth0 application as Native, with Token Endpoint Authentication Method None. A Regular Web Application is confidential, and Auth0 would issue it a client secret this code has nowhere to put — which is also what lets the vault ship inside the desktop app, where a secret would be sitting in every copy.

Two things called a secretAUTH0_CLIENT_SECRET is not used and is safe to delete. AUTH0_SESSION_SECRET is required, is a different thing entirely — it signs this vault's own session cookie — and is shared with nobody. Generate a fresh random value; do not paste an Auth0 secret into it.

The two URLs to register

Auth0 settingValue
Allowed Callback URLs<VAULT_PUBLIC_URL>/admin/callback
Allowed Logout URLs<VAULT_PUBLIC_URL> — the base URL itself
The logout row is the one people get wrongIt is the post-logout returnTo, not a route on the vault. The vault sends your public URL with any trailing slash stripped and nothing appended — so http://localhost:8787, never …/admin and never …/admin/logout. Auth0 matches these entries exactly: it allows a wildcard only in the subdomain position, never in a path, and it does no prefix matching. Register …/admin and every sign-out lands on an Auth0 error page while sign-in keeps working perfectly, which is what makes it confusing.

The custom domain matters

Where the tenant has a custom domain, AUTH0_ISSUER_BASE_URL must be that domain (https://auth.example.com), not the canonical https://TENANT.REGION.auth0.com. Auth0 keeps a separate login session per domain: a vault on one while your website signs people in through the other means two cookie jars, and somebody already signed in to the site gets the full login screen again at the vault. It reads as broken SSO and it is really just two domains. Every surface on the tenant must name the same one.

Going from http to https laterUpdate both Auth0 URLs to their https:// form. The vault corrects VAULT_PUBLIC_URL for you — holding a key/cert pair, it raises an http:// public URL to https:// before handing it to Auth0, because the redirect_uri must carry the scheme the pages are served over or the callback is refused as a mismatch. The startup banner says when it did that. Change the variable anyway: the vault fixes the login, not your notes.
Part II · Running It

Part IIThe admin page

/admin is the whole of the running vault's controls, in the game's own palette. With Auth0 configured it is gated by the admin allow-list. With Auth0 not configured it is reachable only from loopback — so a local install needs no identity provider, and a remote one is never accidentally exposed.

TabWhat it is for
KeysSet, replace and remove provider keys. Leads, because a vault you have just installed does nothing at all until the Anthropic key is in.
ProvidersThe built-in descriptors, which slots each may fill, and any custom provider you author. Restricting a provider to fewer slots is done here.
UsageCalls, tokens, credits and cost per provider, with history. Aggregate only.
MediaWhat the media store holds, what each file was generated from, and a sweep for files no hosted world references.
WorldsThe realms this vault serves: download one, remove one, or tick Public to list it on a registry.
LogsThe last sixty outbound provider calls, redacted, in memory only.
SettingsServer name and description, the public listing, the model allow-lists, the access lists, and a read-only environment report.

The environment report

The fastest way to answer where are my worlds or is TLS on without a shell. Every row is the resolved value the process is using, plus whether it came from the environment or from a default — so a blank means “using the default” rather than “unset and broken”.

Secrets are set or not set, never shownNo value, no last-4, no preview. That is enforced by shape rather than by care: the read-out is built from an allow-list, so a variable nobody named is invisible. A secret variable added to the vault tomorrow does not appear on that page until somebody puts it on the allow-list.

Who may play

Three lists and a switch, and the one that surprises people is the second.

SettingMeaning
VAULT_OWNER_EMAILThe owner admin. Always an admin, and cannot be removed from the admin page by anyone — only by changing the variable. It is what guarantees a deployment can never be locked out of its own administration. Defaults to the first admin email.
VAULT_ADMIN_EMAILSWho may reach /admin. Seeds the list at boot; edits made on Settings › Access are stored and win on later starts.
VAULT_PLAYER_EMAILSWho may play, in addition to the admins and the owner.
VAULT_ANYONE_CAN_JOINIgnore the player list entirely: any authenticated user may play.
An empty player list is not “everyone”It means only the admins and the owner. The reason is the tenant: the Auth0 tenant signing people in may be one you share with a public website, in which case every account there would otherwise be a player on your vault. Opening a vault is a decision you make with VAULT_ANYONE_CAN_JOIN, which also lets a curated list be kept on file and set aside rather than deleted. Only affirmative spellings count — =0 is off.

A signed-in player is handed the access token by /vault/config exactly as a loopback client is, so the bearer-token proxy is unchanged and stays CSRF-immune. Nothing on the client changes when you turn a login on.

Never set VAULT_FIRST_USER_IS_OWNER on a serverIt means the first account to sign in owns this vault, which is right for the desktop app — a packaged install has no lists and would otherwise sign its owner in and then refuse them — and catastrophic on anything reachable from the internet, where the first stranger to arrive owns it. It applies only to a vault with no owner, no admin list and no player list, and only once.

Worlds & the registry

A published world is one pretty-printed world.json in a directory named by its uid, written by rename so a reader always sees a whole file. The vault serves it, the Worlds tab lists it, and the directory — not an index beside it — is the record.

Making one public

Ticking Public offers the world to a central registry, so that players browsing it can find you. It sends the public URL, the name and the description, and nothing else. The box stays disabled, with reasons, until the listing would actually be accepted — which mostly means VAULT_PUBLIC_URL being a real https address.

Taking one back

Unticking Public withdraws the listing — whether the registry had approved it or still had it pending review. It asks first, because a withdrawal destroys the listing and, on a registry that queues arrivals, the place in the queue with it: a realm re-published afterwards arrives pending again, behind everything that was already waiting. When you confirm, the vault records the decision and then calls the registry, and a second dialog says what actually happened — whether the realm is off the registry, or still on it and why.

That second dialog is the point. The vault writes its own flag before it calls the registry, so a call that is refused leaves the box unticked here and the listing standing there — and the box, now unticked, is no longer a control that can ask again. So a refused withdrawal grows a Withdraw & try again button in that cell, the same one a realm the registry has denied gets, and pressing it makes the same request. A world carrying a key is the common case: the registry will not unlist it on this vault's word, because the private half of the key is the author's, so the withdrawal stops at a challenge and the row offers Choose key file… to finish it. Until somebody does, the realm stays listed.

VariableEffect
REGISTRY_URLUnset, this is the project's registry and ticking Public publishes there for real. Point it at another registry to publish there, or set it to nothing (or none) to publish nowhere — the tick then records the decision and validates the descriptor. The path matters as much as the host: the base is joined to /api/worlds/… as given.
REGISTRY_TOKENThe publisher token, where that registry asks for one. A registry with no tokens configured accepts anonymous writes, which is a LAN arrangement rather than a public one.
REGISTRY_ALLOW_LOCALDevelopment only. Lets a vault publish itself at a localhost or LAN address over http, so a vault and a registry on one machine can talk. Set it on both processes — each validates independently. It does not relax https for a public address, and does not skip the origin check. Announced at boot.

The heartbeat, and the one route nobody gates

For each world marked Public the vault sends a heartbeat — shortly after boot and once a day — so listings do not lapse; a registry drops one a week after anything last confirmed it. The heartbeat carries nothing at all: no descriptor, no key, no signature. It cannot change a listing, only confirm it, which is what lets a keyed world stay listed without its author present to sign.

GET /vault/registry-proof is the single route a registry calls on your vault, and it is deliberately ungated: the asker is a registry you have no account with, arriving from an address nobody can predict, so a login gate here would mean a vault behind Auth0 could never be listed. It answers with the nonce echoed back only for a world hosted here and marked Public. Everything else is a 404 indistinguishable from “no such world”, so it cannot be used to enumerate what you hold privately. That is what stops a registry listing pointing at a server which never agreed to serve the world.

The media store

Generated art used to travel inside the save as a base64 data URI. The vault already holds those bytes — it is the thing that called the provider — so it keeps them on disk and hands back a URL instead. Files are named by the SHA-256 of their content, sharded by the first byte, so the same icon reachable from a catalogue, a pack and a room floor is one file rather than three, and regenerating an identical image is a no-op.

Each stored file also carries a provenance record: the provider, the model and a truncated prompt. That is the one place a prompt is written to disk rather than held in memory; VAULT_MEDIA_PROVENANCE=0 turns the capture off and leaves the store working.

Why it matters for publishingWith the store on, a published world is a few hundred KB of text referencing /vault/media/…. With it off, the same world carries its art inline — which still works, and grows by the size of every image on every publish. For a world that is already a file on disk, node Tools/install-world-media.js <world.json> moves the art out first.

Admin › Media lists what is held, shows what made each file, removes any of them, and can sweep every file no hosted world references. Removal refuses by default when a published world still points at the file, and names the worlds. Note that a vault can only see worlds published to it: art that a player's own browser save refers to counts as unreferenced.

Usage, cost & the call log

Every proxied call is tallied server-side: requests, tokens where the provider reports them, credits where it bills that way, and a cost in dollars. It is aggregate only — no prompts, no per-user rows — and it is the only place the whole bill is visible, since a browser's own ledger sees one browser.

Beside it, the call log holds the last sixty outbound provider calls with the resolved URL and the prompt that was sent, in memory, redacted of keys, and never written to disk. VAULT_CALL_LOG=0 turns it off; VAULT_CALL_LOG_QUIET=1 keeps it but stops it printing to the console.

What a model costs is a setting tooSettings holds the GM model allow-list — which sanctioned Claude models this vault permits — and the Nano Banana model ceiling. The client picks its per-context model within whatever you allow, so restricting the list here is how a vault caps what its players can spend on a turn.
Part III · Keeping It

Part IIIBackups & secrets

Three things are worth backing up, and they want three different treatments.

WhatTreatment
The master keyKeep it wherever you keep passwords, and not in the data directory. It is not in the key store, so a copy of that file is useless without it — which is the point, and also means losing the key loses every stored provider key. They can be re-entered; nothing else can recover them.
The worlds directoryThe thing that is actually irreplaceable. A git checkout is the good answer.
The data directoryAccess lists, settings and the media store. Worth having; none of it is unrecoverable if you still have the worlds and the keys.
Never commit thesevault-keys.json (ciphertext, but ciphertext of your money), vault-access.json (other people's email addresses), server.key, and .env. They are gitignored in this repository; if you move the data directory somewhere else, they are only as ignored as you make them.

The claims this guide makes about what is stored, logged and sent are the same ones PRIVACY.md and SECURITY.md make, and those two documents are part of any change that moves data. If you are deciding whether to run a vault for other people, read them; they are short.

Worlds in a git repo

A world is a JSON file in a directory named by its uid — a git-shaped layout already. VAULT_WORLDS_DIR is what lets you put it in an actual repository: version history for your realms, a diff for every publish, and a way to move a world between machines that is not an emailed 400 MB file.

git clone git@example.com:you/realms.git /srv/realms
VAULT_WORLDS_DIR=/srv/realms/worlds  VAULT_ACCESS_TOKEN=... npm start

Leave it unset and nothing changes — the default is exactly where worlds have always lived, so an existing install keeps every one of them with no migration. Setting the variable is the only thing that relocates anything, and moving is then a mv:

mv Server/data/worlds/* /srv/realms/worlds/
cd /srv/realms && git add worlds && git commit -m 'Import the vault worlds'

Because the directory is the record, both things git does to a directory work. A world pulled in is listed without any publish, its record rebuilt from the file — it reports no publisher, because a file cannot say who published it, and shows as adopted. A world edited or reverted on disk is re-read or dropped on the next request. Unchanged worlds cost one stat each.

Only the worlds moveNot the whole data directory. The key store and the access list do not belong in a repository, and the media store is content-addressed binary that is never rewritten in place — so every regenerated image would live in that history for ever. Both stay beside the settings file, which is why the worlds directory is a separate variable rather than a suggestion to move data/.

Upgrading

Pull, npm install if the dependencies moved, restart. Two things make that less alarming than it sounds.

The vault notices when its own source on disk has changed since the process booted and tells every connected client, which shows a “restart needed” banner. So the failure mode where you pull on the server, forget to restart, and spend an afternoon wondering why a fix did nothing is one the app announces rather than one you have to remember.

Nothing in the data directory is version-stamped or migrated on start: the key store, the access lists, the settings and the worlds are read as they are. An older world file is read by a newer vault, and the app's own backfill merges new built-in content into old saves on load rather than refusing them.

Before you restart a vault people are playing onThere is no drain and no graceful hand-off — a restart drops in-flight GM calls, and a player's turn fails with an error they will report to you. It is a second of downtime, but it is a second at a bad moment. Watch the Logs tab for a quiet minute if you have the choice.

When it will not work

The failures a vault actually produces, and what each one means. Nearly all of them are one of three things: a variable naming somewhere you are not, a URL registered somewhere that does not match, or a key that is present in a place nothing reads.

What you seeWhat it is
The server refuses to start, naming the access tokenNo VAULT_ACCESS_TOKEN, or one shorter than 16 characters. It is deliberate: an ungated GM proxy is an open relay on your Anthropic bill.
It refuses to start, naming TLSEither you bound a non-loopback host without a key and certificate, or you set one of VAULT_TLS_KEY/VAULT_TLS_CERT without the other. Neither falls back to plain http, on purpose.
“The Claude/GM key is required to play” at bootNo Anthropic key is resolvable — neither stored nor in the environment. If you entered one on the admin page, check that VAULT_MASTER_KEY is still the same value it was when you entered it: a changed master key leaves the store unreadable and the vault reports the key as stored-but-unreadable rather than pretending it is gone.
Sign-in works, sign-out lands on an Auth0 errorAllowed Logout URLs does not contain your public URL exactly. It is the base URL with no path — see the warning; /admin or /admin/logout there is the usual cause, and it never breaks sign-in, which is what makes it puzzling.
Sign-in bounces back to the login screen, or the callback is refused as a mismatchThe callback URL registered in Auth0 does not match <VAULT_PUBLIC_URL>/admin/callback — most often over the scheme, after TLS was turned on and only the vault knew.
Somebody already signed in to the website gets a full login screen at the vaultTwo Auth0 domains, so two cookie jars. Point AUTH0_ISSUER_BASE_URL at the tenant's custom domain, the same one every other surface uses.
Players are sent to a callback on their own machineVAULT_LOGIN_URL is a loopback address on a vault that gates play. The boot warning says so; the fix is to leave it unset unless you are running the tunnel arrangement it exists for.
It starts up saying Auth0 is not configuredThe gate needs four values and comes on only when it has all of them: AUTH0_ISSUER_BASE_URL, AUTH0_CLIENT_ID, AUTH0_SESSION_SECRET and VAULT_PUBLIC_URL (or VAULT_LOGIN_URL). The startup banner and Settings › Server › Environment both name the ones that are not set. The usual culprit is the fourth, because it is the only one not called AUTH0_*. AUTH0_CLIENT_SECRET is not one of them and setting it will not help — the login is PKCE against a public client and sends no secret.
The Public tick is disabledThe listing would be refused, and the page says why — nearly always VAULT_PUBLIC_URL being loopback, LAN or plain http.
You unticked Public and the realm is still on the registryThe withdrawal was refused and the dialog said so. Nearly always a keyed world: the registry needs a signature from the key file before it will unlist one, and until it gets it the listing stands. Press Withdraw & try again on that row, or Choose key file… to sign. A registry that is simply down, or a REGISTRY_TOKEN it no longer recognises, reads the same way — the dialog names which.
The desktop app will not connect to your dev vaultIt refuses an untrusted certificate by design. TLR_ALLOW_INSECURE_CERT=1 for development only, or trust a local root CA and stop needing it.
A generation fails and you want to know whyThe server console names the provider, the status and the upstream's own message. VAULT_DEBUG=1 adds a per-generation line saying whether a key was found and how the request was authorised — redacted to the last four characters.
Appendices

Appendix AEvery variable

Everything the vault reads from its environment, grouped by what it is for. A blank default means unset and unused rather than unset and broken, unless the row says otherwise.

Required, and the keys

VariableDefaultPurpose
VAULT_ACCESS_TOKEN— requiredGates POST /vault/gm and the other proxy routes. 16 characters or more, or the server will not start.
VAULT_ANTHROPIC_KEY—The Anthropic key the vault injects into GM calls.
ANTHROPIC_API_KEY—Fallback for the above, so a machine that already has the conventional variable needs nothing new.
VAULT_MASTER_KEY—32 bytes (64 hex or base64) encrypting the key store. Absent, the store is disabled and keys come from the environment only.
VAULT_KEYS_FILEServer/data/vault-keys.jsonWhere that encrypted store lives.

Where it listens, and where it is

VariableDefaultPurpose
VAULT_HOST127.0.0.1Bind address. Anything but loopback requires TLS.
VAULT_PORT8787Port.
VAULT_TLS_KEY / VAULT_TLS_CERTauto: server.key + server.certPEM paths. Both or neither; one alone is fatal.
VAULT_TLS_AUTO1Set to 0 to ignore a key/cert pair sitting beside server.js and stay on http.
VAULT_PUBLIC_URL—Where the world reaches this vault: registry listings, the origin-proof answer, and the Auth0 redirect origin.
VAULT_LOGIN_URLVAULT_PUBLIC_URLWhere Auth0 returns the browser, when that differs — the tunnel case.
VAULT_STATIC_DIRrepo rootThe directory the app is served from.
VAULT_ENV_FILEServer/.envWhere the .env loader reads from at boot.

Sign-in and access

VariableDefaultPurpose
AUTH0_ISSUER_BASE_URL—The tenant, as its custom domain where it has one.
AUTH0_CLIENT_ID—The Native (public) application's client id.
AUTH0_SESSION_SECRET—Signs this vault's own session cookie. Not an Auth0 secret; generate a random value.
AUTH0_CLIENT_SECRET—No longer used — the vault signs in with PKCE. Safe to remove.
VAULT_ADMIN_EMAILS—Comma-separated admin allow-list. Seeds the stored list, which then wins.
ADMIN_EMAILS—Fallback spelling of the above.
VAULT_OWNER_EMAILfirst admin emailThe owner: always an admin, removable only here.
VAULT_PLAYER_EMAILS—Who may play besides the admins. Empty is not “everyone”.
VAULT_ANYONE_CAN_JOINoffOpen play to any authenticated user. Affirmative spellings only.
VAULT_MODE— (shared token)credits makes every /vault/* call an account’s: a player Auth0 access token is required instead of the shared vault token, which then opens nothing. Needs AUTH0_ISSUER_BASE_URL; the vault refuses to start without it. In development, for the credits system — leave unset on an ordinary vault.
VAULT_PLAYER_AUDIENCEhttps://thelostrealms.ai/playerThe API audience a player access token must be issued for, in credits mode. A token for any other API of the tenant is refused.
VAULT_PORTAL_URLhttp://127.0.0.1:8793The credits portal this vault reserves and settles every GM call against, in credits mode. A loopback neighbour by default; unreachable means calls are refused, never made uncharged.
VAULT_PORTAL_SECRET— (required in credits mode)The service secret the portal holds (its PORTAL_SERVICE_SECRET), 32+ characters. The vault refuses to start in credits mode without it.
VAULT_ACCOUNT_URL— (credits mode, optional)Where a player who runs out of credits is sent to buy more — the Account Page. Without it the game says the player is out of credits and offers no link.
VAULT_RATE_LIMIT120Requests a minute one caller may make to /vault/, counted per signed-in session or token (not per address, so players behind one proxy do not share a budget). Media reads are not counted. 0 turns the limit off. A caller over it gets a 429 and is told to wait a moment.
VAULT_FIRST_USER_IS_OWNER—Trust on first use, for the desktop shell. Never on a server.
VAULT_ACCESS_FILEdata/vault-access.jsonWhere the edited allow-lists are stored.

Worlds, the registry and what it stores

VariableDefaultPurpose
VAULT_WORLDS_DIRworlds/ beside the settings fileWhere published worlds live. The one path worth relocating.
VAULT_SETTINGS_FILEServer/data/vault-settings.jsonServer name and description, slot overrides, model allow-lists.
VAULT_PROVIDERS_FILEServer/data/vault-providers.jsonAdmin-authored custom provider descriptors.
VAULT_USAGE_FILE / VAULT_USAGE_HISTORY_FILEServer/data/vault-usage*.jsonThe aggregate usage tallies and their history.
VAULT_MEDIA_PROVENANCEon0 stops recording what generated each stored file; the store keeps working.
VAULT_SERVER_NAME / VAULT_SERVER_DESCRIPTION—How the server introduces itself. These only seed the fields; saved edits win.
REGISTRY_URLthe project's registryWhere Public listings go. Empty or none publishes nowhere.
REGISTRY_TOKEN—Publisher token, where the registry asks for one.
REGISTRY_ALLOW_LOCALoffDevelopment only: allow a localhost/LAN listing. Set on both processes.

Diagnostics

VariableDefaultPurpose
VAULT_DEBUGoffPer-generation diagnostics on the console: whether a key was found, how the request was authorised, redacted to the last four.
VAULT_CALL_LOGon0 switches off the in-memory log of the last sixty provider calls.
VAULT_CALL_LOG_QUIEToff1 keeps the log but stops it printing to the console.
VAULT_GENERATIONS_DIRgenerations/ beside the settings fileWhere the last few raw generation artefacts are kept for diagnosis.
VAULT_GENERATIONS_KEEP10How many to keep.
This table is checked, not trustedTests/test_vault_guide.js reads the variable names out of Server/ and fails if this appendix has fallen behind them. A guide that quietly stops describing the server is worse than no guide, and a list of forty names is exactly the thing nobody notices going stale.

Appendix BRoutes

What a vault answers, and who may ask. You do not call these by hand — the app does — but knowing they exist is how you read a log line or a reverse-proxy rule.

RouteGateWhat it does
GET /vault/configopen; the token only to a loopback or signed-in clientWhat makes Vault mode detectable: the proxy URLs, the provider catalog, the model ceilings and whether a GM key is resolvable.
POST /vault/gmbearer tokenThe GM proxy. One allow-listed upstream; the client never names a URL.
POST /vault/generatebearer tokenImage, sound, music and glyph generation by provider id, run from a host-pinned descriptor.
POST /vault/video · /vault/model3d · /vault/worldgen · /vault/textbearer tokenThe four flows that are code rather than descriptors — each is a submit-then-poll or a second content type.
POST /vault/evaluatebearer tokenRuns a world evaluation server-side.
GET /vault/media/<shard>/<sha>.extopen (same-origin content)Serves a stored file. Names are computed from content, so there is no input to sanitise.
GET /vault/registry-proofungated, deliberatelyEchoes a registry's nonce for a world hosted here and marked Public; a 404 for anything else.
GET /admin and /admin/api/…admin allow-list, or loopback with Auth0 offThe admin page and everything on it. Every request is re-checked server-side.
/admin/login · /admin/callback · /admin/logout—The Auth0 round trip. The callback is the URL you register; the logout redirect returns to your public URL.
Static hosting is a denylistThe vault serves the repository, minus the directories that are source rather than app — Server/, Tests/, Designs/ and the rest — plus path traversal and dotfiles. Anything new at the top level is therefore served the moment it exists, which is why adding a top-level directory to this project is a decision with a test behind it rather than a default.

Appendix CAbout this guide

This is the operator's companion to the Dungeon Master's Guide and the in-game Field Guide: same palette, same chrome, a different job. Where they describe authoring and playing a world, this one describes the machine that serves it.

Its source of record is the repository itself — Server/README.md for the configuration, SECURITY.md and PRIVACY.md for the boundaries and what is stored, and Designs/server-vault.html, Designs/vault-media-store.html and Designs/realm-registry.html for why each is shaped the way it is. Where this guide and the server disagree, the server is right, and the disagreement is worth reporting.

What is checkedAppendix A is held to the code by Tests/test_vault_guide.js, which reads every environment variable name out of Server/ and every provider from the managed-key roster, and fails when this guide has fallen behind either. The Auth0 rows are pinned the same way, against the routes Server/identity.js actually installs — because the logout row in particular is a value that looks right in three plausible spellings and works in exactly one.

The Lost Realms · Vault Administrator's Guide · styled after the Field Guide. Copyright © 2026 Brave You Worlds, LLC. All rights reserved. This guide is Game Content, not software: licensed under LICENSE-CONTENT, with no Change Date — it does not become open source on any future date, the way the engine does. Free to read, print and use in personal or non-commercial play, permanently; commercial use needs a separate licence — licensing@braveyouworlds.com.

↑ Top