Runbook — Dev surface access control (/dev + /dev/docs)
Restricts the developer playground (
funday.gg/dev, SvelteKit) and the Quartz docs (funday.gg/dev/docs) to authorized Nakama users. Both were previously public and unauthenticated.
Model
One predicate decides everything: frontend/src/lib/server/auth/devAccess.ts → hasDevAccess(locals).
A request is granted dev access iff it carries a valid Nakama session token whose account either:
- has an id/username in the env allowlist (
DEV_ACCESS_USER_IDS/DEV_ACCESS_USERNAMES), or - carries
role: "developer"or"admin"(string), aroles[]containing one, ordev/developer/isDev: truein its Nakama account metadata.
- Security: the
funday-identitycookie is plaintext/forgeable, so the decision is derived fromgetAccount(token)(Nakama validates the token), never from the cookie’s claimeduser/role. Any error → deny (fail-closed). - Performance: decisions are cached per session token for 60s (Quartz docs fan out into many sub-resources, each triggering an nginx
auth_request).
Enforcement points:
| Surface | Backend | Enforced by |
|---|---|---|
/dev (+ all sub-tools) | SvelteKit :3000 | frontend/src/routes/dev/+layout.server.ts → non-devs 302 → / |
/dev/docs | SvelteKit :3000 | Same dev guard as /dev (FUNDOCS shell) |
/dev/docs/wiki/ | Quartz :8080 | nginx auth_request → /api/auth/dev-access (204 allow / 403 deny) |
/remark42 | Remark42 :8088 | Not auth_request-gated — embed.js + API must load same-origin for gated wiki pages |
Remark42 is intentionally not double-gated:
/dev/docs/wiki/HTML is already dev-only, but gating/remark42/madeembed.jsreturn 403 HTML (mislabeled as JS) so the widget never initialized (“connection denied”).
Granting access
Preferred — Nakama metadata role (no redeploy, effect ≤60s):
- Open the Nakama console → Accounts → find the user.
- Edit Metadata, add
"role": "developer"(or"admin"), save. - The user reaches
/devand/dev/docswithin ~60s (cache TTL). They must be signed into that account (not a throwaway guest) in the browser.
Bootstrap / break-glass — env allowlist (needs a frontend restart):
- Edit
/home/usr/funday/frontend/.env.production, e.g.DEV_ACCESS_USERNAMES=memeblastoise,thunderdragon(case-insensitive; ids viaDEV_ACCESS_USER_IDS). sudo systemctl restart funday-frontend
Revoking: remove the metadata role (effect ≤60s) or the allowlist entry (effect on restart).
First-time cutover (activating the gate)
The nginx gate and the /dev guard only take effect after the frontend is rebuilt. The auth_request is fail-closed: if /api/auth/dev-access is not yet deployed, /dev/docs returns 5xx (denied, never leaked) — so deploy the frontend before reloading nginx.
# 0. Review what will ship — build-atomic builds the current working tree.
cd /home/usr/funday && git status --short
# 1. Build + deploy the frontend (brings up /api/auth/dev-access + /dev guard).
bash scripts/build-atomic.sh # or: --no-restart to stage, then restart
# 2. Verify the endpoint exists (no cookie ⇒ 403, NOT 404).
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/api/auth/dev-access # → 403
# 3. Apply the gated nginx snippet (back up the live one first).
sudo cp /etc/nginx/snippets/funday-traefik-subpaths.conf \
/etc/nginx/snippets/funday-traefik-subpaths.conf.bak-$(date +%Y%m%d%H%M%S)
sudo cp /home/usr/funday/scripts/nginx/funday-traefik-subpaths.conf /etc/nginx/snippets/
sudo nginx -t && sudo systemctl reload nginx
# 4. Verify.
curl -s -o /dev/null -w '%{http_code}\n' https://funday.gg/ # → 200 (site unaffected)
curl -s -o /dev/null -w '%{http_code}\n' https://funday.gg/dev/docs/ # → 403 (no dev cookie)
# As a granted dev (browser, signed in): /dev and /dev/docs → 200.⚠️
scripts/build-atomic.shships the entire current working tree (incl. any uncommitted WIP). Confirmgit statusis in a deployable state first.
Rollback
# nginx (re-open docs immediately):
sudo cp /etc/nginx/snippets/funday-traefik-subpaths.conf.bak-<TS> /etc/nginx/snippets/funday-traefik-subpaths.conf
sudo nginx -t && sudo systemctl reload nginx
# /dev guard: delete frontend/src/routes/dev/+layout.server.ts and redeploy,
# or simply add yourself to DEV_ACCESS_USERNAMES.Residual security TODOs (tracked separately)
- Rotate every secret that was previously public on
/dev/docs/.../architecture/(Nakama console password, session/refresh encryption keys, console signing key, socket server key, runtime HTTP key, DB password). Scrubbing the doc does not rotate them. - De-hardcode the console password in
frontend/src/lib/server/nakama-presence.ts(admin:funday-...). AddNAKAMA_CONSOLE_HOST/PORT/USER/PASSto.env.productionand read them via$env/dynamic/privatelikenakama.tsdoes. - Optional: serve
/dev/docsas a statichugo --minifybuild behind nginx instead of long-runninghugo server(hugo-docs.service).