Self-host.
The map is a static bundle. npm ci, npm run build, serve dist/ — no database, no media server, no control plane, no keys, no account, and no network call at runtime. The sun is computed, the traffic is simulated, the office is a data file. Everything below this paragraph is optional.
A bundle and a file server
A Content-Security-Policy of default-src 'self' is sufficient, because the build references no external font, CDN or image; img-src needs data: and blob: only because every texture is drawn on a 2D canvas at runtime. Under a subpath, TERA_BASE=/tera/ npm run build, where the trailing slash matters because every asset URL resolves against it.
What a second process adds
Four things and nothing the map already does: live weather from a real forecast source, real ADS-B instead of the simulated plan, markers from an external source, and a login. A deployment that wants none of them runs no server. It is one Fastify process on 127.0.0.1:8431 serving /api/v1/*, with one runtime dependency and no build step — Node runs the TypeScript sources directly by stripping types at load, so a deploy is a git pull and a restart, and the floor is Node 22.18. Health, flights, weather, markers and offices are the whole route table. The API and the static build are deliberately the same origin, which is why TERA_CORS_ORIGIN defaults to empty.
A demotion, never a refusal to boot
A source configured without what it needs is demoted, not fatal: the server logs one TERA DEGRADED line, serves the fallback, and lists the demotion on /api/v1/health. TERA_PORT=banana warns and binds 8431. That rule is a CI gate rather than a sentence — one of the three jobs starts the real entry point under env -i, a genuinely empty environment, and asserts 200 from health, weather=none, auth=none, and an empty degraded array. The last is the sharp one: a demotion on an empty environment means a default was chosen that needs configuration to work.
Auth is something a deployment adds
TERA_AUTH_MODE=none is the default and means the office is open and the self-hoster never creates an account anywhere. password takes a username and a scrypt hash, with no TERA_AUTH_PASSWORD to set instead, so an operator who leaks their unit file has leaked a hash and not an account; scrypt is node:crypto's at N=16384, r=8, p=1, with the parameters recorded in the hash. password resolves to jwt internally, so a browser that signed in here and one carrying a token from an external issuer arrive through one authorisation path. jwt is HS256 by default with JWKS behind a switch; sso holds no credentials at all. Every auth misconfiguration demotes to mode=none, which makes private offices 404 to everybody including the operator.
Not shipped: a published image or a packaged release — deploy/docker-compose.yml builds from the checkout and an upgrade is a git pull and a restart, which works and is not a supported upgrade path. LiveKit presence is not integrated, so a self-hosted deployment carries no voice and needs no media server. Walk mode is not built. No marker feed is running anywhere.