Skip to main content

ADR-0016 — Demo-Fork Frontend Strategy (the demo becomes the product)

  • Status: Accepted (founder approved 2026-06-10)
  • Date: 2026-06-10
  • Deciders: Vaisakh, Ashik
  • Related: ADR-0015 (A2A communication model), ADR-0001 client-tier scaling ADR (upsquad-client), the MVP plan "Giving Life to the Demo" (2026-06-10)

Context

demo.upsquad.ai (a mock-only Vite SPA at upsquad-client/demo/) is the approved look and information architecture. The production portal (upsquad-client/src/, Next.js + shadcn) was overhauled to match it structurally, but the look still diverges — every screen is a re-implementation that must chase the demo. The founder directive: the existing portal is not reused anywhere; the demo layout is adopted as-is.

Decision

Fork demo/app/ and wire real components into it. The demo is the spec; the fork is the product.

  1. Verbatim UI: app/'s layout, CSS, components, and copy come from demo/ unchanged. Nothing visual from the Next portal (src/) crosses over — no shadcn, no Tailwind, no layout code. Enforced by an import-boundary CI check (no imports from src/ or shadcn/tailwind packages).
  2. Only non-visual TypeScript ports from the portal: generated proto stubs (src/gen/*), the Connect transport + auth-interceptor logic, and React-Query hook logic — rewrapped behind a single data-adapter seam that mirrors the demo's access-model.jsx API. Screens talk only to the adapter.
  3. Auth: Clerk React SDK in the SPA; authZ stays fully server-side (gateway JWT → RLS → governance cascade). The Clerk webhook rehomes to the core gateway; the /api/rpc proxy is replaced by direct Connect. A compile-time VITE_DEV_MODE flag (dead-code-eliminated from prod bundles, CI-verified) allows Clerk-less dev builds using the demo persona switcher as identity.
  4. demo↔app drift rule: design iterations land in demo/ first (it remains the NDA showcase + design source); a tracked "sync to app" issue ports each pass. app/ never leads the demo visually.
  5. Serving: app/ is static + CDN-first (trivially horizontally scalable; supersedes the Next standalone/k8s scaling work, which is deferred-not-deleted). Beta host during build-out; app.upsquad.ai cutover is founder-triggered; the Next portal remains at legacy.app.upsquad.ai until its remaining surfaces (Billing/Settings/Admin/Onboarding) are re-cut in demo style post-MVP, then retires.

Consequences

  • Positive: pixel-exact by construction; one wiring seam; demo iterations flow to the product; simpler scale story; the look-and-feel divergence problem is structurally gone.
  • Negative: portal shadcn re-skin is sunk cost; its component tests don't carry over; 4 portal-only surfaces must be re-cut in demo style (deferred, post-MVP); a transition window with two frontends (beta + legacy).
  • Supersedes: Wave-5 Next-portal scaling items (client #239/#240/#242, PR #282 on hold); /api/rpc proxy bug #130. All deferred with comments, not closed.

Amendment 2026-06-12 — app hosts are live-wired applications (serving + cutover)

Recorded alongside ADR-0017 (staged promotion pipeline), founder approved 2026-06-12. This amendment supersedes the "DNS-to-Pages cutover" reading of decision point 5 (serving). The original framing left it ambiguous whether app.upsquad.ai would be a Cloudflare-Pages host like the demo. It will not.

  • demo.upsquad.ai stays on Cloudflare Pages as a static, mock-only SPA — the design source and NDA showcase. It is the only Pages host.
  • Every host downstream of demo is a live-wired application, not a static mockup. In a GCP spoke (per ADR-0017's hub-and-spoke), the SPA dist is served from GCS + CDN same-origin with the Cloud Run API behind a single load balancer — one origin, no CORS seam, the SPA and /… API routed by the same LB. There is no web-server container in the spokes; the static bundle is an LB/CDN origin, not a running Next/Node process.
  • SPA releases are promoted by release prefix, not rebuilt. The bundle for a source SHA is written once to spa-releases/<sha>/ and a spoke's live-release pointer is moved to an already-uploaded prefix (build-once / byte-identical, per ADR-0017). This replaces any notion of redeploying a server.
  • Cutover to app.upsquad.ai is the founder-triggered DNS re-point onto the prod spoke's LB (ADR-0017 prod-activation runbook), with the legacy Next portal parked at legacy.app.upsquad.ai — deferred-not-deleted, unchanged from the original decision.
  • The demo→app design-sync rule (decision point 4) is now CI-enforced via upsquad-ai/upsquad-client#314: app/ may not lead demo/ visually; the sync gate fails CI when app/ diverges from the demo design source ahead of a tracked sync pass.

Net: the "static + CDN-first" serving intent of decision point 5 holds, but the implementation is a same-origin GCS+CDN+Cloud-Run LB in a GCP spoke (not Cloudflare Pages, not a server container), and the design-sync discipline is no longer honor-system.

Appendix: service-mesh stance (recorded here to avoid a separate ADR)

Istio/service-mesh adoption is deferred. The A2A model (ADR-0015) moves agent traffic onto the message bus — meshes only help synchronous east-west RPC, and the remaining sync chain is ~6 services already covered by edge Envoy, in-code circuit breaking, OTel, and NetworkPolicies; tenant isolation is app-layer and not mesh-replaceable. Revisit tripwires:

10–15 synchronous services, multi-cluster/region, mTLS-everywhere compliance, or multi-team traffic policy — then prefer managed Cloud Service Mesh (ambient) or Linkerd over self-managed Istio.