Skip to main content

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.enabled kill-switch is flipped for the env AND the caller's org is on the per-env PREVIEW_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-wildcard domains ["*.upsquad.ai"] → cluster preview_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.ai is a *.app.upsquad.ai host (not a direct child of upsquad.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-router service joins BOTH upsquad-dev (Postgres + host-publish) and the external sbx-internal net (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:8095 only (Envoy is the sole reacher).
  • PREVIEW_HMAC_KEY MUST 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)

  1. Env-wide flag preview.enabled (DB key preview.enabled / env PREVIEW_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';
  2. 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.
  3. Per-org running kill PreviewService.EndAllPreviews (org-scoped RPC): revokes every live preview for the caller's org; the router serves 410 within 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 → 302 to 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-ancestors present on every proxied response; X-Frame-Options stripped.
  • app / research / beta-dev Host routing UNCHANGED.