Pricing plans

Define products, plans and line items in apps/web/config/billing.config.ts, and the rules the config schema enforces.

Last updated

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

LevelFields
Productid, name, description, currency, features (at least one), plans, and optional badge, highlighted, hidden, enableDiscountField
Planid, name, paymentType (recurring or one-time), lineItems, and optional interval (month or year), trialDays, custom, label, buttonLabel, href
Line itemid, 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 contain flat items.
  • A plan has at most one flat and one per_seat item.
  • metered items need a unit and tiers, and a cost of 0 (the tiers carry the price).
  • A custom plan has no line items. Use label, buttonLabel and href for 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.

Live demo

Resets hourly

Get the kit