Skip to main content

SPA vs Actual UI — Frontend Drift

Audience: anyone touching upsquad-client UI, or wondering why a feature they merged never showed up on beta-dev. Goal: understand that we run two frontends of the same product, why that causes silent drift, and the CI gate that now catches it.

Status legend: 🟢 LIVE (in the prod code path) · 🟡 STUBBED/partial · 🔵 DEV-ONLY (beta-dev/docker) · 🟠 PENDING/PARKED (designed, gated off, or blocked).

Changelog since 2026-07-06 (imported from Knowledge-Base Google Doc)

  • Drift is now guarded, not just documented. A per-PR frontend-parity gate exists (founder directive 2026-07-13, audit upsquad-client#564): frontend-parity.yml + scripts/check-frontend-parity.sh + parity-manifest.tsv. The base doc predated it and only described the problem. 🟢
  • Decision recorded: validate SPA↔portal parity (one-time audit + per-PR CI check) — do not consolidate the two frontends now; the GCP backend migration collapses them later. Ships as a stopgap, explicitly marked for retirement.
  • Grounded the "we got bitten" story in the actual audit finding rather than the ADR-0022 wave framing (see below).

1. The two frontends (verified)

upsquad-client builds two complete, separate frontends. Base claim confirmed against the repo:

The SPA — app/The Next portal — src/
TechVite + React (single-page app)Next.js (server-rendered)
Served atbeta-dev.app.upsquad.ai (docker + host Envoy + Cloudflare tunnel) 🟢app.upsquad.ai (Next container) 🟠
AuthCompile-time VITE_DEV_MODE bypass + persona switcher (Clerk in prod builds)Clerk (real sign-in, signup webhooks)
BackendLive adapter → /rpc → context-engine (+ mock mode for demos)Same RPCs, own generated stubs
Who uses itYou, daily — the demo + staff surfaceEffectively nobody — Clerk-gated, no one signs in

Which one is deployed and real: the app/ Vite SPA is the founder's live surface on beta-dev (docker stack, not GCP — see the beta-dev topology note). The src/ Next portal targets app.upsquad.ai but is not in daily use. Both frontend-parity.yml and parity-manifest.tsv state this in their own headers.

How it got this way (per ADR-0016, "Demo-Fork Frontend Strategy", accepted 2026-06-10): demo/ (a mock-only Vite SPA, the approved look) was forked to app/ and live-wired until it became the working product. The Next portal was overhauled to match structurally but never entered daily use. ADR-0016 amendment (2026-06-12): app.upsquad.ai cutover is a founder-triggered DNS re-point; the Next portal parks at legacy.app.upsquad.ai, deferred-not-deleted.


2. Why it's a problem (we already got bitten)

The drift is not theoretical. During a frontend wave the work was split by anchoring each page to wherever its "real" code already lived — some features landed in the SPA, others in the portal. The portal-side ones merged, deployed, and were invisible on beta-dev, because the live surface is the SPA. They then had to be re-ported into the SPA. Net effect: the team-gateway, agent-tool-access, and guardrail surfaces exist twice, in two codebases, with two sets of generated stubs. Audit #564 catalogued the divergences.

The failure mode is silent: nothing is red, CI is green, the PR merges — the feature just never appears where the founder looks.


3. Current mitigation — the per-PR parity gate 🟢

Rather than consolidate now (deferred to the GCP migration), we guard the two-frontend state with a CI gate. Three artifacts, all in upsquad-client:

ArtifactPathRole
Gate workflow.github/workflows/frontend-parity.ymlFires when either frontend, the manifest, or the script changes (paths: app/**, src/**, manifest, script).
Skip-twin.github/workflows/frontend-parity-skip.ymlReports the same frontend-parity check on out-of-path PRs so it ALWAYS reports and can be a required status check (mirrors the app-ci / docker-build skip-twin pattern, core#654).
Parity mapparity-manifest.tsvTab-separated: feature-key ⇥ app-glob ⇥ src-glob ⇥ flag.
Gate logicscripts/check-frontend-parity.shDiffs PR changed files against the manifest.

How it decides (per PR): for each gated row it computes appHit / srcHit. Touching both sides → fine. Touching neither → fine. Touching one side → VIOLATION and the PR fails — unless the PR body carries a waiver line:

parity-waiver: <feature-key> — <reason>

(The gate re-runs on PR-body edited events so adding a waiver clears the failure. The escape hatch mirrors the demo-sync-provenance provenance line.) Files matched by a grandfathered row are exempt — the gate blocks new drift only, it does not force back-filling the pre-existing gaps.

Current manifest state (verified):

flagfeature-keys
gated (parity enforced)team-gateway, agent-tool-access, approvals-triage
grandfathered (exempt, known gaps)mcp-registry, pending-binding-approval, server-edit, manager-inbox, guardrail-backend-fork

🟡 Doc-vs-manifest nit: the workflow/script headers describe "four pre-existing #564 gaps", but the manifest carries five grandfathered rows. The gate behaves off the manifest (five), so this is a stale comment, not a functional bug — worth a one-line fix on the next touch.

Remediating a gap = land the missing side, then flip that row from grandfathered to gated (or delete it) in the same PR.


4. Long-term resolution — the GCP backend migration

The parity gate is explicitly a stopgap ("retire it once the two frontends are unified" — manifest header). The permanent fix is the GCP backend migration: per ADR-0016's 2026-06-12 amendment, the SPA dist is served same-origin from GCS + CDN behind the same load balancer as the Cloud Run API in a GCP spoke — one origin, no CORS seam, no web-server container. SPA releases promote by release prefix (spa-releases/<sha>/), byte-identical, per ADR-0017. Once operations move to GCP, the Next portal's reason-to-exist (server-rendered auth surface) collapses into the same-origin SPA and the drift class is structurally gone; the Next portal retires from legacy.app.upsquad.ai.

Until then: land user-facing features on both frontends in the same PR, or file a waiver with the reason. Don't assume a portal-only merge is visible on beta-dev — it isn't.


5. Source map

  • Gate: .github/workflows/{frontend-parity,frontend-parity-skip}.yml, scripts/check-frontend-parity.sh, parity-manifest.tsv (all in upsquad-client).
  • SPA: upsquad-client/app/ (Vite). Portal: upsquad-client/src/ (Next.js). Design source: upsquad-client/demo/.
  • Decisions: ADR-0016 (demo-fork frontend strategy + 2026-06-12 serving amendment), ADR-0017 (staged promotion). Audit + directive: upsquad-client#564 (OPEN).

Reflects upsquad-core@9858af5f (2026-07-24). If the GCP migration lands or the manifest flips grandfathered→gated, update §3–§4.