feat(backend)!: flat-tier pricing — drop the pro tier, uncap paid private repos - #24
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vate repos Plans are now free / team / business. Paid tiers have unlimited private repos, providers, and environments and differ by governance features only (Exposure/SSO stay gated on business); the Free tier goes from 1 to 10 private repos. Members were already uncapped everywhere. Migration 0044 remaps data before rebuilding the enum (Postgres cannot drop an enum value in place): org pro folds into team when a Stripe customer exists and to free otherwise, personal paid plans reset to free unless a Stripe customer exists (never divorce an active subscription from its entitlements), and lapsed unconverted trials are normalized to free to match their effective plan. Billing drops the pro_* lookup keys, derives the reverse price→plan map from the forward one, and /v1/billing/prices no longer returns a pro entry (deployed clients must tolerate that first — see the dashboard tolerance fix). The soft-limit FIFO ordering now breaks created_at ties on id so the read-only split is deterministic, and the vault-listing read-only flag is covered by a test. BREAKING CHANGE: GET /v1/billing/prices no longer includes prices.pro, and the user_plan enum loses the 'pro' value. Run db:migrate before deploying this backend. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ADR 0002 supersedes ADR 0001 (tier list, migration semantics, org-only direction, deploy ordering). Backend CLAUDE.md plan-limits snippet now matches src/config/plans.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR removes the ChangesFlat-tier pricing migration
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Migration 0044 gains the repo-conventional statement-breakpoint markers and an explicit Stripe guard on the users pro→team remap so each statement is independently true to the stated policy (residual org 'pro' rows without a Stripe customer now fold to free — 'pro' was never a legitimate org tier — and the header comment says so). ADR 0002 no longer claims features the code does not implement (the only enforced gate is Exposure at business; SSO is planned, not shipped), resolves its internal contradiction about legacy pro subscriptions (none exist; the fold is defensive), and drops an incidental query-ordering note that lives in code comments. Test fixture amounts go back to arbitrary values, and the gitignore entries for AGENTS.md and *.db are root-anchored so deliberate per-package files stay commitable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
🧹 Nitpick comments (3)
packages/backend/tests/helpers/mocks.ts (1)
32-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider freezing the shared fixture to prevent cross-suite mutation.
This object is explicitly documented as shared between
billing.routes.test.tsandorganizations.billing.test.ts. Since it's a plain mutable object passed by reference intomockResolvedValue, any test that mutates a nested field (e.g.,mockAvailablePrices.team.monthly.amount = ...) would silently leak state into the other suite depending on execution order.♻️ Suggested immutability guard
-export const mockAvailablePrices = { +export const mockAvailablePrices = Object.freeze({ team: { - monthly: { id: 'price_team_monthly', amount: 1900, currency: 'eur', interval: 'month' }, - yearly: { id: 'price_team_yearly', amount: 19000, currency: 'eur', interval: 'year' }, + monthly: Object.freeze({ id: 'price_team_monthly', amount: 1900, currency: 'eur', interval: 'month' }), + yearly: Object.freeze({ id: 'price_team_yearly', amount: 19000, currency: 'eur', interval: 'year' }), }, business: { - monthly: { id: 'price_business_monthly', amount: 3900, currency: 'eur', interval: 'month' }, - yearly: { id: 'price_business_yearly', amount: 39000, currency: 'eur', interval: 'year' }, + monthly: Object.freeze({ id: 'price_business_monthly', amount: 3900, currency: 'eur', interval: 'month' }), + yearly: Object.freeze({ id: 'price_business_yearly', amount: 39000, currency: 'eur', interval: 'year' }), }, -}; +});🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/backend/tests/helpers/mocks.ts` around lines 32 - 47, Freeze the shared mock fixture to prevent state leakage between billing test suites. The exported mockAvailablePrices in mocks.ts is reused by billing.routes and organizations.billing, so make it immutable (including nested price objects) before it is passed to mockResolvedValue. Use the existing mockAvailablePrices symbol to apply a deep immutability guard or equivalent read-only construction so any accidental mutation in one suite cannot affect the other.packages/backend/drizzle/0044_drop_pro_plan_flat_tiers.sql (1)
32-37: 🚀 Performance & Scalability | 🔵 TrivialHeads-up: expect a full-table lock during this migration.
ALTER TABLE ... ALTER COLUMN plan TYPE "user_plan" USING ...triggers a full table rewrite under anACCESS EXCLUSIVElock in Postgres, blocking all reads/writes onusers/organizationsfor the duration — potentially significant on large tables. Worth confirming table sizes and picking a low-traffic deploy window, consistent with the PR's noted requirement to rundb:migratebefore the backend deploy.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/backend/drizzle/0044_drop_pro_plan_flat_tiers.sql` around lines 32 - 37, The migration step in the ALTER COLUMN plan TYPE transition on users and organizations will take an ACCESS EXCLUSIVE lock and rewrite the full table, so confirm the table sizes and plan this as a deliberately scheduled low-traffic operation. If possible, adjust the migration strategy around the ALTER TABLE statements in 0044_drop_pro_plan_flat_tiers.sql to avoid a long blocking rewrite, otherwise keep it as-is but ensure db:migrate runs ahead of deploy in a maintenance window.packages/backend/tests/softLimits.test.ts (1)
10-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReuse the shared DB mock helper here.
tests/helpers/mocks.tsalready hascreateMockDb; extend itsselectstub to supportorderByand use that here instead of re-creating the query-chain scaffold inline.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/backend/tests/softLimits.test.ts` around lines 10 - 34, The test is re-creating a full DB query-chain mock inline instead of using the shared helper. Update the `softLimits.test.ts` setup to use `createMockDb` from `tests/helpers/mocks.ts`, and extend that helper’s `select` chain so it supports `orderBy` in addition to the existing query methods. Keep the existing `mockOrderBy` and `mockGetEffectivePlanForVault` behavior wired through the shared mock so `getPrivateVaultAccess` and `canWriteToVault` still exercise the same path.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/backend/drizzle/0044_drop_pro_plan_flat_tiers.sql`:
- Around line 32-37: The migration step in the ALTER COLUMN plan TYPE transition
on users and organizations will take an ACCESS EXCLUSIVE lock and rewrite the
full table, so confirm the table sizes and plan this as a deliberately scheduled
low-traffic operation. If possible, adjust the migration strategy around the
ALTER TABLE statements in 0044_drop_pro_plan_flat_tiers.sql to avoid a long
blocking rewrite, otherwise keep it as-is but ensure db:migrate runs ahead of
deploy in a maintenance window.
In `@packages/backend/tests/helpers/mocks.ts`:
- Around line 32-47: Freeze the shared mock fixture to prevent state leakage
between billing test suites. The exported mockAvailablePrices in mocks.ts is
reused by billing.routes and organizations.billing, so make it immutable
(including nested price objects) before it is passed to mockResolvedValue. Use
the existing mockAvailablePrices symbol to apply a deep immutability guard or
equivalent read-only construction so any accidental mutation in one suite cannot
affect the other.
In `@packages/backend/tests/softLimits.test.ts`:
- Around line 10-34: The test is re-creating a full DB query-chain mock inline
instead of using the shared helper. Update the `softLimits.test.ts` setup to use
`createMockDb` from `tests/helpers/mocks.ts`, and extend that helper’s `select`
chain so it supports `orderBy` in addition to the existing query methods. Keep
the existing `mockOrderBy` and `mockGetEffectivePlanForVault` behavior wired
through the shared mock so `getPrivateVaultAccess` and `canWriteToVault` still
exercise the same path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 20a179c1-878d-4139-85ed-977ce481bb2b
📒 Files selected for processing (24)
.gitignoredocs/adr/0002-flat-tier-pricing.mdpackages/backend/CLAUDE.mdpackages/backend/drizzle/0044_drop_pro_plan_flat_tiers.sqlpackages/backend/drizzle/meta/_journal.jsonpackages/backend/src/api/v1/routes/billing.routes.tspackages/backend/src/config/plans.tspackages/backend/src/db/schema.tspackages/backend/src/services/billing.service.tspackages/backend/src/services/trial.service.tspackages/backend/src/services/usage.service.tspackages/backend/src/services/vault.service.tspackages/backend/tests/helpers/mocks.tspackages/backend/tests/integration/billing.routes.test.tspackages/backend/tests/integration/environments.test.tspackages/backend/tests/integration/integrations.test.tspackages/backend/tests/integration/organizations.billing.test.tspackages/backend/tests/integration/organizations.privilege-escalation.test.tspackages/backend/tests/integration/trash.routes.test.tspackages/backend/tests/integration/vaults.routes.test.tspackages/backend/tests/planLimits.test.tspackages/backend/tests/plans.test.tspackages/backend/tests/services/vault.service.test.tspackages/backend/tests/softLimits.test.ts
💤 Files with no reviewable changes (1)
- packages/backend/src/api/v1/routes/billing.routes.ts
What
Plans are now Free · Team · Business (supersedes the four-tier model from ADR 0001 — see the included ADR 0002):
proremoved from theuser_planenum; migration0044remaps data then rebuilds the enum (Postgres can't drop an enum value in place)pro_*lookup keys retired, reverse price→plan map derived from the forward one,/v1/billing/pricesno longer returns aproentrycreated_atties onid(deterministic read-only split), and the vault-listingisReadOnlyflag gained test coverageMigration semantics (0044)
pro+ Stripe customerteampro, no Stripefreefree(stored plan now matches effective plan)freepro→team) — an active subscription is never divorced from its entitlementsValidated on a fresh Postgres 16 (full migration chain) and on a 13-case representative dataset.
Deployment
proprice entry)team_*_eur/business_*_eurlookup keys must exist in Stripe Live (already created in Test)db:migratebefore deploying this backend (nothing migrates at startup;prorows would makePLANS['pro']throw)Tests
944 unit + 5 integration-db green;
validate(type-check + build) green.BREAKING CHANGE:
GET /v1/billing/pricesno longer includesprices.pro;user_planenum losespro.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation