Skip to main content

Pricing & Stripe Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Ship a 4-tier pricing model (Free/Pro/Team/Enterprise) as a fresh plans-as-data entitlements engine, gate the right features/counts per tier, and wire Stripe Checkout + webhook so Pro/Team are self-serve. Architecture: PLANS catalog (data) + EntitlementsService.resolve()/can()/assert*() (the only code that knows the pricing model) + @RequiresFeature/FeatureGuard for on/off features + per-count asserts (mirroring the existing repo-limit pattern) for numeric ceilings. Stripe is a thin adapter on top: Checkout creates a subscription, the webhook maps price+quantity → plan+overrides and calls the same WorkspacesService.setPlan() seam an admin uses manually today. Tech Stack: NestJS (apps/api), TypeORM migrations, stripe npm SDK, React dashboard (apps/dashboard), Next.js landing (apps/web).

Global Constraints

  • Do NOT touch or merge feat/entitlements-engine. That branch was cut before Projects/Collections/Comments/Areas/Broker/Chat existed on staging — its diffs of workspaces.module.ts/workspaces.controller.ts/data-source.ts delete all of that from the entity list. It is reference-only for the EntitlementsService/FeatureGuard pattern, already reproduced correctly in Task 2-3 below against current staging.
  • Knowledge/Topics are never gated. No task in this plan touches apps/api/src/topics/**.
  • Enforcement stays OFF by default (ENTITLEMENTS_ENFORCE unset/false) through every task in this plan. Flipping it on is a deliberate, separate step after Task 14 (webhook) is verified end-to-end in staging — do not flip it as part of any task here.
  • All work happens directly on staging (no feature branch), per this repo’s existing workflow — commit after each task’s tests pass.
  • Every new migration file must be registered in libs/db/src/data-source.ts (import + entities array if a new entity + migrations array) in the SAME commit as the migration, or prod breaks on next deploy.
  • New TypeORM entities/columns used at runtime must be registered in apps/api/src/app.module.ts’s forRootAsync entities list too, not just data-source.ts — that’s the runtime wiring; missing it is a silent 500 “No metadata” that tests won’t catch since they exercise the compiled path differently. Check app.module.ts for a Workspace reference — if entities are already imported as a shared list from @driftless/db, no new entry is needed for a column-only migration (only new entities need adding).
  • Never commit a real Stripe secret. STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET are read from env only; the repo’s secret-scan.ts will already block a literal sk_live_/whsec_ value if one is accidentally pasted into a topic — the same discipline applies to code/config committed here.

Task 1: Plans catalog + workspace.plan migration

Files:
  • Create: apps/api/src/entitlements/plans.catalog.ts
  • Create: apps/api/src/entitlements/plans.catalog.spec.ts
  • Create: libs/db/src/migrations/1715200000102-AddWorkspacePlan.ts
  • Modify: libs/db/src/entities/workspace.entity.ts
  • Modify: libs/db/src/data-source.ts
Interfaces:
  • Produces: PlanId = 'free' | 'pro' | 'team' | 'enterprise', FeatureKey = 'byom_chat' | 'broker', PlanDef { seats, workspaces, repos, projects, collections: number | null; features: Record<FeatureKey, boolean> }, PLANS: Record<PlanId, PlanDef>, isPlanId(value: unknown): value is PlanId, planIdOrFree(value: unknown): PlanId. Task 2 imports all of these from ./plans.catalog.
  • Step 1: Write the failing test
  • Step 2: Run test to verify it fails
Run: cd apps/api && npx vitest run src/entitlements/plans.catalog.spec.ts Expected: FAIL — Cannot find module './plans.catalog'
  • Step 3: Write the catalog
  • Step 4: Run test to verify it passes
Run: cd apps/api && npx vitest run src/entitlements/plans.catalog.spec.ts Expected: PASS (7 tests)
  • Step 5: Add the plan column — migration
  • Step 6: Register the migration in data-source.ts
In libs/db/src/data-source.ts, add the import next to the other 102x-range imports:
Append AddWorkspacePlan1715200000102 to the end of the migrations: [...] array (after AddProviderCredentialModels1715200000101).
  • Step 7: Add the entity column
In libs/db/src/entities/workspace.entity.ts, add after clerk_org_id:
  • Step 8: Run the API build to confirm no type errors
Run: cd apps/api && npx tsc --noEmit Expected: no errors referencing workspace.entity.ts or data-source.ts
  • Step 9: Commit

Task 2: EntitlementsService

Files:
  • Create: apps/api/src/entitlements/entitlements.service.ts
  • Create: apps/api/src/entitlements/entitlements.service.spec.ts
  • Modify: apps/api/src/ops/error-codes.ts
Interfaces:
  • Consumes: PLANS, planIdOrFree, FeatureKey, PlanDef, PlanId from ./plans.catalog (Task 1); Workspace, WorkspaceMember, Repo, Project, Collection from @driftless/db; DomainException, ErrorCode from ../ops/error-codes.
  • Produces: EntitlementsService with resolve(workspaceId), can(workspaceId, feature), assertFeature(workspaceId, feature), assertSeatAvailable(workspaceId), assertRepoAvailable(workspaceId), assertProjectAvailable(workspaceId), assertCollectionAvailable(workspaceId), assertWorkspaceAvailable(ownerClerkUserId). Tasks 3-10 inject and call these.
  • Step 1: Add the two error codes
In apps/api/src/ops/error-codes.ts, add inside the ErrorCode object, after UNKNOWN_TOPIC:
  • Step 2: Write the failing test
  • Step 3: Run test to verify it fails
Run: cd apps/api && npx vitest run src/entitlements/entitlements.service.spec.ts Expected: FAIL — Cannot find module './entitlements.service'
  • Step 4: Write the service
  • Step 5: Run test to verify it passes
Run: cd apps/api && npx vitest run src/entitlements/entitlements.service.spec.ts Expected: PASS (6 tests)
  • Step 6: Commit

Task 3: FeatureGuard + decorator + module + setPlan endpoint

Files:
  • Create: apps/api/src/entitlements/require-feature.decorator.ts
  • Create: apps/api/src/entitlements/feature.guard.ts
  • Create: apps/api/src/entitlements/entitlements.module.ts
  • Modify: apps/api/src/workspaces/workspaces.module.ts
  • Modify: apps/api/src/workspaces/workspaces.service.ts
  • Modify: apps/api/src/workspaces/workspaces.controller.ts
  • Test: apps/api/src/workspaces/workspaces.service.spec.ts (extend if it exists, else create)
Interfaces:
  • Consumes: EntitlementsService (Task 2), FeatureKey (Task 1).
  • Produces: @RequiresFeature(key) decorator + FeatureGuard, EntitlementsModule (exports EntitlementsService, FeatureGuard), WorkspacesService.setPlan(workspaceId, plan, overrides?). Tasks 4-10 import EntitlementsModule into their own module and inject EntitlementsService.
  • Step 1: Decorator + guard (no test — these are declarative wiring, covered by Task 3’s endpoint test and Tasks 9-10’s feature-gate tests)
  • Step 2: Write the failing test for setPlan
Add to apps/api/src/workspaces/workspaces.service.spec.ts (create the file with this one test + minimal harness if it doesn’t already exist — check first with ls apps/api/src/workspaces/*.spec.ts; if workspaces.service.spec.ts exists, add this describe block alongside the existing ones and reuse its repo mocks):
  • Step 3: Run test to verify it fails
Run: cd apps/api && npx vitest run src/workspaces/workspaces.service.spec.ts -t setPlan Expected: FAIL — service.setPlan is not a function
  • Step 4: Add setPlan to WorkspacesService
In apps/api/src/workspaces/workspaces.service.ts, add after findById:
Add the import at the top: import { isPlanId } from '../entitlements/plans.catalog'. Confirm BadRequestException/NotFoundException are already imported (they are, per the existing file).
  • Step 5: Run test to verify it passes
Run: cd apps/api && npx vitest run src/workspaces/workspaces.service.spec.ts -t setPlan Expected: PASS (2 tests)
  • Step 6: Add the admin endpoint
In apps/api/src/workspaces/workspaces.controller.ts, add after the existing update (PATCH :slug) method — inject ConfigService in the constructor if not already present:
Add ConfigService to the constructor (private readonly config: ConfigService,) and import { ConfigService } from '@nestjs/config'. Add BadRequestException to the existing @nestjs/common import if missing.
  • Step 7: Wire EntitlementsModule into WorkspacesModule
In apps/api/src/workspaces/workspaces.module.ts, add the import and add EntitlementsModule to imports (keep every existing import/provider — this module still needs OAuthModule/ApiKeyService for the owner-resolution path, unlike the stale branch):
  • Step 8: Run the full workspaces test file + build
Run: cd apps/api && npx vitest run src/workspaces/ && npx tsc --noEmit Expected: all PASS, no type errors
  • Step 9: Commit

Task 4: Gate repo connection (Free = 1 repo)

Files:
  • Modify: apps/api/src/repos/repos.service.ts
  • Modify: apps/api/src/repos/repos.service.spec.ts
Interfaces:
  • Consumes: EntitlementsService.assertRepoAvailable(workspaceId) (Task 2), already exported via WorkspacesModule which owns ReposService.
  • Step 1: Write the failing test
Add to apps/api/src/repos/repos.service.spec.ts (find its existing DI-mock setup for ReposService and add an EntitlementsService mock alongside):
(Match this file’s existing mocking convention exactly — if it uses Test.createTestingModule, add { provide: EntitlementsService, useValue: { assertRepoAvailable: assertSpy } } to the providers array used in this file’s beforeEach.)
  • Step 2: Run test to verify it fails
Run: cd apps/api && npx vitest run src/repos/repos.service.spec.ts -t assertRepoAvailable Expected: FAIL — either a DI error (no EntitlementsService provider) or the spy never called
  • Step 3: Wire the gate
In apps/api/src/repos/repos.service.ts, add the import and constructor param:
In create(), add as the first line of the method body:
  • Step 4: Run test to verify it passes
Run: cd apps/api && npx vitest run src/repos/repos.service.spec.ts Expected: PASS (all tests, including the new one)
  • Step 5: Commit

Task 5: Gate workspace creation (Pro = 3, Free = 1)

Files:
  • Modify: apps/api/src/workspaces/workspaces.controller.ts
  • Modify: apps/api/src/workspaces/workspaces.controller.spec.ts (or equivalent — check for an existing workspaces.controller.spec.ts; if none exists, add this test to workspaces.service.spec.ts targeting create() instead, since create() doesn’t move)
Interfaces:
  • Consumes: EntitlementsService.assertWorkspaceAvailable(ownerClerkUserId) (Task 2, already implemented — unused until now).
  • Step 1: Write the failing test
  • Step 2: Run test to verify it fails
Run: cd apps/api && npx vitest run src/workspaces/ -t assertWorkspaceAvailable Expected: FAIL — spy not called
  • Step 3: Wire the gate
In apps/api/src/workspaces/workspaces.controller.ts, inject EntitlementsService in the constructor and call it in create() before this.workspacesService.create(...):
(Keep the rest of the existing create() body — the API-key/OAuth owner-resolution fallbacks already there — this only adds the assertWorkspaceAvailable call right after ownerClerkId is resolved from whichever source found it. Add private readonly entitlements: EntitlementsService, to the constructor and import it.)
  • Step 4: Run test to verify it passes
Run: cd apps/api && npx vitest run src/workspaces/ Expected: PASS
  • Step 5: Commit

Task 6: Gate seat invitations (Team = 5)

Files:
  • Modify: apps/api/src/invitations/invitations.service.ts
  • Modify: apps/api/src/invitations/invitations.module.ts
  • Modify: apps/api/src/invitations/invitations.service.spec.ts
Interfaces:
  • Consumes: EntitlementsService.assertSeatAvailable(workspaceId) (Task 2).
  • Step 1: Write the failing test
Add to apps/api/src/invitations/invitations.service.spec.ts:
  • Step 2: Run test to verify it fails
Run: cd apps/api && npx vitest run src/invitations/invitations.service.spec.ts -t assertSeatAvailable Expected: FAIL
  • Step 3: Wire EntitlementsModule + the gate
In apps/api/src/invitations/invitations.module.ts, add the import and add to imports:
In apps/api/src/invitations/invitations.service.ts, add private readonly entitlements: EntitlementsService, to the constructor (import it), and in invite(), add right after the role validation (before the workspace lookup):
  • Step 4: Run test to verify it passes
Run: cd apps/api && npx vitest run src/invitations/invitations.service.spec.ts Expected: PASS
  • Step 5: Commit

Task 7: Gate project creation (Free = 5, else unlimited)

Files:
  • Modify: apps/api/src/projects/projects.service.ts
  • Modify: apps/api/src/projects/projects.module.ts
  • Modify: apps/api/src/projects/projects.service.spec.ts
Interfaces:
  • Consumes: EntitlementsService.assertProjectAvailable(workspaceId) (Task 2).
  • Step 1: Write the failing test
  • Step 2: Run test to verify it fails
Run: cd apps/api && npx vitest run src/projects/projects.service.spec.ts -t assertProjectAvailable Expected: FAIL
  • Step 3: Wire EntitlementsModule + the gate
In apps/api/src/projects/projects.module.ts:
In apps/api/src/projects/projects.service.ts, add private readonly entitlements: EntitlementsService, to the constructor (import it), and in create(), add as the first line of the method body (before resolveOwner):
  • Step 4: Run test to verify it passes
Run: cd apps/api && npx vitest run src/projects/projects.service.spec.ts Expected: PASS
  • Step 5: Commit

Task 8: Gate collection creation (Free = 5, else unlimited)

Files:
  • Modify: apps/api/src/collections/collections.service.ts
  • Modify: apps/api/src/collections/collections.module.ts
  • Modify: apps/api/src/collections/collections.service.spec.ts
Interfaces:
  • Consumes: EntitlementsService.assertCollectionAvailable(workspaceId) (Task 2).
  • Step 1: Write the failing test
  • Step 2: Run test to verify it fails
Run: cd apps/api && npx vitest run src/collections/collections.service.spec.ts -t assertCollectionAvailable Expected: FAIL
  • Step 3: Wire EntitlementsModule + the gate
In apps/api/src/collections/collections.module.ts:
In apps/api/src/collections/collections.service.ts, add private readonly entitlements: EntitlementsService, to the constructor (import it), and in create(), add as the first line of the method body:
  • Step 4: Run test to verify it passes
Run: cd apps/api && npx vitest run src/collections/collections.service.spec.ts Expected: PASS
  • Step 5: Commit

Task 9: Gate byom_chat (Chat threads + Auditor/Architect)

Files:
  • Modify: apps/api/src/agent-runs/agent-config.service.ts
  • Modify: apps/api/src/agent-runs/agent-runs.module.ts
  • Modify: apps/api/src/agent-runs/agent-config.service.spec.ts
  • Modify: apps/api/src/chat/chat.controller.ts
  • Modify: apps/api/src/chat/chat.controller.spec.ts
Interfaces:
  • Consumes: EntitlementsService.assertFeature(workspaceId, 'byom_chat') (Task 2).
  • Step 1: Write the failing test — agent config
Add to apps/api/src/agent-runs/agent-config.service.spec.ts:
  • Step 2: Run test to verify it fails
Run: cd apps/api && npx vitest run src/agent-runs/agent-config.service.spec.ts -t byom_chat Expected: FAIL
  • Step 3: Wire EntitlementsModule + the gate
In apps/api/src/agent-runs/agent-runs.module.ts:
In apps/api/src/agent-runs/agent-config.service.ts, add private readonly entitlements: EntitlementsService, to the constructor (import it), and in update(), add right after the ws lookup (before building next):
  • Step 4: Run test to verify it passes
Run: cd apps/api && npx vitest run src/agent-runs/agent-config.service.spec.ts Expected: PASS
  • Step 5: Write the failing test — chat
Add to apps/api/src/chat/chat.controller.spec.ts:
  • Step 6: Run test to verify it fails
Run: cd apps/api && npx vitest run src/chat/chat.controller.spec.ts -t byom_chat Expected: FAIL
  • Step 7: Wire the gate in chat.controller.ts
ChatModule already imports AgentRunsModule, but EntitlementsService isn’t exported from there — add EntitlementsModule directly to ChatModule’s imports too:
In apps/api/src/chat/chat.controller.ts, inject EntitlementsService in the constructor (import it), and in createThread, add right after assertHasIdentity(principal):
  • Step 8: Run test to verify it passes
Run: cd apps/api && npx vitest run src/chat/chat.controller.spec.ts Expected: PASS
  • Step 9: Commit

Task 10: Gate broker (integrations — Team+)

Files:
  • Modify: apps/api/src/broker/broker.controller.ts
  • Modify: apps/api/src/broker/broker.module.ts
  • Modify: apps/api/src/broker/broker.controller.spec.ts (or the closest existing broker controller test file)
Interfaces:
  • Consumes: EntitlementsService.assertFeature(workspaceId, 'broker') (Task 2). Distinct from assertBrokerEnabled (env-level kill switch, broker-feature.ts) — both checks apply, in either order; this task adds the plan check alongside the existing env check.
  • Step 1: Write the failing test
  • Step 2: Run test to verify it fails
Run: cd apps/api && npx vitest run src/broker/ -t 'gates on the broker plan feature' Expected: FAIL
  • Step 3: Wire EntitlementsModule + the gate
In apps/api/src/broker/broker.module.ts:
In apps/api/src/broker/broker.controller.ts, inject EntitlementsService in the constructor (import it), and in createGrant, add right after assertBrokerEnabled(this.config):
  • Step 4: Run test to verify it passes
Run: cd apps/api && npx vitest run src/broker/ Expected: PASS
  • Step 5: Commit

Task 11: Stripe SDK, config, and product/price seed script

Files:
  • Modify: apps/api/package.json (add stripe dependency)
  • Create: apps/api/src/billing/stripe-client.ts
  • Create: apps/api/scripts/stripe-seed-products.ts
Interfaces:
  • Produces: getStripeClient(config: ConfigService): Stripe — Tasks 12-13 use this. STRIPE_PRICE_PRO_MONTHLY, STRIPE_PRICE_TEAM_BASE_MONTHLY, STRIPE_PRICE_TEAM_SEAT_EXTRA, STRIPE_PRICE_TEAM_WORKSPACE_EXTRA env vars (populated by the seed script’s output) — Task 12 reads these.
  • Step 1: Add the dependency
Run: cd apps/api && npm install stripe Expected: stripe added to apps/api/package.json dependencies
  • Step 2: Stripe client factory
  • Step 3: Seed script (idempotent — safe to re-run)
  • Step 4: Run the build to confirm no type errors
Run: cd apps/api && npx tsc --noEmit Expected: no errors
  • Step 5: Commit
Manual step (not code — do after this task, before Task 12): run the seed script against Stripe test mode first (STRIPE_SECRET_KEY=sk_test_...), verify the 4 prices appear in the Stripe dashboard, and set the 4 STRIPE_PRICE_* env vars in the API’s env for local/staging testing. Re-run in live mode only once Task 13’s webhook is verified end-to-end in staging.

Task 12: Checkout session endpoint

Files:
  • Create: apps/api/src/billing/billing.module.ts
  • Create: apps/api/src/billing/billing.controller.ts
  • Create: apps/api/src/billing/billing.service.ts
  • Create: apps/api/src/billing/billing.service.spec.ts
  • Modify: apps/api/src/app.module.ts (register BillingModule)
Interfaces:
  • Consumes: getStripeClient (Task 11), WorkspacesService.findById (existing).
  • Produces: BillingService.createCheckoutSession(workspaceId, tier: 'pro' | 'team', extraSeats?: number, extraWorkspaces?: number): Promise<{ url: string }>. Task 15 (dashboard) calls POST /billing/checkout-session.
  • Step 1: Write the failing test
  • Step 2: Run test to verify it fails
Run: cd apps/api && npx vitest run src/billing/billing.service.spec.ts Expected: FAIL — Cannot find module './billing.service'
  • Step 3: Write the service
  • Step 4: Run test to verify it passes
Run: cd apps/api && npx vitest run src/billing/billing.service.spec.ts Expected: PASS (2 tests)
  • Step 5: Controller + module
Register in apps/api/src/app.module.ts: add import { BillingModule } from './billing/billing.module' and add BillingModule to the root module’s imports array (find the array holding WorkspacesModule, ProjectsModule, etc. and append it there).
  • Step 6: Run the API build
Run: cd apps/api && npx tsc --noEmit Expected: no errors
  • Step 7: Commit

Task 13: Stripe webhook — checkout completed / subscription updated/deleted

Files:
  • Create: apps/api/src/billing/billing-webhook.controller.ts
  • Create: apps/api/src/billing/billing-webhook.service.ts
  • Create: apps/api/src/billing/billing-webhook.service.spec.ts
  • Modify: apps/api/src/billing/billing.module.ts (add the new controller/service)
  • Modify: apps/api/src/main.ts (raw-body parsing for /webhooks/stripe, since Stripe signature verification needs the raw payload — check how apps/api/src/main.ts sets up the body parser today; NestJS’s default express.json() consumes the body before the controller sees it, so this route needs rawBody: true on the Nest factory or a route-specific raw parser)
Interfaces:
  • Consumes: getStripeClient (Task 11), WorkspacesService.setPlan (Task 3).
  • Produces: POST /webhooks/stripe — separate from apps/api/src/webhooks (GitHub-specific).
  • Step 1: Write the failing test — event mapping
  • Step 2: Run test to verify it fails
Run: cd apps/api && npx vitest run src/billing/billing-webhook.service.spec.ts Expected: FAIL — Cannot find module './billing-webhook.service'
  • Step 3: Write the service
  • Step 4: Run test to verify it passes
Run: cd apps/api && npx vitest run src/billing/billing-webhook.service.spec.ts Expected: PASS (4 tests)
  • Step 5: Controller with signature verification
  • Step 6: Enable raw body for this route
In apps/api/src/main.ts, find NestFactory.create(...) and add { rawBody: true } to its options if not already present:
This makes req.rawBody available on every request (Nest’s built-in support) without disabling JSON parsing for other routes — no custom body-parser needed.
  • Step 7: Register in BillingModule
In apps/api/src/billing/billing.module.ts, add BillingWebhookController to controllers and BillingWebhookService to providers:
  • Step 8: Run the API build
Run: cd apps/api && npx tsc --noEmit Expected: no errors
  • Step 9: Commit
Manual verification (do before Task 17 flips ENTITLEMENTS_ENFORCE, and before this plan is considered fully done): in Stripe test mode, run a real Checkout against staging, confirm the webhook fires (stripe listen --forward-to or the Stripe dashboard’s webhook log), and confirm workspaces.plan actually updates in the staging DB.

Task 14: Customer Portal (self-serve upgrade/downgrade/cancel)

Files:
  • Modify: apps/api/src/billing/billing.service.ts
  • Modify: apps/api/src/billing/billing.service.spec.ts
  • Modify: apps/api/src/billing/billing.controller.ts
Interfaces:
  • Produces: BillingService.createPortalSession(workspaceId): Promise<{ url: string }>, POST /billing/portal-session.
  • Step 1: Write the failing test
Add to apps/api/src/billing/billing.service.spec.ts:
  • Step 2: Run test to verify it fails
Run: cd apps/api && npx vitest run src/billing/billing.service.spec.ts -t Portal Expected: FAIL — createPortalSession is not a function
  • Step 3: Implement (and persist the customer id on first checkout)
In billing.service.ts’s createCheckoutSession, Stripe creates the Customer implicitly on Checkout completion — to have a stripe_customer_id for the Portal, read it back in the webhook instead. Add this to createPortalSession:
Then in apps/api/src/billing/billing-webhook.service.ts, persist the customer id on checkout.session.completed — replace the await this.workspacesService.setPlan(workspaceId, tier, undefined) line with:
(This duplicates a small merge that setPlan already does for overrides — acceptable here since the two writes target different sub-keys of the same billing object and happen sequentially, not concurrently.)
  • Step 4: Run test to verify it passes
Run: cd apps/api && npx vitest run src/billing/ Expected: PASS (all billing tests)
  • Step 5: Controller endpoint
In apps/api/src/billing/billing.controller.ts, add:
  • Step 6: Run the API build
Run: cd apps/api && npx tsc --noEmit Expected: no errors
  • Step 7: Commit

Task 15: Dashboard — Upgrade / Manage billing button

Files:
  • Modify: apps/dashboard/src/redesign/Settings.tsx
Interfaces:
  • Consumes: api.post (existing, apps/dashboard/src/api.ts), POST /billing/checkout-session and POST /billing/portal-session (Task 12/14).
  • Step 1: Add a 'billing' section
In apps/dashboard/src/redesign/Settings.tsx:
  • Add 'billing' to the Section union type.
  • Add billing: 'Billing' to SET_LABEL.
  • Add 'billing' to the { items: ['workspace', 'members'] } group in SET_GROUPS (or its own group — match whichever grouping reads better next to workspace).
  • Step 2: Render the section
Find where other sections render their panel content (look for the existing section === 'workspace' conditional block) and add a sibling block:
(Match the exact prop names SettingsSection/SettingsCard/Button already take elsewhere in this file — copy the pattern from the nearest existing section rather than inventing new props.)
  • Step 3: Manual verification
Run: cd apps/dashboard && npm run dev, open Settings → Billing, click “Upgrade to Pro” with STRIPE_SECRET_KEY pointed at Stripe test mode — confirm it redirects to a real Stripe Checkout page for the $9 price.
  • Step 4: Commit

Task 16: Rewrite landing pricing-section.tsx

Files:
  • Modify: apps/web/src/components/landing/pricing-section.tsx
Interfaces: none (presentation only).
  • Step 1: Replace the tiers array
Keep the rest of the component (the <section> JSX, the grid, the card rendering) untouched — only the data array changes, and the grid is already md:grid-cols-2 lg:grid-cols-4, which fits 4 tiers without layout changes.
  • Step 2: Manual verification
Run: cd apps/web && npm run dev, open the landing page, confirm all 4 tiers render with correct copy and the “Most popular” badge now sits on Team (already driven by featured: true, moved from Builder to Team in the array above).
  • Step 3: Commit

Self-Review Notes

  • Spec coverage: all 3 brainstorming blocks covered — tiers/catalog (Tasks 1-2), gating call-sites for every axis discussed (seats T6, workspaces T5, repos T4, projects T7, collections T8, byom_chat T9, broker T10), Stripe (Tasks 11-14), landing copy (Task 16), dashboard upgrade UI (Task 15).
  • Branch decision resolved: Task 1’s Global Constraints explicitly rules out merging feat/entitlements-engine — it predates Projects/Collections/Comments/Broker/Chat and its diffs would delete them from data-source.ts. Tasks 1-3 re-derive the same EntitlementsService/FeatureGuard pattern fresh against current staging, extended with projects/collections/byom_chat/broker.
  • Enforcement flag: left OFF through every task (per Global Constraints); flipping ENTITLEMENTS_ENFORCE=true is called out as a deliberate post-Task-13 step, not part of any task’s commit.
  • Type consistency: EntitlementsService method names (assertRepoAvailable, assertProjectAvailable, assertCollectionAvailable, assertSeatAvailable, assertWorkspaceAvailable, assertFeature, can, resolve) are identical between Task 2’s definition and every call site in Tasks 4-10. FeatureKey is 'byom_chat' | 'broker' consistently from Task 1 through Task 10.