Pricing plans
Define products, plans and line items in apps/web/config/billing.config.ts, and the rules the config schema enforces.
Your plans live in apps/web/config/billing.config.ts. Out of the box it re-exports billing.sample.config.ts, whose Starter, Pro and Enterprise products use sample price ids and sample features. Copy the sample into billing.config.ts and replace the ids, prices and features with your own.
The schema is createBillingSchema in packages/billing/core/src/create-billing-schema.ts. A config that breaks a rule fails with a message saying which one.
Shape
import { BillingProviderSchema, createBillingSchema } from '@repo/billing';
const provider = BillingProviderSchema.parse(
process.env.NEXT_PUBLIC_BILLING_PROVIDER,
);
export default createBillingSchema({
provider,
products: [
{
id: 'pro',
name: 'Pro',
description: 'For growing teams',
currency: 'USD',
features: ['Unlimited projects', 'Priority email support'],
plans: [
{
id: 'pro-monthly',
name: 'Pro Monthly',
paymentType: 'recurring',
interval: 'month',
trialDays: 14,
lineItems: [
{
id: 'price_123', // Stripe price id or Lemon Squeezy variant id
name: 'Base',
cost: 29,
type: 'flat',
},
],
},
],
},
],
});
Fields
| Level | Fields |
|---|---|
| Product | id, name, description, currency, features (at least one), plans, and optional badge, highlighted, hidden, enableDiscountField |
| Plan | id, name, paymentType (recurring or one-time), lineItems, and optional interval (month or year), trialDays, custom, label, buttonLabel, href |
| Line item | id, name, cost, type (flat, per_seat or metered), and optional description, unit, tiers, setupFee |
Rules the schema enforces
- Recurring plans need an
interval; one-time plans must not have one, and can only containflatitems. - A plan has at most one
flatand oneper_seatitem. metereditems need aunitandtiers, and acostof 0 (the tiers carry the price).- A
customplan has no line items. Uselabel,buttonLabelandhreffor something like "Contact us". - Plan ids are unique within a product, and line item ids within a plan.
- With Lemon Squeezy, a plan has exactly one line item.
Per-seat plans
For a per_seat item on a team plan, the kit keeps the quantity equal to the team's member count: it updates when someone accepts an invitation or a member is removed (account-per-seat-billing.service.ts in packages/features/team-accounts).
Showing the plans
The billing pages in the app read this config. For a public pricing page, PricingTable from @repo/billing-gateway/marketing renders the same plans. The kit's own /pricing page currently shows the kit's offer (apps/web/config/kit-offer.config.ts) instead; mount PricingTable there when you replace it with your product.