Files
waggle-os/docs/ux-refactor/solo-team-migration-spec-2026-07-05.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

6.1 KiB

Solo-vs-Team Migration — Ratified Spec (2026-07-05)

Founder call: collapse the funnel to Solo (free) vs Team (paid). Kill PRO. Full clean. Scout change-list: session tool-result bhgz24e6c.txt (6 areas, ~170 changes). This doc is the ratified decision layer on top of it.

Model

  • FREE tier value stays FREE (no stored-value migration), displays as "Solo" via a new TIER_LABELS map. TIERS = ['TRIAL','FREE','TEAMS','ENTERPRISE'] (PRO removed).
  • PRO removed from TIERS + TIER_CAPABILITIES. parseTier('PRO')/'basic''FREE' (LEGACY_TIER_MAP: basic:'FREE', add pro:'FREE'). Never null, never lock out a legacy sub.
  • FREE raised to old-PRO capability (6 fields): connectorLimit 5→-1, workspaceLimit 5→-1, embeddingProviders +voyage,+openai (NOT litellm), customSkills false→true, exportFormats +pdf,+json, auditLog none→basic. All TEAMS-only deltas stay off FREE (teamSkillLibrary, cloudSync, sharedWorkspaces, adminPanel, selfHosted, managedModelPool, priorityModels, teamMembersLimit:1, auditLog stays basic not full, stripePriceId:null).
  • TEAMS unchanged (paid $49/seat, keeps all collaboration/governance). ENTERPRISE/KVARK unchanged. TRIAL = 15-day Team preview → falls back to Solo.
  • New in tiers.ts: TIER_LABELS: Record<Tier,string> = {TRIAL:'Trial', FREE:'Solo', TEAMS:'Team', ENTERPRISE:'Enterprise'} + tierLabel(t) helper (single source of truth for display names).

Ratified sub-decisions (all "clean" defaults)

# Decision Ruling
1 Personal-feature server gates: marketplace install and publish, mcps install + custom, personas create/generate, install_mcp cmd, connector_fetch cron, skill-audit All → free (Solo). Remove the requireTier('PRO') gate (keep validateBody on personas).
2 Stripe portal gate Ungate to any authenticated user (requireTier('FREE')); rely on the route's own NO_STRIPE_CUSTOMER 400. A legacy PRO sub (now FREE) must still reach the portal to self-cancel.
3 Solo session cap 10 (old PRO cap). TEAMS stays 25. Set tier-session-cap.ts + settings.ts:395 identically.
4 settings.ts /api/tier flags marketplace: true, customModels: true (Solo gets BYO cloud models/embeddings). Keep kvark/governance at ENTERPRISE, cloud-sync/admin/audit-export at TEAMS.
5 Legacy PRO Stripe price → tier tierFromPriceId maps legacy PRO/BASIC price IDs → 'FREE' (keep reading the envs; only the return flips). Keep STRIPE_PRICE_PRO* envs (annotate legacy).
6 New checkout TEAMS only. checkout.ts rejects anything but TEAMS. www checkout narrows to teams; www webhook keeps accepting pro for legacy events.
7 feature-gates.ts + useFeatureGate.ts DELETE both (F31 root — kills the 'solo'/'business' vocabulary; the mapping reads the onboarding-complexity axis, not billing, so it's unfixable in place). Rewire the 2 consumers: PersonaSwitcher → personas always unlocked (remove lock scaffolding, clean); SettingsApp → canonical parseTier(billing.tier) + tierSatisfies.
8 web-only BillingTier union PRO slot Remove it fully (clean). Renumber BILLING_TIER_ORDER; replace the magic billingRank >= 3 TEAMS gate in AppShell with a named >= BILLING_TIER_ORDER.TEAMS. Coerce inbound 'PRO''FREE' in ShellContext so it never reaches the dock.
9 Cost visibility (/api/costs, /api/cost/by-workspace) TEAMS (reconcile both to one tier).
10 Settings 'enterprise' tab Stays gated at ENTERPRISE.
11 UpgradeModal comparison Trim FEATURE_ROWS to Team-differentiating caps (honest "what Team adds"). "Most popular" highlight → Team card. Enterprise stays a separate CTA block (no 3rd PlanCards column).
12 Marketplace publish Free (Solo) — personal marketplace publishing. (The team skill library is the TEAMS feature; that's a different mechanism, stays TEAMS.)

Execution partition (file-disjoint → parallel-safe)

  1. Foundation (packages/shared/src/tiers.ts only): the model change above + TIER_LABELS/ tierLabel. Then tsc --build packages/shared so dist carries the new exports (web + server tsc read shared's dist/index.d.ts). MUST land before everything else.
  2. Consumers (parallel, disjoint dirs): (a) packages/server — all gates/caps/settings-flags/ stripe/command-registry/cron per decisions 1-6,9. (b) apps/web source — delete feature-gates+ useFeatureGate, rewire PersonaSwitcher/SettingsApp, reframe all billing UI (PlanCards/UpgradeModal/ TrialExpiredModal/PaymentSuccess/Capabilities/MCPHub/useBilling/adapter/ShellContext/dock-tiers/ AppShell) to Solo/Team + TIER_LABELS, decisions 7,8,11,12. (c) apps/www — Pricing (2-tier), css grid, messages/en.json, terms, www stripe routes (decision 6).
  3. Tests + docs (after consumers): update every PRO test assertion (shared/core/server/web/e2e) to the new contract; CLAUDE.md §1 tier table (drop PRO, Free→Solo, TRIAL→Team-preview, "4-tier"), §10 M7/E-10 stripe note; .env.example legacy annotation.
  4. Verify: tsc --noEmit on shared + server + apps/web (all three — build only checks web); npm run build:packages; full vitest run; git status. Then browser QA.

Critical gotchas

  • Blast-radius asymmetry: npm run build typechecks only apps/web, whose PRO refs are all loose-string (won't break). The real tsc tripwires are in packages/shared (Tier union) and the tsx-transpiled packages/server. Always run npx tsc --noEmit -p packages/{shared,server}.
  • A missed requireTier('PRO') is worse than a wrong one: with PRO gone from TIER_ORDER, TIER_ORDER['PRO'] is undefinedn >= undefined is false403s every user (feature permanently locked). Removing PRO from the union is the deliberate tripwire that surfaces them all.
  • Two semantic reversals (FREE.workspaceLimit and connectorLimit 5→-1): their tests encode the finite value "as the whole point"; rewrite them, and drop the now-wrong 5-workspace nag UI.