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 onstaging— its diffs ofworkspaces.module.ts/workspaces.controller.ts/data-source.tsdelete all of that from the entity list. It is reference-only for theEntitlementsService/FeatureGuardpattern, already reproduced correctly in Task 2-3 below against currentstaging. - Knowledge/Topics are never gated. No task in this plan touches
apps/api/src/topics/**. - Enforcement stays OFF by default (
ENTITLEMENTS_ENFORCEunset/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’sforRootAsyncentities list too, not justdata-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. Checkapp.module.tsfor aWorkspacereference — 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_SECRETare read from env only; the repo’ssecret-scan.tswill already block a literalsk_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
-
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
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
cd apps/api && npx vitest run src/entitlements/plans.catalog.spec.ts
Expected: PASS (7 tests)
- Step 5: Add the
plancolumn — migration
- Step 6: Register the migration in data-source.ts
libs/db/src/data-source.ts, add the import next to the other 102x-range imports:
AddWorkspacePlan1715200000102 to the end of the migrations: [...] array (after AddProviderCredentialModels1715200000101).
- Step 7: Add the entity column
libs/db/src/entities/workspace.entity.ts, add after clerk_org_id:
- Step 8: Run the API build to confirm no type errors
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
-
Consumes:
PLANS,planIdOrFree,FeatureKey,PlanDef,PlanIdfrom./plans.catalog(Task 1);Workspace,WorkspaceMember,Repo,Project,Collectionfrom@driftless/db;DomainException,ErrorCodefrom../ops/error-codes. -
Produces:
EntitlementsServicewithresolve(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
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
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
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)
-
Consumes:
EntitlementsService(Task 2),FeatureKey(Task 1). -
Produces:
@RequiresFeature(key)decorator +FeatureGuard,EntitlementsModule(exportsEntitlementsService,FeatureGuard),WorkspacesService.setPlan(workspaceId, plan, overrides?). Tasks 4-10 importEntitlementsModuleinto their own module and injectEntitlementsService. - 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
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
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
apps/api/src/workspaces/workspaces.service.ts, add after findById:
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
cd apps/api && npx vitest run src/workspaces/workspaces.service.spec.ts -t setPlan
Expected: PASS (2 tests)
- Step 6: Add the admin endpoint
apps/api/src/workspaces/workspaces.controller.ts, add after the existing update (PATCH :slug) method — inject ConfigService in the constructor if not already present:
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
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
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
-
Consumes:
EntitlementsService.assertRepoAvailable(workspaceId)(Task 2), already exported viaWorkspacesModulewhich ownsReposService. - Step 1: Write the failing test
apps/api/src/repos/repos.service.spec.ts (find its existing DI-mock setup for ReposService and add an EntitlementsService mock alongside):
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
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
apps/api/src/repos/repos.service.ts, add the import and constructor param:
create(), add as the first line of the method body:
- Step 4: Run test to verify it passes
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 existingworkspaces.controller.spec.ts; if none exists, add this test toworkspaces.service.spec.tstargetingcreate()instead, sincecreate()doesn’t move)
-
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
cd apps/api && npx vitest run src/workspaces/ -t assertWorkspaceAvailable
Expected: FAIL — spy not called
- Step 3: Wire the gate
apps/api/src/workspaces/workspaces.controller.ts, inject EntitlementsService in the constructor and call it in create() before this.workspacesService.create(...):
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
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
-
Consumes:
EntitlementsService.assertSeatAvailable(workspaceId)(Task 2). - Step 1: Write the failing test
apps/api/src/invitations/invitations.service.spec.ts:
- Step 2: Run test to verify it fails
cd apps/api && npx vitest run src/invitations/invitations.service.spec.ts -t assertSeatAvailable
Expected: FAIL
- Step 3: Wire EntitlementsModule + the gate
apps/api/src/invitations/invitations.module.ts, add the import and add to imports:
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
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
-
Consumes:
EntitlementsService.assertProjectAvailable(workspaceId)(Task 2). - Step 1: Write the failing test
- Step 2: Run test to verify it fails
cd apps/api && npx vitest run src/projects/projects.service.spec.ts -t assertProjectAvailable
Expected: FAIL
- Step 3: Wire EntitlementsModule + the gate
apps/api/src/projects/projects.module.ts:
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
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
-
Consumes:
EntitlementsService.assertCollectionAvailable(workspaceId)(Task 2). - Step 1: Write the failing test
- Step 2: Run test to verify it fails
cd apps/api && npx vitest run src/collections/collections.service.spec.ts -t assertCollectionAvailable
Expected: FAIL
- Step 3: Wire EntitlementsModule + the gate
apps/api/src/collections/collections.module.ts:
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
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
-
Consumes:
EntitlementsService.assertFeature(workspaceId, 'byom_chat')(Task 2). - Step 1: Write the failing test — agent config
apps/api/src/agent-runs/agent-config.service.spec.ts:
- Step 2: Run test to verify it fails
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
apps/api/src/agent-runs/agent-runs.module.ts:
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
cd apps/api && npx vitest run src/agent-runs/agent-config.service.spec.ts
Expected: PASS
- Step 5: Write the failing test — chat
apps/api/src/chat/chat.controller.spec.ts:
- Step 6: Run test to verify it fails
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:
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
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)
-
Consumes:
EntitlementsService.assertFeature(workspaceId, 'broker')(Task 2). Distinct fromassertBrokerEnabled(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
cd apps/api && npx vitest run src/broker/ -t 'gates on the broker plan feature'
Expected: FAIL
- Step 3: Wire EntitlementsModule + the gate
apps/api/src/broker/broker.module.ts:
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
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(addstripedependency) - Create:
apps/api/src/billing/stripe-client.ts - Create:
apps/api/scripts/stripe-seed-products.ts
-
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_EXTRAenv vars (populated by the seed script’s output) — Task 12 reads these. - Step 1: Add the dependency
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
cd apps/api && npx tsc --noEmit
Expected: no errors
- Step 5: Commit
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(registerBillingModule)
-
Consumes:
getStripeClient(Task 11),WorkspacesService.findById(existing). -
Produces:
BillingService.createCheckoutSession(workspaceId, tier: 'pro' | 'team', extraSeats?: number, extraWorkspaces?: number): Promise<{ url: string }>. Task 15 (dashboard) callsPOST /billing/checkout-session. - Step 1: Write the failing test
- Step 2: Run test to verify it fails
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
cd apps/api && npx vitest run src/billing/billing.service.spec.ts
Expected: PASS (2 tests)
- Step 5: Controller + module
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
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 howapps/api/src/main.tssets up the body parser today; NestJS’s defaultexpress.json()consumes the body before the controller sees it, so this route needsrawBody: trueon the Nest factory or a route-specific raw parser)
-
Consumes:
getStripeClient(Task 11),WorkspacesService.setPlan(Task 3). -
Produces:
POST /webhooks/stripe— separate fromapps/api/src/webhooks(GitHub-specific). - Step 1: Write the failing test — event mapping
- Step 2: Run test to verify it fails
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
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
apps/api/src/main.ts, find NestFactory.create(...) and add { rawBody: true } to its options if not already present:
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
apps/api/src/billing/billing.module.ts, add BillingWebhookController to controllers and BillingWebhookService to providers:
- Step 8: Run the API build
cd apps/api && npx tsc --noEmit
Expected: no errors
- Step 9: Commit
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
-
Produces:
BillingService.createPortalSession(workspaceId): Promise<{ url: string }>,POST /billing/portal-session. - Step 1: Write the failing test
apps/api/src/billing/billing.service.spec.ts:
- Step 2: Run test to verify it fails
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)
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:
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:
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
cd apps/api && npx vitest run src/billing/
Expected: PASS (all billing tests)
- Step 5: Controller endpoint
apps/api/src/billing/billing.controller.ts, add:
- Step 6: Run the API build
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
-
Consumes:
api.post(existing,apps/dashboard/src/api.ts),POST /billing/checkout-sessionandPOST /billing/portal-session(Task 12/14). -
Step 1: Add a
'billing'section
apps/dashboard/src/redesign/Settings.tsx:
-
Add
'billing'to theSectionunion type. -
Add
billing: 'Billing'toSET_LABEL. -
Add
'billing'to the{ items: ['workspace', 'members'] }group inSET_GROUPS(or its own group — match whichever grouping reads better next toworkspace). - Step 2: Render the section
section === 'workspace' conditional block) and add a sibling block:
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
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
- Step 1: Replace the
tiersarray
<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
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 fromdata-source.ts. Tasks 1-3 re-derive the sameEntitlementsService/FeatureGuardpattern fresh against currentstaging, extended withprojects/collections/byom_chat/broker. - Enforcement flag: left OFF through every task (per Global Constraints); flipping
ENTITLEMENTS_ENFORCE=trueis called out as a deliberate post-Task-13 step, not part of any task’s commit. - Type consistency:
EntitlementsServicemethod names (assertRepoAvailable,assertProjectAvailable,assertCollectionAvailable,assertSeatAvailable,assertWorkspaceAvailable,assertFeature,can,resolve) are identical between Task 2’s definition and every call site in Tasks 4-10.FeatureKeyis'byom_chat' | 'broker'consistently from Task 1 through Task 10.
