Files
waggle-os/docs/redesign-warm-hive/PR7-BUILD-PLAN.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

36 KiB
Raw Permalink Blame History

Warm-Hive PR7 — Auth (Clerk, themed · screen 13) · Billing (Stripe, themed · screen 14) — Build Plan

Source design: docs/design_handoff_waggle_app/SCREENS.md §13 (Auth) + §14 (Billing) + design-files/screens/{auth,billing}.html. Roadmap: BUILD-PLAN.md §6 (PR7 = screens 13·14). Strategic gate: DESIGN_POV.md §4 (BYO-vs-metered) + BUILD-PLAN.md §7.5. Branch: TBD off main @ 3764bc13 (PR1PR6 all shipped). Status: FOUNDER-RATIFIED 2026-06-18 — D1=Option A (BYO-key + flat subscription); scope=PR7a Billing first; Clerk=optional sign-in (b), key forthcoming. Building PR7a now; PR7b deferred to next arc (needs Clerk key). D5D17 proceed as recommended. Recon: 7-reader grounded recon (pr7-recon/01..07), every claim verified vs live code with file:line. Corrections + drift in §8 (honesty log). The single most important truth in this plan: unlike PR3PR6, PR7 renders identity and money. Real auth and real payments cannot be faked — every fabrication trap (a logged-in user that isn't real, invented invoices, hardcoded payment methods, fake "next charge" dates, a custom card form that doesn't tokenize) is gated off in §5. PR7 is the highest fabrication-risk PR of the redesign.


0. STRATEGIC GATE — BYO-key vs Waggle-metered (settle this FIRST, before any feature code)

DESIGN_POV §4 (DESIGN_POV.md:62-70) flagged "who pays for inference (BYO-key vs Waggle-metered)" as the decision that "quietly reshapes Billing, Onboarding, and Usage" and "must be settled before Billing goes live" (DESIGN_POV.md:89-90; BUILD-PLAN.md:165-166). It was deferred through PR5 (where D1 ratified BYO-key for the model gate) and now becomes the literal precondition for PR7.

The de-facto current commitment is Option A — and it is already shipped, wired, and tested end-to-end:

  • BYO-key is the only inference-payment path that exists. The shared ModelGate copy is literally "Bring your own key — it's stored encrypted in your Vault and never leaves your machine" (ModelGate.tsx:186), live-validated then written to Vault (ModelGate.tsx:94-99); onboarding hard-gates Continue on a working model (ModelGateStep.tsx:49). Waggle never holds an inference key or pays a provider on the user's behalf in any shipped path.
  • Stripe is flat mode:'subscription' on both the local sidecar (checkout.ts:40, line_items qty 1, fixed price, PRO/TEAMS only) and apps/www (checkout/route.ts:170). Two products (Pro $19, Teams $49/seat), monthly+annual.
  • There is ZERO inference-metering plumbing. No createUsageRecord / billing_meter / reportUsage / credits ledger anywhere in packages/server/src (grep → 0; only a code comment at local/index.ts:1980). The only "usage" surface is TelemetryApp ("Usage & cost") reading the estimate-only /api/cost/summary with an advisory 80%/exceeded budget warning that enforces nothing (cost.ts:162-169) — a dashboard, not a quota.
Option A — Ratify BYO-key + flat subscription Option B — Pivot to Waggle-metered
Build cost Near-zero new backend. PR7 Billing = theming over the working subscription flow + minor wiring (annual toggle, deep-link, success route). Multi-month strategic arc. New per-request metering, durable usage ledger, hard quota enforcement on the inference/agent-loop path, Stripe metered prices + new webhook events, a credits/balance surface, a managed key pool (the managedModelPool flag has no inference-path impl today).
Positioning Consistent with the shipped local-first "your key never leaves your machine" promise. Contradicts that promise; adds inference COGS + abuse surface; inverts the onboarding model gate.
Blast radius PR7 theming only. Billing + Onboarding (gate inverts) + Usage (full TelemetryApp rebuild) + core inference path + Stripe webhook + product copy.

Recommendation: ratify Option A. The codebase already committed end-to-end; Option B is a product pivot PR7 must not silently absorb. The Plans copy "Memory is free forever. You only pay for scale — no feature-count games." (billing.html:130-131) leans metered but is honestly satisfiable under A (Pro/Teams unlock scale: workspaces, marketplace, sync). This is a founder strategic call, not a build choice — it must be answered before §3 / any code.


1. The design contract

  • §13 Auth (auth.html): split grid (1.05fr brand panel / 1fr form, brand panel hidden <820px, auth.html:20-22,73,89-152). 4 statesSign in (Google+Apple SSO + email/pw + "Forgot?"), Sign up (honey local-note "You don't need this to start." + Name/Email/Password "At least 10 characters"), Verify (6-box OTP, auto-advance + backspace-to-prev, auth.html:163-169), SSO/enterprise (work-email field + neutral "SAML, SCIM provisioning, audit logs available on Teams and KVARK · Talk to sales →"). Load-bearing trust copy: "An account is optional — Waggle runs fully local without one" + "Your memory stays yours; sign-in only adds sync" (auth.html:98-99). Design note SCREENS.md:277: "Build with Clerk components themed to the tokens." The top .controls segmented switcher (auth.html:78-87) is concept-harness scaffolding for previewing states — NOT product UI.
  • §14 Billing (billing.html): segmented Plans / Checkout / Success / Manage (billing.html:117-122). Plans: H1 "Upgrade your hive.", Monthly|Annual 20% toggle, 3 cards (Solo $0 "Current" disabled · Pro $19/mo honey "Most popular" → "Choose Pro" · Teams $49/seat → "Choose Teams"). Checkout: a mock in-app card form + order summary. Success: "You're Pro." + a receipt block. Manage: current plan + payment method + invoices (Paid+PDF) + change/cancel. Governing instruction SCREENS.md:294: "Use Stripe Checkout/Customer Portal where possible; theme to tokens." The card form and invoice table are fidelity mockups of Stripe's hosted surfaces, not a spec to hand-build.

2. Current state — grounded, the two-stack split is the master fact (verified vs live, 2026-06-18)

THE master fact (read before anything): there are two server stacks and PR7 builds in apps/web → the LOCAL sidecar (packages/server/src/local/index.ts), which is config.json-tier and has NO Clerk auth. The rich Clerk-linked Stripe flow + real Clerk UI live only in apps/www (Next.js, PR8) + the cloud entry packages/server/src/index.ts. Do not conflate them. (recon 01 §0, recon 02 §0, recon 05 §1-3, recon 03 §0.)

Layer Reality today (file:line) State
Local "auth" Per-process random bearer token wsSessionToken = crypto.randomBytes(32) minted at sidecar boot (local/index.ts:1409), served auth-exempt+same-origin via GET /api/auth/session-token (local/index.ts:2047-2051), enforced by token equality, no user lookup (security-middleware.ts:340-377). Adapter attaches Authorization: Bearer + 401→refresh→retry for process rotation (adapter.ts:298-337,444-470). This IS the design's "account is optional, runs fully local." REAL (not a login)
Clerk in apps/web ZERO. grep @clerk in apps/web/package.json → none; ClerkProvider/SignIn/useSignIn across apps/web/src → 0 files. No /auth route. Clean slate. ABSENT
Clerk elsewhere (REAL, not PR7's surface) Cloud server verifies Clerk JWTs (plugins/auth.ts:3,21,31-48 verifyToken→users table keyed by clerkId); apps/www has full themed Clerk UI (<SignIn/> catch-all, ClerkProvider + Hive appearance map, apps/www/app/layout.tsx:6-93). Local sidecar references Clerk only as isTeamMode = !!process.env.CLERK_SECRET_KEY to toggle a session timeout (security-middleware.ts:307-309). REAL (cloud/www only)
Desktop "identity" Free-text name from IdentityLayer (onboarding), surfaced as HomeBriefing.userName (home.ts:261-270) + sidebar row via adapter.getIdentity() degrading to "Account"/"W" when blank (AppShell.tsx:98-104,321). Identity, NOT auth — must never render as "signed in." REAL (identity ≠ account)
Stripe backend (local sidecar) stripeRoutes registered (local/index.ts:128,2161); all gate on STRIPE_SECRET_KEY503 STRIPE_NOT_CONFIGURED when absent (stripe/index.ts:25-43). create-checkout-session REAL (hosted Checkout, mode:subscription, PRO/TEAMS, period-aware, returns {url}, checkout.ts:16). create-portal-session REAL (hosted Customer Portal, requireTier('PRO'), needs config.json.stripe_customer_id else 400 NO_STRIPE_CUSTOMER, portal.ts:15,32). sync REAL + payment-gated (sync.ts:46-49), returns only {tier,customerId} (sync.ts:83). Webhook flips tiers (payment-gated, signature-verified, idempotent, TOCTOU-serialized; 17/17 tests green per CLAUDE.md §10 E-10; webhook.ts:114-158). REAL
No invoice / payment-method / sub-detail routes grep invoice/paymentMethod/invoices.list/paymentMethods in packages/server/src0 product hits. MUST-BUILD or defer to Portal
Billing FE Fully wired: adapter.createCheckoutSession/createPortalSession/syncStripeCheckout/getTier (adapter.ts:2658-2685) + useBilling hook orchestrates the happy path incl. ?session_id= auto-sync (useBilling.ts:104-115) + the tierResolved honesty guard (useBilling.ts:16-46). Settings → "Plan" tab is already the honest Manage surface: real tier badge, upgrade buttons → startCheckout, "Manage Subscription" → openPortal (SettingsApp.tsx:511-655, :621-635). REAL
Routing All 28 screens are children of one <Route path="/" element={<AppShell/>}> (App.tsx:52-110). No /auth, /billing, /payment-success routes (grep: 0). SettingsApp inits activeTab='models' with no ?tab= reader (SettingsApp.tsx:53) → /settings?tab=billing silently opens Models. Breadcrumb label sourced only from dock-tiers via matchNavRoute (AppShell.tsx:224-229). mixed
Price env contract Dual: 4-var (STRIPE_PRICE_PRO_MONTHLY/_ANNUAL/_TEAMS_*) + legacy single-var, resolved by tierFromPriceId/priceIdForTier (stripe/index.ts:71-100). Live+test Stripe accounts hold the prices per CLAUDE.md §10 M7. REAL (EXTERNAL-DEP at runtime)

Net: Screen 14 (Billing) is ~90% real — a themed re-skin of the existing useBilling→Stripe-hosted flow + four small builds (annual toggle wiring, ?tab= deep-link, /payment-success route, gated Success/Manage detail). Screen 13 (Auth) is overwhelmingly net-new + EXTERNAL-DEP + decision-gated — no Clerk SDK, no login UI, no real user session anywhere the desktop can reach.

Auth and Billing have opposite risk shapes: Billing is reskin-over-real with a localized blast radius and no new dependency; Auth is a net-new pre-shell route + an EXTERNAL Clerk-key dependency + an unproven Tauri-WebView OAuth question. Coupling them in one PR would block the de-risked Billing work behind the founder's Clerk-architecture decision and key provisioning. Recommend a 2-way split, ordered by value/risk:

Sub-PR Screen Scope theme Why this grouping Effort
PR7a — Billing themed over existing Stripe 14 Billing Reskin the Settings → Plan tab (and/or a standalone /billing) to the §14 4-state layout over the already-real useBilling→hosted-Checkout/Portal flow. Adds: annual-toggle wiring, ?tab= deep-link, /payment-success route. No new dependency. Lower risk, ships standalone, gated only on the §0 BYO/metered call (which is a copy decision under Option A, not a code blocker — the flow supports either). Hosted Checkout/Portal means zero fabricated billing data. SM
PR7b — Auth (Clerk) pre-shell route 13 Auth New top-level sibling route outside AppShell + (if Option b) @clerk/clerk-react provider themed to warm tokens, with a hard no-key → local-first accountless fallback. Higher risk + EXTERNAL-DEP-blocked (Clerk publishable key) + an architectural decision (cloud-only vs optional-sign-in vs full-gate) + an unproven Tauri-WebView OAuth spike. Isolating it keeps the Clerk dependency + the pre-shell routing risk off Billing. ML (or S if cloud-only/handoff)

Sequencing: PR7a first (de-risked, no dependency, near-zero new backend, ships the upgrade funnel). PR7b second (carries the Clerk dependency + the architecture call). Each is independently revertible. If the founder defers the Clerk-architecture decision or the key, PR7a still ships alone — Billing is not blocked by Auth.


3. Decisions — NEED FOUNDER RATIFICATION before feature code (recommend-and-proceed unless you object; D1/D2/D5 are strategic)

# Decision Recommended Why
D1 §0 BYO-key vs Waggle-metered (the strategic gate) Ratify Option A — BYO-key + flat subscription The codebase already committed end-to-end (ModelGate.tsx:186, checkout.ts:40, zero metering plumbing). Option B is a multi-month pivot that contradicts the shipped "your key never leaves your machine" promise. Founder strategic call; everything else assumes A.
D2 Clerk architecture for the desktop (cloud-only vs optional-sign-in vs full-gate) Option (b): optional Clerk in the SPA, accountless default; sign-in unlocks sync/Teams/billing. REJECT full-gate. The only option consistent with the design copy ("account is optional", auth.html:98), the accountless sidecar (local/index.ts:2047-2052, settings.ts:309-331), the existing useBilling tier flow, and the prior ratified "Tauri Clerk = Phase 2 fast-follow, NOT Day 0" (brief 2026-05-03:225). Full-gate (c) breaks accountless boot — do not build. Founder strategic call (governs the whole-SPA provider wrapping in main.tsx).
D3 Sub-PR split (§2.5) Ship the 2-way split: PR7a (Billing) → PR7b (Auth) Opposite risk shapes; coupling blocks de-risked Billing behind the Clerk decision + key. Billing has no new dependency; Auth is EXTERNAL-DEP + architecture-gated. Independently revertible. Founder structural call.
D4 Auth surface scope — browser/cloud-first vs in-WebView Tauri sign-in now Browser/cloud-first; treat in-WebView Tauri sign-in as an explicit later deep-link/OAuth spike Clerk hosted OAuth/redirect inside a Tauri WebView is unproven and the prior brief deferred it (brief:225); the apps/www browser pattern is proven. Determines whether PR7b must also solve Tauri deep-link OAuth (large, risky) or reuse the proven browser provider (small).
D5 Checkout UI — custom in-app card form vs hosted Stripe Checkout Hosted Stripe Checkout redirect (reuse createCheckoutSession); theme the "Checkout" segment as an order-summary that hands off SCREENS.md:294 mandates "use Stripe Checkout where possible"; the backend only emits a hosted URL (checkout.ts:39-51); apps/web has zero Stripe.js/PaymentElement (grep 0). A real PAN field raises PCI scope SAQ-A → SAQ-A-EP for zero functional gain. The billing.html card form is a mockup of Stripe's page, not a spec. Both a fabrication trap and a PCI trap — do not hand-build.
D6 Manage state — in-app invoice/payment-method/cancel vs hosted Customer Portal Hosted Customer Portal launchpad (reuse createPortalSession); themed current-plan header in-app, all mutations + invoices + payment method via Portal Portal natively covers payment-method update, invoice PDFs, plan change, cancel. No invoice/payment-method/sub-detail route exists (grep 0). SettingsApp.tsx:621-635 already does exactly this. Building in-app Stripe write-APIs is scope+fabrication risk we don't need. (If the founder explicitly wants in-app rendering: build new sidecar routes against stripe.invoices.list/paymentMethods.list and render strictly from live data — never placeholders.)
D7 Billing registration — standalone /billing vs Settings "Plan" tab Keep the Settings → Plan tab as primary (it's fully wired to real Stripe); reskin it to the §14 layout + add a ?tab= reader. Add a standalone /billing (PR6 wrapper pattern, AppShell child) only if design wants the full-screen funnel outside Settings chrome — reusing useBilling verbatim The Plan tab is real and tier-honest today. A ?tab=billing deep-link inherits the "Settings" breadcrumb automatically (dock-tiers.ts:108). Standalone is optional and must add its own dock-tiers route+label entry (don't repeat PR6's label-less surfaces).
D8 Monthly/Annual 20% toggle — honor at checkout vs display-only Build it: thread billingPeriod through adapter.createCheckoutSession + useBilling.startCheckout Backend priceIdForTier(tier, 'annual') already resolves the annual price (stripe/index.ts:91-100) but the adapter drops the arg (adapter.ts:2667), so it always charges monthly. A toggle that visibly changes price but charges monthly is a trust/billing bug. Small FE change; needs STRIPE_PRICE_*_ANNUAL envs set (EXTERNAL-DEP).
D9 Success state — render the receipt block vs confirmation-only Confirmation off the synced tier only ("You're Pro"); GATE OFF the receipt rows; drive any trial line from real trialDaysRemaining sync returns only {tier,customerId} (sync.ts:83) — the receipt #, "Trial ends Jun 28", "Then $19", "Emailed →" have no data source and would be fabricated. The post-redirect /payment-success URL is real (checkout.ts:42) but needs a route (D11).
D10 Plan-card prices — hardcode copy vs machine-readable source Hardcode the display copy ($0/$19/$49-seat, $15/$39 annual) from tiers.ts:7-12 in ONE constant, treat as copy not fact; never invent per-seat math beyond what tiers.ts documents Only stripePriceId is a real field in TIER_CAPABILITIES; the dollar amounts are doc-comment copy. The charged amount/tax must come from Stripe's hosted page, never asserted by our UI. Prices match billing.html exactly today.
D11 /payment-success (+ /payment-cancelled) route Add /payment-success as an AppShell child (user is back inside the app post-checkout) using the PR6 wrapper pattern; reuse useBilling's ?session_id= auto-sync checkout.ts:42 sets success_url=/payment-success?session_id=… but no such route exists (grep 0); today ?session_id= is only read on whatever page is mounted. Small MUST-BUILD.
D12 ?tab= deep-link reader in SettingsApp Add a small ?tab= initializer (read once on mount, snap activeTab); retarget ⌘K "Upgrade to Pro"/"Settings" + UpgradeModal/TrialExpiredModal fallbacks to /settings?tab=billing SettingsApp hardcodes activeTab='models' with no reader (SettingsApp.tsx:53); the existing routes.ts:52 backup deep-link is already broken the same way. Without it every upgrade entry lands on Models, not Plan.
D13 OTP fidelity (PR7b) — themed prebuilt Clerk vs bespoke 6-box widget Themed prebuilt <SignIn/>/<SignUp/> for v1 (accept Clerk's built-in code step); upgrade to useSignIn custom flow only if review demands the exact 48×56 honey OTP boxes SCREENS.md:277 literally says "Build with Clerk components themed to the tokens." Prebuilt = small; custom flow = a whole hand-built sign-in/verify state machine with its own error handling + a11y.
D14 Clerk appearance (PR7b) — shadcn theme vs full manual map shadcn theme (@clerk/themes shadcn) auto-reading the warm shadcn vars PR1 repointed (index.css:20,29,46) + a thin variables override for honey + Hanken; copy the as const gotcha avoidance from apps/www/layout.tsx:31-34 components.json exists, so theming is mostly automatic + token-driven. A full manual hex map duplicates ~60 lines/component and drifts from the token system. Copy the pattern, not the cooler apps/www hex.
D15 SSO/enterprise panel (PR7b) — live SAML vs sales CTA Keep it a custom "Talk to sales → Teams/KVARK" CTA; do NOT implement live SAML/SCIM The design panel (auth.html:148) is a sales CTA, not a live form; SAML/SCIM/audit are Teams/KVARK/Clerk-Enterprise features. Matches the KVARK funnel.
D16 EXTERNAL-DEP provisioning Founder provides VITE_CLERK_PUBLISHABLE_KEY (reuse the existing apps/www instance key) + confirms Google/Apple social connections in the Clerk dashboard; confirms the sidecar runtime env carries STRIPE_SECRET_KEY + the 4 price IDs + STRIPE_WEBHOOK_SECRET Without the Clerk key, PR7b degrades to the accountless local-first state (honest). Without the Stripe envs, all /api/stripe/* return 503 and Billing must render an honest "not configured" disabled state. See §6.
D17 Warm-token sweep In every sub-PR, sweep the touched files — Auth/Billing must use --honey/--surface/--line-strong/--r from waggle.css (already landed by PR1, BUILD-PLAN §3.1); honey button-fg #1a1407 = PR1's --primary-foreground Both screens are heavily token-dependent; one pass keeps the "single honey accent" honest. No new tokens needed.

4. Architecture

PR7a (Billing — reskin over REAL Stripe; DO NOT recreate useBilling, the adapter Stripe layer, or the server stripe/* routes)

  • Reskin SettingsApp.tsx:511-655 (Plan tab) to the §14 segmented Plans / Checkout / Success / Manage layout, warm tokens (D17). Reuse the existing tier card + useBilling + tierResolved honesty guard verbatim.
  • Plans: 3 cards from tiers.ts:7-12 copy (D10) + Monthly/Annual toggle (D8); current-plan marker from useBilling().tier honoring tierResolved; "Choose Pro/Teams" → startCheckout(tier, period).
  • Checkout segment: themed order-summary that redirects to hosted Stripe Checkout (D5) — no in-app PAN field.
  • Success: confirmation off synced tier (D9); receipt block gated off.
  • Manage: themed current-plan header + single "Manage via Stripe" → hosted Customer Portal (D6); no in-app invoice/payment-method rows.
  • Wiring (the small builds): thread billingPeriod through adapter.createCheckoutSession (adapter.ts:2667) + useBilling.startCheckout (D8); add ?tab= reader to SettingsApp (D12); add /payment-success AppShell child route via the PR6 wrapper pattern (D11); retarget ⌘K + modal fallbacks (D12). Optional standalone /billing (D7) follows the PR6 BenchmarkRoute.tsx:1-11 pattern (AppShell child).
  • Backend: none under Option A (D1) + hosted Checkout/Portal (D5/D6). Only if the founder picks in-app Manage rendering do new invoices.list/paymentMethods.list sidecar routes land → tsc -p packages/server.

PR7b (Auth — net-new pre-shell route + optional Clerk; the clerk-setup/clerk-react-patterns/clerk-custom-ui/clerk-billing skills apply)

  • /auth is the ONE pre-shell route — register as a top-level sibling OUTSIDE <Route path="/" element={<AppShell/>}> (App.tsx:52), no Sidebar/StatusBar/ChatHost/boot gate. ThemeProvider already wraps <BrowserRouter> (App.tsx:53) so warm tokens apply outside the shell. Use a plain AppErrorBoundary (onClose → window.location.reload()), NOT SurfaceBoundary (its onClose → /home assumes an authenticated home, SurfaceBoundary.tsx:13). No ⌘K entry (pre-login). No breadcrumb needed (renders outside AppShell, so StatusBar never mounts).
  • Custom layout (MUST-BUILD, no backend): split brand panel + pitch + trust lines + local-first banner + enterprise→sales CTA (D15). All copy verbatim in recon 03 §2/§4.
  • Clerk (Option b, D2): add @clerk/clerk-react + @clerk/themes; wrap <App/> in main.tsx:18 with a no-key guard — if import.meta.env.VITE_CLERK_PUBLISHABLE_KEY is undefined, render children WITHOUT ClerkProvider and show the accountless local-first state (never crash, never fabricate a user). Themed via the shadcn appearance approach (D14). Prebuilt <SignIn/>/<SignUp/> themed (D13); <UserButton/>/useUser() may re-feed the sidebar userName row (currently null→"Account").
  • getToken() seam: useAuth().getToken() returns the Clerk JWT in exactly the Bearer shape the adapter (adapter.ts:445) + cloud plugins/auth.ts:31 already consume — an optional sign-in can swap the device token for a user JWT only when the user opts into cloud/Teams, leaving the accountless local path untouched. Do NOT make Clerk the API authorizer on the local path (regresses local-first).

5. No-fabrication contract — every place PR7 could invent identity or money (gated off)

PR7 renders identity and money. Each item below has no honest data source today and must be gated off, never invented (carry the PR3/PR3.5 tierResolved/userName=null precedent):

# Fabrication trap Why it's fake Gate (the honest behavior)
F1 A logged-in identity that isn't real (name/email/avatar, "Signed in as…") The desktop has no Clerk session; "identity" is a memory-derived IdentityLayer name (AppShell.tsx:98-104). With no VITE_CLERK_PUBLISHABLE_KEY, render the accountless local-first state; the sidebar userName=null→"Account"/"W" fallback is the accepted honest UI (BUILD-PLAN §9). Only populate from a real useUser().
F2 Fake invoices / PDF receipts (Manage: "Invoice · Paid · PDF", billing.html/SCREENS.md:292) No invoice route exists (grep 0). Route to the hosted Customer Portal (openPortal); never render invented invoice rows or fake PDF links. No customer → empty/portal state, not stub rows.
F3 Fake payment method ("VISA ···4242") (SCREENS.md:291) The 4242 is literally Stripe's test PAN, design filler; no payment-method route. Render only from live stripe.paymentMethods.list (if D6 in-app path) else Portal-only; never hardcode a masked card.
F4 Fake "next charge" / renewal / billing-cycle date ("renews Jul 14", SCREENS.md:291-292) Not returned by sync (sync.ts:83) or getTier. Derive from a real subscription fetch or omit; trial dates only via real trialDaysRemaining/trialStartedAt.
F5 Itemized Success receipt ("$19 charged", receipt #, "Emailed →", billing.html) sync returns only {tier,customerId}. Generic "You're Pro" confirmation off the synced tier; receipt rows gated off (D9).
F6 Custom in-app card form (email/card 4242/expiry/CVC) Backend only emits a hosted URL; no Stripe.js in apps/web (grep 0). A real-looking PAN that doesn't tokenize is fake AND a PCI-scope trap (SAQ-A → SAQ-A-EP). Never collect raw PAN in-app; redirect to hosted Stripe Checkout (D5).
F7 Tier shown as FREE before resolution useBilling defaults to 'FREE' as a placeholder, not a fact. Honor tierResolved (useBilling.ts:16-46) — render the unresolved state, not the FREE upgrade grid, until a real getTier() round-trip succeeds.
F8 A working checkout when Stripe is unconfigured All /api/stripe/* 503 without STRIPE_SECRET_KEY (index.ts:25-43). Render an honest "not configured" disabled state, not a clickable fake "Subscribe".
F9 Fake annual price (client-side $19 × 0.8) The charged amount must match Stripe. The 20% toggle must resolve through the real annual price var (priceIdForTier(tier,'annual'), D8), not a cosmetic client number; the hosted page renders the true amount/tax.
F10 Fake SSO success ("Signed in with Google") + fake OTP verification The auth.html demo advances on any input (auth.html:138,164); SSO buttons "succeed" with no provider. SSO/OTP must perform a real Clerk redirect/verification; with no Clerk wired, disable / route to apps/www / show the local-first path — never simulate a session.
F11 SAML/SCIM rendered as a live form Enterprise panel names SAML/SCIM/audit (auth.html:148) — Teams/KVARK/Clerk-Enterprise features. Keep a sales CTA (D15); do not render a SAML form that does nothing.
F12 Usage/credits balance ("$N remaining", "X of Y tokens") Only relevant if Option B; no ledger exists, /api/cost/summary is estimate-only + advisory (cost.ts:162-169 enforces nothing). Under Option A this surface isn't built; the estimate dashboard must not be dressed up as a metered balance or imply requests are capped.
F13 Hardcoded demo identity (mara@egzakta.com / "Mara Kovač", auth.html:113,127,136; billing.html name-on-card) Pure mock placeholders. Never pre-fill or display the demo user; no real account email exists pre-Clerk.

6. External dependencies — founder must supply (EXTERNAL-DEP blockers)

Dep Needed by Blocker behavior if absent (the honest fallback)
VITE_CLERK_PUBLISHABLE_KEY (reuse existing apps/www instance key) PR7b Clerk SPA sign-in No key → /auth degrades to the accountless local-first state ("continue without an account → Home"); never a fake identity (F1/F10). Desktop default path needs no key.
Google/Apple social connections enabled in the Clerk dashboard PR7b SSO buttons Absent → SSO buttons disabled / "coming soon" / route to apps/www; never fake a session (F10).
STRIPE_SECRET_KEY + 4 price IDs (STRIPE_PRICE_{PRO,TEAMS}_{MONTHLY,ANNUAL}) + STRIPE_WEBHOOK_SECRET in the sidecar runtime env PR7a checkout/portal/sync/webhook Absent → all /api/stripe/* return 503 STRIPE_NOT_CONFIGURED; Billing renders an honest "not configured" disabled state (F8). Live+test accounts already hold the prices (CLAUDE.md §10 M7) — the secret + IDs must be present at runtime.
config.json.stripe_customer_id (written only after a real paid checkout/webhook, webhook.ts:60) PR7a Manage → Portal Absent → create-portal-session returns 400 NO_STRIPE_CUSTOMER (portal.ts:32); Manage must show the pre-subscription state, not a broken portal jump.
§0 BYO-vs-metered decision (D1) Plans copy + whether any Usage/metering surface is implied Unsettled → do not write the Plans/Usage copy that commits to metered; the flow supports either but the positioning must be chosen.
Clerk architecture decision (D2) + Tauri-WebView OAuth spike (D4) PR7b shape Unsettled → PR7b cannot start; PR7a still ships independently.

7. Phased plan (TDD; commit per phase; FE tsc -p apps/web/tsconfig.app.json + vitest each)

PR7a — Billing (gated on D1 copy decision; no new dependency)

  • A1 — Wiring prerequisites. ?tab= reader in SettingsApp (D12); thread billingPeriod through adapter + useBilling.startCheckout (D8); add /payment-success AppShell child route (D11); retarget ⌘K + UpgradeModal/TrialExpiredModal fallbacks (D12). Unit tests (deep-link snaps to Plan; checkout sends period; success route auto-syncs on ?session_id=).
  • A2 — Plan tab reskin. §14 Plans/Checkout/Success/Manage segmented layout over useBilling; warm tokens (D17); 3 cards + annual toggle (D10/D8); Checkout = hosted-redirect summary (D5); Success = confirmation only, receipt gated (D9/F5); Manage = current-plan header + Portal launchpad (D6/F2/F3/F4); honor tierResolved (F7) + 503 "not configured" state (F8). Extend SettingsApp tests (renders unresolved state; gated receipt/invoice/payment-method; honey "Most popular").
  • (optional) A3 — Standalone /billing (D7) only if design needs the full-screen funnel — PR6 wrapper pattern, reuse useBilling, add a dock-tiers route+label entry.

PR7b — Auth (gated on D2 architecture + D16 Clerk key; can defer/ship cloud-only)

  • B1 — Pre-shell /auth route + custom layout. Sibling route outside AppShell (AppErrorBoundary, not SurfaceBoundary); split brand panel + pitch + trust lines + local-first banner + enterprise sales CTA (D15); all copy verbatim. Unit test (renders outside shell; no fabricated identity with no key — F1/F13).
  • B2 — Clerk provider + themed components (Option b). @clerk/clerk-react + main.tsx wrap with no-key guard; shadcn appearance + warm override (D14); prebuilt themed <SignIn/>/<SignUp/> (D13); SSO/OTP perform real Clerk flows or degrade honestly (F10/F11). Optional getToken()→Bearer cloud-sync seam (do not make Clerk the local API authorizer). Tests (no-key → accountless; real useUser() only).

Each sub-PR ends with: adversarial review (correctness / security / honest-identity-and-money / design-fidelity) + live smoke (dark + light, 0 console errors).


8. Honesty log — every drift the recon found (verified vs live)

  • The two-stack split is the master correction. PR7 builds in apps/web → the local sidecar (config.json tier, no Clerk). The rich Clerk-linked Stripe flow + real Clerk UI are only in apps/www + the cloud server — PR8 territory, not reachable from the desktop. Conflating them would falsely assume Clerk identity + lazy-customer billing are "already available." (recon 01 §0, recon 02 §0, recon 03 §0, recon 05 §1-3.)
  • "Auth" today is a per-process machine bearer token, not a login. wsSessionToken (local/index.ts:1409) enforced by token equality (security-middleware.ts:340-377); the only "session lifecycle" is a silent 401→refresh for process rotation (adapter.ts:316-337). There is no signin/signup/logout anywhere in apps/web. LoginBriefing.tsx is the overnight-work overlay, not auth.
  • Desktop "identity" ≠ account. IdentityLayer name (AppShell.tsx:98-104) is a memory record typed in onboarding — it must never render as "signed in" (F1).
  • Billing is ~90% real and tier-honest already. useBilling + adapter + stripe/* all wired; the Settings Plan tab already does FREE/TRIAL→startCheckout and PRO/TEAMS→openPortal with the tierResolved guard. Screen 14 is a reskin, not a rebuild — the biggest risk is mistaking the real flow for net-new.
  • The billing.html card form + invoice table + "VISA ···4242" + receipt rows are fidelity MOCKUPS of Stripe's hosted surfaces, not a spec. SCREENS.md:294 says "use Stripe Checkout/Customer Portal where possible." A custom PAN form is both fabrication (F6) and a PCI-scope jump.
  • The annual toggle is a real FE build gap, not a styling gap. Backend priceIdForTier(tier,'annual') resolves the annual price (stripe/index.ts:91-100) but the adapter drops billingPeriod (adapter.ts:2667) → the design's 20% toggle would visibly change the price label while charging monthly (a billing bug) unless wired (D8/F9).
  • /settings?tab=billing silently opens Models. SettingsApp hardcodes activeTab='models' with no ?tab= reader (SettingsApp.tsx:53); the existing routes.ts:52 backup deep-link is already broken the same way. Upgrade buttons currently land on Models, not Plan (D12).
  • /payment-success is a dangling success_url. checkout.ts:42 redirects to a route that doesn't exist (grep 0); today ?session_id= is only read on whatever page happens to be mounted (D11).
  • DESIGN_POV said the design "supports either but commits to neither" (DESIGN_POV.md:70) — but the IMPLEMENTATION has since committed to Option A (BYO-key + flat subscription) end-to-end. The §0 gate is now reconciling copy with shipped reality, not choosing from scratch.
  • The auth.html top .controls segmented switcher is concept-harness scaffolding for previewing the 4 states — NOT product UI. In the real build, state is route/Clerk-flow-driven.
  • @clerk/react is in node_modules only as a transitive dep of apps/www's @clerk/nextjs — NOT an apps/web dependency. PR7b must add @clerk/clerk-react explicitly. The correct skill is clerk-react-patterns (RR6 SPA), NOT clerk-react-router-patterns (RR7 framework mode — does not apply, recon 05 §10).
  • apps/www's themed Clerk uses the OLD cooler Hive hex (#08090c/#e5a000, apps/www/layout.tsx:38-44); copy the pattern, not the hex — apps/web is warm (#14110b/#e9a52c, index.css:20,29). Copy the as const gotcha avoidance (layout.tsx:31-34).
  • No KvarkNudge component exists in apps/web/src (grep 0; CLAUDE.md §9 references it but it isn't in the web app) — the Enterprise CTA is the inline kvark.ai link (SettingsApp.tsx:644-651); the Auth enterprise panel (D15) reuses that funnel as a sales CTA.