# PR7 Recon — 06 · Routing & Surfaces (Auth /auth + Billing /billing) > Topic: where `/auth` and `/billing` routes + entries live, reusing the PR1–PR6 shell > patterns. RECON ONLY — no product code touched. Every claim cites `file:line`. > Verified against `origin/main @ 3764bc13` (PR1–PR6 all shipped). --- ## TL;DR (the two registration shapes) 1. **`/auth` (screen 13) is SPECIAL — it is the ONE pre-shell, full-screen route.** Every prior warm-Hive screen mounts INSIDE the `` layout route (`App.tsx:63-101`); `/auth` must NOT. It belongs as a **sibling `` at the top level, outside the `path="/"` AppShell element** — no Sidebar, no StatusBar, no ChatHost, no boot gate. It is the only screen in the whole redesign that breaks the "child-of-AppShell" rule. 2. **`/billing` (screen 14) is NOT a new top-level route at all.** A complete Stripe billing surface ALREADY EXISTS as **Settings → "Plan" tab** (`SettingsApp.tsx:511-655`), wired to the real `useBilling` hook → real adapter Stripe calls → real server routes. PR7's billing work is **(a) reskin that existing tab to the warm tokens + the SCREENS §14 4-state layout, and (b) make it deep-linkable** (today `/settings` always opens on the Models tab and has **no `?tab=` reader** — see the breadcrumb/deep-link gap below). A standalone themed `/billing` route is OPTIONAL and only justified if the design wants the full-screen Plans/Checkout/Success/Manage flow outside the Settings chrome. --- ## 1. The route table today (`apps/web/src/App.tsx`) `App.tsx:52-110` — a SINGLE layout route owns everything: ``` }> } /> …all 28 child routes (home, workspaces, memory, …, benchmarks, platform)… } /> // App.tsx:100 ``` - **Every** screen is a child of `` (`App.tsx:63`). There is currently **no top-level route outside the shell** at all. - Imports come from the `@/routes` barrel (`App.tsx:12-39`); the catch-all `*` must stay last (`App.tsx:99` comment "ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL"). - PR6a added the two ⌘K-only static surfaces directly here: `benchmarks` (`App.tsx:97`) and `platform` (`App.tsx:98`). - **No `/auth`, `/billing`, `/login`, `/sign-in`, `/payment-success`, `/payment-cancelled` routes exist** (verified by grep — zero hits in `App.tsx` / `routes/index.ts`). --- ## 2. The PR6 route-wrapper pattern (to copy) A PR6 ⌘K-only surface is 3 small pieces. `BenchmarkRoute.tsx:1-11` is the template: ```tsx /** PR6a route wrapper — `/benchmarks` → BenchmarkApp (static, ⌘K-only surface). */ import BenchmarkApp from '@/components/os/apps/BenchmarkApp'; import SurfaceBoundary from './SurfaceBoundary'; const BenchmarkRoute = () => ( ); export default BenchmarkRoute; ``` The three pieces for any in-shell surface: 1. **`routes/Route.tsx`** — thin wrapper that renders the App component inside `` (`SurfaceBoundary.tsx:10-17` wraps in `AppErrorBoundary`, `onClose` → `navigate('/home')`). 2. **`routes/index.ts` barrel export** (`routes/index.ts:56-59` — PR6 lines). 3. **`}/>` in `App.tsx`** under the AppShell layout (`App.tsx:96-98` — the PR6a block). > **PR7 BILLING (if a standalone `/billing` is wanted)** follows this exact 3-step pattern > — a `BillingRoute` wrapper → barrel → child route under AppShell. **PR7 AUTH does NOT** — > see §4: it is a sibling route, not an AppShell child, and `SurfaceBoundary`'s > `onClose → /home` is wrong for a pre-login screen. --- ## 3. Command-catalog entries (`apps/web/src/lib/command-catalog.ts`) The ⌘K catalog is built by `buildCommandCatalog()` (`command-catalog.ts:59-118`), grouped **Jump to / Do / Power tools** (+ Pro "★ Pinned"). Entry shape `CatalogCommand` (`command-catalog.ts:22-40`): `{ id, group, name, subtitle, icon, to?, action?, meta?, minBillingRank?, pinned? }`. Each routes to a REAL `to:` or fires `action:'spawn'`. **Billing is already represented in the catalog — but points at the wrong place:** - `command-catalog.ts:75` — `Settings · "models · failover · permissions · plan"` → `to:"/settings"` - `command-catalog.ts:76` — **`"Upgrade to Pro"` · `"plans · billing · invoices"` → `to:"/settings"`** Both land on `/settings` (which opens on the **Models** tab, NOT Plan — see §6 gap). PR7 should retarget these to whatever the billing entry point becomes (`/settings?tab=billing` once a `?tab=` reader exists, or a new `/billing`). **Auth has NO catalog entry and should NOT get one** — sign-in is a pre-login full-screen route reached by redirect, not a ⌘K jump from inside the authenticated shell. The catalog's tier-gating mechanism to reuse: `gate()` (`command-catalog.ts:99-100`) filters on `minBillingRank` (FREE 0 / TRIAL 1 / PRO 2 / TEAMS 3 / ENT 4, `command-catalog.ts:36`); Pro "★ Pinned" floats `pinned:true` items (`command-catalog.ts:104-111`). Catalog is consumed in `AppShell.tsx:272-279` (`buildCommandCatalog` + `handleCatalogSelect`). --- ## 4. `/auth` — the pre-shell, full-screen registration (THE special case) ### Why it can't be an AppShell child `` (`AppShell.tsx:436-466`) wraps everything in `` → `` (`AppShell.tsx:68`), which renders the BootScreen gate, the Sidebar (`AppShell.tsx:314-323`), StatusBar (`AppShell.tsx:303-309`), ChatHost, all overlays, and the onboarding takeover (`AppShell.tsx:285-296`). A sign-in screen must show **none of that**. Mounting `/auth` as a child of `path="/"` would draw the whole authenticated chrome behind the login form. ### Recommended shape (sibling route, outside the shell) ```tsx } /> {/* NEW — sibling, pre-shell */} }> …existing 28 children… ``` - `AuthRoute` is a **standalone full-screen component** — it MUST NOT use `SurfaceBoundary` (its `onClose → navigate('/home')` assumes an authenticated home, `SurfaceBoundary.tsx:13`). Wrap in a plain `AppErrorBoundary` if any, with `onClose → window.location.reload()` (the same pattern `App.tsx:61` uses at the root). - Theme still applies: `data-theme` is set pre-paint in `main.tsx` (per BUILD-PLAN §4) and `` wraps `` at `App.tsx:53`, so `/auth` inherits warm tokens even though it's outside AppShell. Good — no extra wiring needed for theming. ### Honesty gate (CRITICAL — auth is the #1 fabrication risk) There is **no real user-identity auth in the product today.** What exists is a **same-origin local sidecar session-token** (`adapter.ts:295-308` `fetchSessionToken`, refreshed at `adapter.ts:316-331`; server route `local/index.ts:2047` `/api/auth/session-token`, auth-exempt `security-middleware.ts:238`). That is a *dev/desktop bootstrap Bearer*, NOT a logged-in human. The only "Clerk-gated" surface is the **CLOUD** server (`local/routes/agents.ts:12` comment: "`/api/agents/*` CRUD exists only on the Clerk-gated CLOUD server"), which the local app does not run. So `/auth` is **EXTERNAL-DEP / MUST-BUILD**: real Clerk components need a `CLERK_PUBLISHABLE_KEY` + the `@clerk/clerk-react` provider (neither present — zero `@clerk` imports in `apps/web/src`). **Until that wiring is real, the auth screen must NOT show a fake signed-in identity, a fake name/avatar, or pretend a session exists.** The design's own framing helps here: SCREENS §13 (`SCREENS.md:271-278`) says "An account is optional — Waggle runs fully local without one" and "Continue routes to Home." A PR7-honest auth screen can render the themed Clerk UI but, with no key configured, must degrade to the local-first "continue without an account → Home" path rather than inventing a logged-in user. (The sidebar user row already degrades to "Account" + "W" avatar when `getIdentity()` returns no name — `AppShell.tsx:98-109`, PR1 LOW #2 — so the "no real identity" state is already an accepted, non-fabricated UI.) The `clerk-setup` / `clerk-react-patterns` / `clerk-billing` skills are available for the build PR. --- ## 5. `/billing` — reuse the EXISTING Settings "Plan" tab (do not rebuild from zero) ### What is already REAL (verified, fully wired) - **Settings "Plan" tab** — `SettingsApp.tsx:41` (`{ id:'billing', label:'Plan', icon:DollarSign }`); renders at `SettingsApp.tsx:511-655` (`activeTab === 'billing'`). - **`useBilling` hook** — `hooks/useBilling.ts:24-124`: `startCheckout('PRO'|'TEAMS')` (`useBilling.ts:69-81`), `openPortal()` (`useBilling.ts:84-96`), `syncAfterCheckout` (`useBilling.ts:49-66`), auto-detects `?session_id=` on return (`useBilling.ts:104-115`). P1b honesty already baked in: `tierResolved=false` until a real `getTier()` succeeds; the default 'FREE' is NEVER shown as fact (`useBilling.ts:13-22, 38-43`; rendered unresolved state at `SettingsApp.tsx:525-536`). - **Adapter Stripe layer** — `adapter.ts:2658-2680`: `syncStripeCheckout`, `createCheckoutSession`, `createPortalSession`. - **Server Stripe routes (REAL)** — `packages/server/src/stripe/{checkout,portal,sync,webhook}.ts`. `checkout.ts` returns **`503 STRIPE_NOT_CONFIGURED`** when no `STRIPE_SECRET_KEY` (`checkout.ts:19-20`); success/cancel URLs are `/payment-success?session_id=…` and `/payment-cancelled` (`checkout.ts:42-43`). Webhook handler + 4 price-var resolution shipped per CLAUDE.md §10 (E-10). - **CoverageCompassCard** value-prop card already renders above the tier card (`SettingsApp.tsx:520`). ### What is a build GAP for the SCREENS §14 design (`SCREENS.md:282-295`) | §14 design element | Status | Evidence | |---|---|---| | Plans state (3 cards, Pro popular) | DERIVABLE — upgrade buttons exist | `SettingsApp.tsx:579-619` | | **Monthly / annual toggle (−20%)** | **MUST-BUILD on FE** — server already accepts `billingPeriod` (`checkout.ts:14,29`) but adapter `createCheckoutSession(tier)` does **not pass it** (`adapter.ts:2667`); `useBilling.startCheckout` has no period arg (`useBilling.ts:69`) | grep: zero `billingPeriod`/`annual`/`monthly` in adapter+useBilling | | Checkout state (card form) | EXTERNAL-DEP — use **Stripe Checkout** (hosted), per SCREENS §14 "Use Stripe Checkout/Customer Portal where possible" (`SCREENS.md:294`) | `checkout.ts` creates hosted `session.url` | | Success state | **MUST-BUILD route** — `success_url` points at `/payment-success` which **does not exist** as a route (grep: 0 hits in App.tsx); today only `?session_id=` is read by `useBilling.ts:104-115` on whatever page is mounted | `checkout.ts:42` vs App.tsx | | Manage state (portal, invoices, PDF) | REAL — "Manage Subscription" → `openPortal()` → Stripe Customer Portal (`SettingsApp.tsx:621-635`, `useBilling.ts:84-96`). **Invoices/PDF are inside Stripe's portal, not our UI** — do NOT render fake invoice rows in-app. | ### Recommended registration for billing - **Primary:** keep billing as the **Settings → Plan tab**, reskinned to warm tokens + the §14 segmented 4-state layout. Add the **`/payment-success`** route (and optionally `/payment-cancelled`) — these CAN be AppShell children using the PR6 wrapper pattern (the user is back inside the app post-checkout), OR a tiny standalone confirmation. Wire ⌘K `upgrade`/`settings` entries to deep-link the Plan tab. - **Optional standalone `/billing`:** only if design wants the full Plans/Checkout/Success/Manage flow outside Settings chrome. If so, follow the §2 PR6 wrapper pattern (AppShell child — billing IS post-login, unlike auth). Reuse `useBilling` verbatim; do not duplicate Stripe calls. ### BYO-vs-metered (BLOCKER — founder decision before billing ships) BUILD-PLAN §7 item 5 (`BUILD-PLAN.md:165-166`) and DESIGN_POV §4 (`DESIGN_POV.md:62-70`) flag **who pays for inference (BYO-key vs Waggle-metered)** as the decision that "quietly reshapes Billing, Onboarding, and Usage" and "should be settled before Billing goes live" (`DESIGN_POV.md:89-90`). This is a **decision gate for PR7**, not a code question — the current billing tab supports either ("supports either but commits to neither", `DESIGN_POV.md:70`). --- ## 6. The breadcrumb / `matchNavRoute` label gap (must NOT repeat for /billing) ### How the breadcrumb label is derived `AppShell.tsx:224-229`: ```ts const labelEntries = flattenAppEntries(getDockForTier('power', billingTier)); // 224 const activeRoute = matchNavRoute(location.pathname, labelEntries.map(e=>e.route)…); // 225 const surfaceLabel = labelEntries.find(e => e.route === activeRoute)?.label ?? null; // 229 ``` `surfaceLabel` is passed to `` (`AppShell.tsx:304`). `matchNavRoute` (`routes.ts:146-154`) is a longest-prefix match against the **dock-tiers route table only**. ### The gap (this is the handoff "P3 / PR6 ⌘K-only routes show a fuzzy fallback label" note) `dock-tiers.ts` `POWER_CONFIG` (`dock-tiers.ts:64-118`) does **NOT contain `/benchmarks` or `/platform`** (confirmed by grep — zero hits in dock-tiers.ts). So for those routes `matchNavRoute` returns `null` → `surfaceLabel = null` → **the StatusBar breadcrumb simply does not render** (it is gated `{focusedWindowLabel && (…)}` at `StatusBar.tsx:84`). The PR6 ⌘K-only surfaces therefore show **no breadcrumb at all** (not literally a wrong/fuzzy string — the label is null and the breadcrumb chip is hidden). Either way the surface is unlabeled in the status bar. ### Requirement for PR7 **`/billing` (and `/payment-success`, and conceptually `/auth`) MUST get a breadcrumb label so they don't repeat the unlabeled-surface gap.** The label map (`labelEntries`) is sourced ONLY from `getDockForTier(...)` — i.e. from `dock-tiers.ts`. Two options: 1. **Add a dock-tiers entry** (with `route` + `label`) for the billing surface so `matchNavRoute` resolves it — same fix PR6 should have applied to benchmarks/platform. But billing lives under `/settings` today (Settings already has a dock entry `dock-tiers.ts:108`, label "Settings"), so a `?tab=billing` deep-link inherits the "Settings" breadcrumb already — acceptable. A standalone `/billing` would need its own entry. 2. **`/auth` needs NO breadcrumb** — it renders outside AppShell (§4), so `AppShell.tsx`'s StatusBar never mounts for it. The gap is irrelevant for auth by construction. > Net: the breadcrumb gap is an AppShell-internal concern. `/auth` sidesteps it (no shell). > `/billing` should either ride the existing "Settings" entry (deep-link path) or, if > standalone, add a `dock-tiers.ts` route+label entry — do **not** ship it label-less. ### Deep-link gap that BLOCKS the "/settings?tab=billing" approach (verified) `SettingsApp` initializes `activeTab` to **`'models'`** (`SettingsApp.tsx:53`) and has **NO `?tab=` / `useSearchParams` reader** (grep: zero `tab=`/`useSearchParams`/`searchParams`/ `initialTab` in `SettingsApp.tsx`). So today `/settings?tab=billing` and even the existing `APP_ROUTES.backup = '/settings?tab=backup'` (`routes.ts:52`) **silently open on Models, not the requested tab.** For PR7 to deep-link billing from ⌘K / Upgrade buttons, SettingsApp needs a small **`?tab=` initializer** (read once on mount, snap `activeTab`). This is a real, small MUST-BUILD — without it the catalog "Upgrade to Pro" / UpgradeModal-fallback `navigate('/settings')` (`AppShell.tsx:411-413, 422`) lands a user on Models, not Plan. --- ## 7. Existing upgrade entry points (where "Upgrade"/"Manage plan" link today) | Surface | Action | Target | Evidence | |---|---|---|---| | Settings → Plan tab, FREE/TRIAL | Pro / Teams cards | `billing.startCheckout('PRO'\|'TEAMS')` → Stripe Checkout | `SettingsApp.tsx:583-598` | | Settings → Plan tab, PRO | "Upgrade to Teams" | `startCheckout('TEAMS')` | `SettingsApp.tsx:604-619` | | Settings → Plan tab, PRO/TEAMS | **"Manage Subscription"** | `billing.openPortal()` → Stripe Customer Portal | `SettingsApp.tsx:621-635` | | Settings → Plan tab, non-ENT | Enterprise CTA | external link `https://www.kvark.ai` | `SettingsApp.tsx:638-652` | | `UpgradeModal` overlay | `onUpgrade(tier)` | `adapter.createCheckoutSession(...)`, **fallback `navigate('/settings')`** | `AppShell.tsx:404-415` | | `TrialExpiredModal` overlay | `onUpgrade(tier)` | `adapter.createCheckoutSession(...)`, fallback `navigate('/settings')` | `AppShell.tsx:417-424` | | ⌘K catalog | "Upgrade to Pro" / "Settings" | `to:"/settings"` | `command-catalog.ts:75-76` | | Sidebar user row | tier label "Trial · 9d" / "Pro" | (display only; row → Settings) | `AppShell.tsx:264-269, 321-322` | All upgrade paths converge on Stripe checkout (real) or land on `/settings` (which mis-opens on Models per §6). The "Manage plan" affordance is the existing **"Manage Subscription"** button → Stripe Customer Portal. There is **no `KvarkNudge` component in `apps/web/src`** (grep: 0 hits; CLAUDE.md §9 references it but it is not in the web app today — Enterprise CTA is the inline kvark.ai link at `SettingsApp.tsx:644-651`). --- ## 8. Fabrication risks for PR7 (gate-off candidates — HONESTY CONTRACT) 1. **A logged-in identity that isn't real.** No Clerk/user-auth exists; the session-token is a local dev Bearer. The auth screen must not render a fake signed-in user/name/avatar or claim a session. Degrade to the design's local-first "continue without an account" path when no `CLERK_PUBLISHABLE_KEY` is configured. (`adapter.ts:295-308`, `local/index.ts:2047`, `AppShell.tsx:98-109` accepted "Account" fallback.) 2. **Fake invoices / PDFs.** SCREENS §14 lists "invoices (Paid + PDF)". Those live inside the **Stripe Customer Portal**, not our UI. Do NOT render invented invoice rows or fake "Download PDF" links in-app — route to `openPortal()` (`useBilling.ts:84-96`). 3. **Fake payment method ("VISA ···4242").** That `…4242` string in SCREENS §14 is design filler. Real card-on-file data lives in Stripe's portal. Do not display a hardcoded masked card in the Manage state. 4. **Fake "next charge" / usage / due-today numbers.** SCREENS §14 Checkout shows "Due today $19 / won't be charged until …". Those must come from the real Stripe session, not be string-literal'd. Prefer hosted Stripe Checkout (`checkout.ts:39-50`) which renders the real amounts itself. 5. **Presenting tier as fact before it resolves.** Already guarded by `tierResolved` (`useBilling.ts:13-22`); PR7 must preserve that — never show "Free plan" as fact while unresolved (`SettingsApp.tsx:525-536`). 6. **A fake monthly/annual price.** The −20% annual toggle is a build gap (§5); when added it must resolve through a real annual price var (server `priceIdForTier(tier, 'annual')`, `checkout.ts:29`) — not a client-side `$19 × 0.8` cosmetic number that doesn't match what Stripe charges. --- ## 9. File index (everything PR7 routing touches) | Concern | File:line | |---|---| | Route table (add `/auth` sibling, optional `/billing` child, `/payment-success`) | `apps/web/src/App.tsx:52-110` | | Route-wrapper pattern to copy | `apps/web/src/routes/BenchmarkRoute.tsx:1-11`; `routes/SurfaceBoundary.tsx:10-17` | | Barrel | `apps/web/src/routes/index.ts:33-59` | | AppId→URL table (`/auth`,`/billing` are NOT here yet; `backup` deep-link precedent) | `apps/web/src/lib/routes.ts:25-53, 70-80`; `matchNavRoute` `routes.ts:146-154` | | ⌘K catalog (retarget upgrade entry) | `apps/web/src/lib/command-catalog.ts:62-97` | | Shell breadcrumb derivation + label-source gap | `apps/web/src/components/os/AppShell.tsx:224-229, 304`; `StatusBar.tsx:84-93` | | Dock label table (no benchmarks/platform/billing entries) | `apps/web/src/lib/dock-tiers.ts:64-118` | | Existing Billing surface (REUSE) | `apps/web/src/components/os/apps/SettingsApp.tsx:36-47, 511-655` | | Billing hook (REUSE) | `apps/web/src/hooks/useBilling.ts:24-124` | | Adapter Stripe + session-token | `apps/web/src/lib/adapter.ts:2658-2680, 295-331` | | Server Stripe routes | `packages/server/src/stripe/{checkout,portal,sync,webhook}.ts` (checkout `checkout.ts:13-58`) | | Server local session-token (only "auth" today) | `packages/server/src/local/index.ts:2043-2047`; `security-middleware.ts:238` | | SCREENS specs | `docs/design_handoff_waggle_app/SCREENS.md:269-295` | | BYO-vs-metered decision gate | `docs/design_handoff_waggle_app/DESIGN_POV.md:62-70, 89-90`; `BUILD-PLAN.md:165-166` |