# 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.

```bash
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
	@assets path /assets/*
	header @assets Cache-Control "public, max-age=31536000, immutable"
	@documents not path /assets/*
	header @documents Cache-Control "no-cache"
	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=()"
	}
}
```

The cache split is part of the deployment contract. Vite fingerprints files in
`/assets/`, so those responses can be immutable. The HTML shells and release
metadata are not fingerprinted and must revalidate; otherwise a browser can
keep the previous shell after an atomic deploy even though the new assets and
`RELEASE_SHA` are already live. Keep both header rules inside the static
`handle` when the same site also proxies `/api/*`, so API routes retain their
own cache policy.

`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:

```bash
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 `MediaStream`s, not
network media URLs, and signaling remains under same-origin `connect-src`.

The production policy may contain deployment-specific sources, but every one is
an operator-owned exception to this repository minimum. Audit and remove stale
font/CDN/identity origins; do not cargo-cult a broader live header back into this
template. A same-origin Tera build needs no Google Fonts or jsDelivr source.

## 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.
