Repository navigation
feat: add Team ($45) and Business ($90) plans, re-size run limits - #27
Conversation
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".
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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 →
Summary
Adds Team ($45, recommended) and Business ($90) plans and re-sizes run limits so each tier is reachable:
Bigger tiers are cheaper per run, so upgrading is the obvious step.
Changes
BACHS_TEAM_PRODUCT_ID,BACHS_BUSINESS_PRODUCT_ID