Skip to content

feat: add Team ($45) and Business ($90) plans, re-size run limits - #27

Merged
devtofunmi merged 5 commits into
mainfrom
feat/team-pricing-tiers
Sep 27, 2026
Merged

devtofunmi merged 5 commits into
mainfrom
feat/team-pricing-tiers

Conversation

@devtofunmi

Copy link
Copy Markdown
Owner

Summary

Adds Team ($45, recommended) and Business ($90) plans and re-sizes run limits so each tier is reachable:

Plan Price Runs / month
Pro $15 50 (was 75)
Team $45 200
Business $90 500

Bigger tiers are cheaper per run, so upgrading is the obvious step.

Changes

  • One list of all paid plans, with run limits per plan
  • Checkout for the chosen plan; the webhook maps the Bachs product to the plan (falls back to Pro)
  • Pricing page with 4 cards, Team highlighted; features match the Bachs descriptions
  • Admin: plan dropdown and revenue per tier
  • New env vars: BACHS_TEAM_PRODUCT_ID, BACHS_BUSINESS_PRODUCT_ID

Replace the single Pro constant set with a catalog of paid plans so the
app can offer more than one subscription:

- Pro: $15/month, 50 agent runs (down from 75)
- Team: $45/month, 200 agent runs, marked as the recommended plan
- Business: $90/month, 500 agent runs

Limits are sized so they are actually reachable. Every push to a pull
request starts a new review run, so an active solo developer uses
roughly 40-60 runs a month and has a real reason to move up from Pro.
Each bigger tier is cheaper per run (Pro $0.30, Team $0.225, Business
$0.18), which makes Team the obvious upgrade and lets Business signal
that Jargons handles large projects. Measured Gemini cost is about
$0.013 per run, so margins stay healthy even when a tier is maxed out.

Adds PaidPlan/Plan types, PAID_PLANS and PAID_PLAN_IDS, plus
isPaidPlan(), parsePlan() and planRunLimit() helpers for narrowing
untrusted values (DB columns, request bodies) to a known plan.
PRO_RUN_LIMIT and PRO_PRICE_USD are kept, derived from the catalog.

Tests cover the new prices and limits, that only Team is recommended,
that bigger tiers are cheaper per run, and that prototype keys such as
"toString" never parse as a plan.
Each paid plan is now its own Bachs subscription product, so checkout
and the webhook need to know which plan is being bought.

Checkout (POST /api/billing/checkout):
- Accepts { plan } in the body ("pro", "team" or "business"). A missing
  body still defaults to Pro so existing callers keep working; an
  unknown plan returns 400.
- Refuses with 409 already_subscribed when the workspace is already on
  a paid plan. A second checkout would start a second subscription
  next to the first, so switching tiers is not self-serve yet.
- createProCheckout() becomes createCheckout(plan), which looks up the
  plan's product id and stamps the plan on the checkout metadata. It
  throws a clear error if that plan's product id is not configured
  instead of sending an empty product to Bachs.

Webhook (customer.subscription.created):
- planForSubscription() resolves the plan from the event's product_id,
  falling back to the plan stored in the checkout metadata.
- If neither matches, the workspace still gets Pro (the customer paid,
  so they must never stay on free) and a warning is logged so the
  product mapping can be fixed.

markWorkspacePro() becomes markWorkspacePaid(plan), and
getWorkspaceBilling() reads the run limit for whichever plan the
workspace is on.

Config: adds BACHS_TEAM_PRODUCT_ID and BACHS_BUSINESS_PRODUCT_ID to the
env keys and .env.example.
The pricing page now lists four cards: Free, Pro ($15), Team ($45) and
Business ($90). Paid cards are rendered from the plan catalog, so
prices, run limits and taglines come from one place.

- Team carries a "Recommended" badge, an amber border and glow, and the
  primary button style, to steer buyers towards it. Pro and Business
  use the secondary button.
- Each card starts a checkout for its own plan. The loading and error
  states are tracked per plan, and every button is disabled while any
  checkout is starting.
- A workspace already on a paid plan sees "Your current plan" on its
  tier and "You're on <plan>" on the others, matching the API refusing
  a second subscription.
- The layout widens to max-w-6xl with a 1/2/4 column grid, and the
  intro copy now speaks to teams as well as solo developers. Checked at
  1400px wide and at 375px with no horizontal scroll.

Upgrade prompts no longer name Pro now that there are several tiers:
the button reads "Upgrade plan" and the dashboard banner copy is
plan-neutral.
The admin dashboard only knew about "free" and "pro". It now handles
Team and Business too.

- Users table: the Make Pro / Make Free toggle is replaced with a plan
  dropdown (Free, Pro, Team, Business), and the plan badge highlights
  any paid tier.
- POST /api/admin/set-plan accepts free or any paid plan, and
  setWorkspacePlanAsAdmin() applies whichever one is chosen.
- Workspace plans are read through parsePlan(), so an unrecognised
  value shows as free instead of being mislabelled.
- Overview stats: MRR is the sum of each tier's workspaces times its
  price rather than paid workspaces x $15. The paid count and the
  workspace breakdown include every tier, and cost per run is compared
  against the $0.18-0.30 a paid run now earns.
Reword the feature lists on the pricing page so they match what buyers
see on the Bachs checkout for each product.

- Run count now reads "<n> AI code review runs a month" (Free: "1 AI
  code review run a month") instead of "agent runs per month".
- "Automatic PR reviews with findings" becomes "Automatic pull request
  reviews", and "Codebase scans across your repos" becomes "Codebase
  scans".
- Add "Security checks" to every plan; the review and scan engines
  already flag security issues.
- Paid plans say "Billed monthly, cancel anytime" instead of "Cancel
  anytime".
@vercel

vercel Bot commented Sep 27, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
jargons-8yyz Ready Ready Preview Sep 27, 2026 10:51am UTC

@devtofunmi
devtofunmi merged commit 15bfffa into main Sep 27, 2026
2 checks passed

@jargons-ai jargons-ai 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.

Jargons Jargons review

Found 3 issues worth a look:

🟡 Medium — Potential for inconsistent plan state during webhook processing

src/routes/api.billing.webhook.tsx:99

In src/routes/api.billing.webhook.tsx, when a customer.subscription.created event is received, if planForSubscription returns null (meaning the product ID or metadata plan is not recognized), the code defaults to 'pro'. While a warning is logged, this could lead to a user being subscribed to a 'team' or 'business' plan on Bachs but having their workspace marked as 'pro' in the application's database. This inconsistency could cause incorrect feature access, billing discrepancies, or confusion for both users and administrators.

Suggestion: Instead of defaulting to 'pro' for unrecognized plans, consider failing the webhook processing for unknown plans or implementing a more robust fallback mechanism that alerts an administrator to manually resolve the discrepancy. This ensures that the application's state accurately reflects the billing system's state. Alternatively, ensure that planForSubscription is exhaustive and always returns a valid PaidPlan for any product ID configured in Bachs.

🔵 Low — Hardcoded plan name in AdminUsersTable PlanBadge

src/components/admin/users-table.tsx:110

In src/components/admin/users-table.tsx, the PlanBadge component directly displays the plan value. For paid plans, it renders {plan}. While this works for 'pro', 'team', and 'business', if a plan name were to change (e.g., 'pro' becomes 'developer'), the badge would display the internal ID rather than a user-friendly name. It also doesn't leverage the PAID_PLANS object for consistent naming.

Suggestion: Use PAID_PLANS[plan].name to display the user-friendly name for paid plans in the PlanBadge component. This centralizes plan naming and makes it easier to update display names without changing multiple UI components.

🔵 Low — Inconsistent plan display for current paid plans on pricing page

src/routes/pricing.tsx:296

In src/routes/pricing.tsx, the PaidPlanCard component displays 'You're on {PAID_PLANS[currentPlan].name}' if the currentPlan is a paid plan but not the one being rendered. However, the 'Free' plan card uses a generic 'Your current plan' message. This creates an inconsistency in how the user's current plan is communicated across different plan cards.

Suggestion: For consistency, consider updating the CurrentPlan component to accept the currentPlan and display 'You're on {PAID_PLANS[currentPlan].name}' for paid plans, and 'Your current plan' for the free plan, similar to how it's handled for other paid plans. This would make the messaging more uniform.

🔧 Suggested fixes: open the fix PR →

This branch was successfully deployed

1 active deployment
Preview — 70542479 Deployed Sep 27, 2026 by vercel[bot]
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