feat(billing): expand Free plan and tighten Team chunk cap - #225
Merged
Conversation
Pricing model v3: lift Free trial caps so multi-source RAG can actually be evaluated before paying, and lower the Team chunk cap so data-heavy customers feel the Business upgrade pull earlier in their growth curve. - Free: 1 → 2 connectors, 10K → 25K stored chunks (in place; pure upgrade for existing Free users, no grandfathering needed). - Team: 1M → 500K stored chunks. Existing Team subscriptions grandfather onto a renamed `team-legacy-2026-05-v2` row (same Stripe price, same monthly grant, same 1M cap), mirroring the mechanic from migration 0061. New signups land on the fresh `team` row at the 500K cap. Updates the canonical defaults in plan-defaults.ts, the landing page pricing band, and the stale per-connector comment strings that named the old "free → 1 connector" gate. Billing tests aligned to the new fallback values. https://claude.ai/code/session_013xTfsKeF7jh6HDrxTUtaoG
The INSERT for the new Team row used 2000000 credits — the pre-redenom value copy-pasted from migration 0061. After 0063 + 0065's /100 + /100 redenomination, the canonical Team grant is 20,000 credits/month. Left unfixed, every new Team subscription created after this migration would have received 100× the advertised grant. The renamed legacy row (`team-legacy-2026-05-v2`) is unaffected — it keeps the existing post-redenom value of 20,000 it had before the rename. Caught before deployment; no customer data impact. https://claude.ai/code/session_013xTfsKeF7jh6HDrxTUtaoG
The Team → Business cliff was 4× price ($499 → $1,999), 5× credits, and 20× chunks. Customers in the 500K–2M chunk band had no proportionate upgrade — they were forced into Business or sat at Team's storage cap. Scale fills the gap at $999 / 50K credits / 2M chunks, turning the upper ladder into clean 2× steps. - Migration 0071 inserts the new `scale` plan row (public, standard sync; Stripe Price gets provisioned on next worker boot via ensureStripeProductsForPlans). - plan-defaults.ts gains the `scale: 2_000_000` canonical fallback. - limits.ts upgrade ladder: Team-full now suggests Scale (was Business); Scale-full suggests Business. Connector-cap suggestion is unchanged (Team/Scale/Business all have unlimited connectors). - Landing page (pricing-band.tsx) gets a 5th card; grid breakpoints re-shape (md:2 → lg:3 → xl:5) so the cards don't shrink too much on narrow viewports. - /settings/billing PlanGrid: SLUG_ORDER and PURCHASABLE include `scale`; grid breakpoints match the landing page. - /api/stripe/checkout planSlug enum extended to accept `scale` — otherwise the Upgrade-to-Scale button would 400. - Tests cover the new upgrade-suggestion edges (Team→Scale, Scale→ Business) and the canonical defaults map. https://claude.ai/code/session_013xTfsKeF7jh6HDrxTUtaoG
Annual billing was the highest-leverage missing piece for the v1 pricing model. Customers can now select monthly or annual at checkout; annual charges upfront for the year at ~15% off list, and grants 12× the monthly credit allotment at the start of each annual period. Annual prices (clean monthly-equivalents at exactly 15% off): Starter $99/mo → $84/mo billed annually ($1,008/yr) Team $499/mo → $424/mo billed annually ($5,088/yr) Scale $999/mo → $849/mo billed annually ($10,188/yr) Business $1999/mo → $1,699/mo billed annually ($20,388/yr) Implementation: - Migration 0072 adds `annual_price_cents` and `stripe_annual_price_id` columns on `billing_plans` and seeds the annual prices above. - `ensureStripeProductsForPlans` now provisions a second Stripe Price per plan with `recurring.interval: 'year'` and lookup_key `<slug>-annual`. Existing monthly Prices are untouched. - Checkout API accepts `billingInterval: 'monthly' | 'annual'` (defaults to monthly for backwards compatibility) and routes to the right Stripe Price. - Webhook handler detects annual periods by duration (> 60 days) rather than fetching the Stripe price — avoids an extra API round-trip per invoice — and grants 12 × monthlyCredits when annual. The `idempotencyKey` is still the invoice id, so retries collapse. - Landing page extracts the hosted-plans grid into a client component with a Monthly/Annual segmented toggle (defaults to Annual to show the lower per-month headline). Server component handles the rest of the section for SSR. - /settings/billing PlanGrid grows the same toggle; pricing in each card flips to "$X/mo billed annually" with the yearly total as a sub-caption when toggled. Credits on annual are issued upfront, not metered monthly within the year. Customers who commit to annual accept they could burn through 12 months of credits in month 1; top-ups remain available for overage. https://claude.ai/code/session_013xTfsKeF7jh6HDrxTUtaoG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pricing model v3 — two changes that move ARR in the same direction overall:
team-legacy-2026-05-v2row — same Stripe price, same monthly grant, same 1M cap — mirroring the rename mechanic from migration 0061. New signups land on the freshteamrow at the 500K cap.Modeling suggests ~10–12% book ARR uplift, almost entirely from new Team customers in the 500K–1M chunk band who now migrate to Business 4× earlier in their growth curve.
Files touched
packages/db/migrations/0070_free_expansion_team_chunk_cap.sql— new data migration (Free UPDATE + Team rename/replace)packages/db/migrations/meta/_journal.json+0069_snapshot.json— generated bypnpm db:new-data-migrationpackages/billing/src/plan-defaults.ts— canonical fallback caps for Free (25K) and Team (500K)packages/billing/test/plan-defaults.test.ts,storage-quota.test.ts— aligned to new fallback valuesapps/web/src/components/landing/pricing-band.tsx— Free (2 connectors,25K chunks), Team (500K chunks)apps/web/src/app/api/connectors/*/connect/route.ts— refresh stale(free → 1 connector)comment to(free → 2 connectors)Test plan
pnpm --filter @holo/billing test— 32/32 passingpnpm --filter @holo/web typecheck— cleanpnpm --filter @holo/db check— migration meta OK (70 entries)/pricinglanding page once deployed/settings/billingplan grid (auto-pulls from DB after migrating)maxStoredChunks: 1000000after migration (they keep the legacy plan row's value)https://claude.ai/code/session_013xTfsKeF7jh6HDrxTUtaoG
Generated by Claude Code