6.1 KiB
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 newTIER_LABELSmap.TIERS = ['TRIAL','FREE','TEAMS','ENTERPRISE'](PRO removed). - PRO removed from
TIERS+TIER_CAPABILITIES.parseTier('PRO')/'basic'→'FREE'(LEGACY_TIER_MAP:basic:'FREE', addpro:'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 staysbasicnotfull, 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)
- Foundation (
packages/shared/src/tiers.tsonly): the model change above +TIER_LABELS/tierLabel. Thentsc --build packages/sharedsodistcarries the new exports (web + server tsc read shared'sdist/index.d.ts). MUST land before everything else. - Consumers (parallel, disjoint dirs): (a)
packages/server— all gates/caps/settings-flags/ stripe/command-registry/cron per decisions 1-6,9. (b)apps/websource — 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). - 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.examplelegacy annotation. - Verify:
tsc --noEmiton shared + server + apps/web (all three — build only checks web);npm run build:packages; fullvitest run;git status. Then browser QA.
Critical gotchas
- Blast-radius asymmetry:
npm run buildtypechecks onlyapps/web, whose PRO refs are all loose-string (won't break). The real tsc tripwires are inpackages/shared(Tier union) and the tsx-transpiledpackages/server. Always runnpx tsc --noEmit -p packages/{shared,server}. - A missed
requireTier('PRO')is worse than a wrong one: with PRO gone fromTIER_ORDER,TIER_ORDER['PRO']isundefined→n >= undefinedisfalse→ 403s every user (feature permanently locked). Removing PRO from the union is the deliberate tripwire that surfaces them all. - Two semantic reversals (
FREE.workspaceLimitandconnectorLimit5→-1): their tests encode the finite value "as the whole point"; rewrite them, and drop the now-wrong 5-workspace nag UI.