Preview ingress runbook (Live Build Experience)
Operational runbook for the authenticated preview ingress: the preview-router,
the Envoy *.upsquad.ai wildcard vhost, the Cloudflare wildcard DNS + tunnel
ingress, and the kill-switches.
- ADR: ADR-0028 (D1–D8, founder-approved #2236).
- HLD: #2234 §3 (ingress routing + auth) / §8 (security model) / §12.4 (org allowlist).
- Slice: LBE-P2 (#2250). P1 serve substrate = #2242 (merged). P3 (DSL step / park) is a follow-on.
This surface is DARK by default. Nothing is reachable until BOTH the
preview.enabledkill-switch is flipped for the env AND the caller's org is on the per-envPREVIEW_ORG_ALLOWLIST. The Cloudflare change below is a founder-gated item (new wildcard DNS + tunnel ingress — ADR-0028 founder decision point 1).
1. Cloudflare wildcard DNS + tunnel ingress (founder-gated — apply by hand)
The platform fronts a single Cloudflare tunnel → host Envoy (:10000). Exact
public-hostname routes already exist for app. / research. / beta-dev.app. /
refund-mcp.app.. Preview adds ONE wildcard.
DNS record (Cloudflare zone upsquad.ai, proxied/orange-cloud):
Type: CNAME
Name: * (i.e. *.upsquad.ai)
Target: <tunnel-id>.cfargotunnel.com
Proxy: ON (orange cloud) # Universal SSL covers ONE wildcard level, no ACM spend
Tunnel ingress rule (Zero Trust → Networks → Tunnels → the app tunnel → Public Hostnames), placed BELOW the exact hostnames so they still win:
Hostname: *.upsquad.ai
Service: http://localhost:10000 # host Envoy — same as every exact route
Envoy then routes *.upsquad.ai to the preview_router cluster (see §2); the
router hard-404s any Host that is not preview-{id}.upsquad.ai (ADR-0028 D1), so
this is strictly tighter than Envoy's legacy "*" catch-all.
Access posture (ADR-0028 founder decision 2): recommend a Cloudflare Access
bypass app for preview-*.upsquad.ai (a full Access login wall would break
tenant UX; router-level handoff-token→cookie auth is the control).
Rollback: delete the tunnel ingress rule (or the DNS record). Preview hosts immediately stop resolving; no other route is affected.
2. Envoy wildcard vhost (in-repo: infra/edge/envoy.yaml)
- vhost
preview-wildcarddomains["*.upsquad.ai"]→ clusterpreview_router(127.0.0.1:8095),timeout: 0s+idle_timeout: 300s, websocket upgrade. - Envoy matches MOST-SPECIFIC domain first: exact hosts (app/research/beta-dev/
refund-mcp) ALWAYS beat the suffix-wildcard, which beats the
"*"catch-all.beta-dev.app.upsquad.aiis a*.app.upsquad.aihost (not a direct child ofupsquad.ai) so it does not even fall under the suffix — untouched. - Verify with
scripts/preview-ingress-smoke.sh(Host-routing precedence check).
3. Dev-box wiring (docker-compose.dev.yml)
preview-routerservice joins BOTHupsquad-dev(Postgres + host-publish) and the externalsbx-internalnet (upsquad-sandbox-dev_sbx-internal) to dial the served preview container by name (sbx-prev-{id}:8080). It is the ONLY bridge between the edge and the no-route sandbox net (ADR-0028 D8) — sandboxes keep no route out.- Host-published on
127.0.0.1:8095only (Envoy is the sole reacher). PREVIEW_HMAC_KEYMUST match context-engine's. Dev dummy provided; never in prod.- Bring up the sandbox stack first (the external net must exist) if exercising previews.
4. Kill-switches (ADR-0028 D7)
- Env-wide flag
preview.enabled(DB keypreview.enabled/ envPREVIEW_ENABLED, compile-default false): checked at mint + gateway mount. Flip false to stop ALL new previews instantly.INSERT INTO platform_feature_flags (key, value) VALUES ('preview.enabled', 'false')ON CONFLICT (key) DO UPDATE SET value = 'false'; - Per-org allowlist
PREVIEW_ORG_ALLOWLIST(CSV of org ids). Empty ⇒ ALL orgs denied (fail-closed). Remove an org to revoke its access to new previews. - Per-org running kill
PreviewService.EndAllPreviews(org-scoped RPC): revokes every live preview for the caller's org; the router serves410within its ≤5s cache TTL and the reaper stops the sandbox.
Revocation is bounded by the router's ≤5s session-cache TTL; the sandbox is dead after end anyway, so the residual is ≤5s of 410s/502s.
5. Fail-closed contract (what the smoke test proves)
scripts/preview-ingress-smoke.sh:
- no cookie →
302to the app mint endpoint, NEVER proxied. - handoff token single-use (2nd exchange →
401). - cross-preview cookie rejected.
- revoked ≤5s →
410. - inbound
Cookie/Authorization/CF-*/X-Forwarded-*stripped — the sandbox never sees them. frame-ancestorspresent on every proxied response;X-Frame-Optionsstripped.- app / research / beta-dev Host routing UNCHANGED.