1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/deploy/STATIC.md
T

3.8 KiB

Hosting the static build

Tera's map view is a static bundle that needs a file server and nothing else. No API, no keys, no account: the sun and moon are computed locally by src/engine/solar.ts, the traffic is simulated, the minimap is drawn from the city pack, and the office is a data file.

It does make two requests at boot, and both are meant to fail on a plain static host: GET /api/v1/health and GET /api/v1/session, which is how src/access.ts works out whether this deployment has accounts at all. Nothing answering means nothing to sign in to, so the visitor gets the full public experience and no sign-in link — see that file's header for why an unreachable API and an API that answered 5xx are deliberately not the same case. Your server log will show two 404s per load; that is the zero-config path working, not a misconfiguration.

npm ci
npm run build          # -> dist/

Serve dist/ from anything. A Content-Security-Policy of default-src 'self' is sufficient; the build references no external fonts, CDNs or images.

Caddy

tera.example.com {
	root * /var/www/tera
	encode zstd gzip
	try_files {path} {path}/index.html /index.html
	file_server
	header {
		Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'"
		# Screen sharing and webcam faces remain contextual, per-action browser
		# prompts. Same-origin permission enables those prompts; it does not grant
		# capture. Microphone capture is deliberately unavailable.
		Permissions-Policy "display-capture=(self), camera=(self), microphone=(), geolocation=(), payment=(), usb=()"
	}
}

try_files … /index.html matters if you add client-side routes later: without it a route that exists only in JavaScript 404s for anyone who types it or refreshes on it.

img-src needs data: and blob: because every texture in the asset library is drawn on a canvas at runtime rather than shipped as a file — see ARCHITECTURE.md §3.1 for why that is a licensing decision and not a technical one.

Capture-policy acceptance check

Webcam faces require a secure context and camera=(self) on both the Tera and Office hostnames. That policy only permits the application to ask: capture must still begin inside the explicit face or screen action, display a persistent active indicator, and stop on the in-product control or pagehide. Keep microphone=() because neither webcam faces nor office screens request audio.

After changing the live Caddy site, reload Caddy through the normal reviewed deployment procedure and verify both hostnames before enabling the UI:

curl -fsSI https://tera.example.com/ | grep -i '^permissions-policy:'
curl -fsSI https://office.example.com/ | grep -i '^permissions-policy:'

Both responses must contain display-capture=(self), camera=(self), and microphone=(). In browser developer tools, document.permissionsPolicy?.allowsFeature("camera") should be true on the top-level same-origin page; denying the browser prompt must leave the generated face and every office screen placeholder intact. The CSP needs no camera/media host exception: webcam and display tracks are caller-owned MediaStreams, not network media URLs, and signaling remains under same-origin connect-src.

Serving the API too

Only needed for live weather, real ADS-B, or markers from an external source. The map runs fully without it. See deploy/tera-api.service, deploy/Caddyfile.snippet and deploy/docker-compose.yml.

Base path

Deployed at a subdomain root, no configuration is needed. Under a subpath, set TERA_BASE at build time (TERA_BASE=/tera/ npm run build) — the trailing slash matters, since every asset URL resolves against it.