Skip to content

feat(backend)!: flat-tier pricing — drop the pro tier, uncap paid private repos - #24

Merged
NicolasRitouet merged 4 commits into
mainfrom
feat/flat-tier-pricing
Jul 6, 2026
Merged

feat(backend)!: flat-tier pricing — drop the pro tier, uncap paid private repos#24
NicolasRitouet merged 4 commits into
mainfrom
feat/flat-tier-pricing

Conversation

@NicolasRitouet

@NicolasRitouet NicolasRitouet commented Jul 2, 2026

Copy link
Copy Markdown
Member

What

Plans are now Free · Team · Business (supersedes the four-tier model from ADR 0001 — see the included ADR 0002):

  • pro removed from the user_plan enum; migration 0044 remaps data then rebuilds the enum (Postgres can't drop an enum value in place)
  • Paid tiers: unlimited private repos/providers/environments, differentiated by governance features only (Exposure/SSO stay fail-closed on Business)
  • Free tier: 10 private repos (up from 1); public repos unlimited everywhere; members never capped on any tier
  • Billing: pro_* lookup keys retired, reverse price→plan map derived from the forward one, /v1/billing/prices no longer returns a pro entry
  • Soft-limit FIFO ordering now breaks created_at ties on id (deterministic read-only split), and the vault-listing isReadOnly flag gained test coverage

Migration semantics (0044)

Before After
org pro + Stripe customer team
org pro, no Stripe free
org with lapsed unconverted trial, no Stripe free (stored plan now matches effective plan)
org plan granted manually (no Stripe, no trial) kept
personal paid, no Stripe customer free
personal paid + Stripe customer kept on a valid paid tier (proteam) — an active subscription is never divorced from its entitlements

Validated on a fresh Postgres 16 (full migration chain) and on a 13-case representative dataset.

Deployment

⚠️ Order matters:

  1. Merge & deploy fix(dashboard): tolerate pricing API responses without the pro tier #23 first (dashboard must tolerate the missing pro price entry)
  2. team_*_eur / business_*_eur lookup keys must exist in Stripe Live (already created in Test)
  3. Run db:migrate before deploying this backend (nothing migrates at startup; pro rows would make PLANS['pro'] throw)

Tests

944 unit + 5 integration-db green; validate (type-check + build) green.

BREAKING CHANGE: GET /v1/billing/prices no longer includes prices.pro; user_plan enum loses pro.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Updated pricing tiers to Free, Team, and Business.
    • Team and Business now include unlimited private repos, providers, environments, and secrets; Business also includes Exposure access.
  • Bug Fixes

    • Improved billing and checkout behavior to accept only current pricing options.
    • Made private vault handling more consistent when items share the same creation time.
  • Documentation

    • Added release notes for the new flat-tier pricing model and updated internal guidance.

NicolasRitouet and others added 3 commits July 3, 2026 00:20
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>
@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
keyway-dashboard Ready Ready Preview, Comment Jul 3, 2026 9:50am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR removes the pro pricing tier throughout the backend, migrating to a flat-tier model with free, team, and business plans. Changes span the database enum/migration, plan config quotas and ranking, billing service/routes, vault access logic, and extensive test updates.

Changes

Flat-tier pricing migration

Layer / File(s) Summary
ADR and documentation
docs/adr/0002-flat-tier-pricing.md, packages/backend/CLAUDE.md, .gitignore
Adds ADR describing removal of Pro tier and flat org-level entitlements; updates docs to reflect free/team/business tiers; adds unrelated gitignore entries.
Schema and migration
packages/backend/src/db/schema.ts, packages/backend/drizzle/0044_drop_pro_plan_flat_tiers.sql, packages/backend/drizzle/meta/_journal.json
Removes pro from user_plan enum, adds migration remapping existing pro users/orgs to team/free based on Stripe linkage and trial status, rebuilds the enum.
Plan config and rank
packages/backend/src/config/plans.ts, packages/backend/src/services/trial.service.ts
Updates PLANS quotas (free maxPrivateRepos to 10, team/business unlimited) and PLAN_RANK; updates trial service return type.
Billing service and routes
packages/backend/src/services/billing.service.ts, packages/backend/src/api/v1/routes/billing.routes.ts
Reworks Stripe lookup keys to team/business only, uses planRank for upgrade detection, changes deleted-subscription fallback plan to team, removes pro from prices/checkout endpoints.
Vault access logic
packages/backend/src/services/usage.service.ts, packages/backend/src/services/vault.service.ts
Adds deterministic secondary sort on vault id for FIFO tie-breaking; updates free-plan denial message to reference upgrading to Team.
Tests
packages/backend/tests/**
Updates test fixtures and assertions across billing, plans, plan limits, vault service, soft limits, and multiple integration tests to reflect the team/business-based tier model.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • keywaysh/keyway#22: Also modifies billing routes and plan config to change which tiers are validated in pricing and checkout logic.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly summarizes the main backend pricing change: removing pro and moving to flat-tier paid plans.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/flat-tier-pricing

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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>
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@NicolasRitouet

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@NicolasRitouet

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/backend/tests/helpers/mocks.ts (1)

32-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider freezing the shared fixture to prevent cross-suite mutation.

This object is explicitly documented as shared between billing.routes.test.ts and organizations.billing.test.ts. Since it's a plain mutable object passed by reference into mockResolvedValue, 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 | 🔵 Trivial

Heads-up: expect a full-table lock during this migration.

ALTER TABLE ... ALTER COLUMN plan TYPE "user_plan" USING ... triggers a full table rewrite under an ACCESS EXCLUSIVE lock in Postgres, blocking all reads/writes on users/organizations for 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 run db:migrate before 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 win

Reuse the shared DB mock helper here. tests/helpers/mocks.ts already has createMockDb; extend its select stub to support orderBy and 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

📥 Commits

Reviewing files that changed from the base of the PR and between c9911f8 and 13b1a00.

📒 Files selected for processing (24)
  • .gitignore
  • docs/adr/0002-flat-tier-pricing.md
  • packages/backend/CLAUDE.md
  • packages/backend/drizzle/0044_drop_pro_plan_flat_tiers.sql
  • packages/backend/drizzle/meta/_journal.json
  • packages/backend/src/api/v1/routes/billing.routes.ts
  • packages/backend/src/config/plans.ts
  • packages/backend/src/db/schema.ts
  • packages/backend/src/services/billing.service.ts
  • packages/backend/src/services/trial.service.ts
  • packages/backend/src/services/usage.service.ts
  • packages/backend/src/services/vault.service.ts
  • packages/backend/tests/helpers/mocks.ts
  • packages/backend/tests/integration/billing.routes.test.ts
  • packages/backend/tests/integration/environments.test.ts
  • packages/backend/tests/integration/integrations.test.ts
  • packages/backend/tests/integration/organizations.billing.test.ts
  • packages/backend/tests/integration/organizations.privilege-escalation.test.ts
  • packages/backend/tests/integration/trash.routes.test.ts
  • packages/backend/tests/integration/vaults.routes.test.ts
  • packages/backend/tests/planLimits.test.ts
  • packages/backend/tests/plans.test.ts
  • packages/backend/tests/services/vault.service.test.ts
  • packages/backend/tests/softLimits.test.ts
💤 Files with no reviewable changes (1)
  • packages/backend/src/api/v1/routes/billing.routes.ts

@NicolasRitouet
NicolasRitouet merged commit 8adea70 into main Jul 6, 2026
5 checks passed
@NicolasRitouet
NicolasRitouet deleted the feat/flat-tier-pricing branch July 6, 2026 04:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant