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.
- Verbatim UI:
app/'s layout, CSS, components, and copy come fromdemo/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 fromsrc/or shadcn/tailwind packages). - 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'saccess-model.jsxAPI. Screens talk only to the adapter. - 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/rpcproxy is replaced by direct Connect. A compile-timeVITE_DEV_MODEflag (dead-code-eliminated from prod bundles, CI-verified) allows Clerk-less dev builds using the demo persona switcher as identity. - 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. - 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.aicutover is founder-triggered; the Next portal remains atlegacy.app.upsquad.aiuntil 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/rpcproxy 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.aistays 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
distis 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.aiis the founder-triggered DNS re-point onto the prod spoke's LB (ADR-0017 prod-activation runbook), with the legacy Next portal parked atlegacy.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 leaddemo/visually; the sync gate fails CI whenapp/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.