Demo

Saturday, June 7th 2025

Billing Overhaul

The billing system got completely overhauled, now supporting diverse plans, including lifetime and metered options, with flexible pricing models.

Schema

The billing configuration is composed of the following entities:

  • Product: Defines the offering (e.g. Starter, Pro, Enterprise, etc.) with 1-n plans.
  • Plan: Defines the payment plan (e.g. Pro Monthly, Pro Yearly. etc) with 1-n prices.
  • Price: Defines the type, interval, model, amount and currency.

Following enums define a price more granulary:

  • PriceType: Can be recurring or one-time.
  • PriceInterval: Can be month, year or undefined.
  • PriceModel: Can be flat, per_seat or metered.

Multiple prices are required if you want to combine multiple strategies, so more line items will be generate on the invoice.

Tactic Change

We've shifted our strategy for handling billing data:

  • Previous Tactic: Store as little as possible and query everything directly from the billing provider.
  • New Tactic: Store as much billing data as possible in our own database. This change is driven by the realization that billing providers can impose aggressive rate limits as user amounts increase.

Database Changes

To support this new tactic, we've made several important database schema changes:

  • Organization: Now stores a billing customer (including billing email and billing address). Previous tiers have been removed.
  • Added Subscription: A new table to manage active subscriptions.
  • Added SubscriptionItem: Details the individual items within a subscription.
  • Added Order: A new table for one-time purchases.
  • Added OrderItem: Details the individual items within an order.

Example Configuration

Below is an example of how a billingConfig might be structured, showcasing Free, Pro, Lifetime and Enterprise products with their respective plans and prices:

packages/billing/src/config.ts
export const billingConfig = createBillingConfig({  products: [    {      id: 'free',      name: 'Free',      description: 'Start for free.',      label: 'Get started',      isFree: true,      features: [Feature.AICustomerScoring, Feature.SmartEmailAnalysis],      // Even if it is free, keep the plans and prices to display the interval and currency correctly      plans: [        {          id: 'plan-free-month',          displayIntervals: [PriceInterval.Month],          prices: [            {              id: 'price-free-month-id', // a placebo ID is fine here              type: PriceType.Recurring,              model: PriceModel.Flat,              interval: PriceInterval.Month,              cost: 0,              currency            }          ]        },        {          id: 'plan-free-year',          displayIntervals: [PriceInterval.Year],          prices: [            {              id: 'price-free-year-id', // a placebo ID is fine here              interval: PriceInterval.Year,              type: PriceType.Recurring,              model: PriceModel.Flat,              cost: 0,              currency            }          ]        }      ]    },    {      id: 'pro',      name: 'Pro',      description: 'Best for most teams.',      label: 'Get started',      recommended: true,      features: [        Feature.AICustomerScoring,        Feature.SmartEmailAnalysis,        Feature.SentimentAnalysis,        Feature.LeadPredictions      ],      plans: [        {          id: 'plan-pro-month',          displayIntervals: [PriceInterval.Month],          trialDays: 7,          prices: [            {              id:                keys().NEXT_PUBLIC_BILLING_PRICE_PRO_MONTH_ID ||                'price-pro-month-id', // keep for marketing pages, so you only need to specify id in dashboard              interval: PriceInterval.Month,              type: PriceType.Recurring,              model: PriceModel.Flat,              cost: 24,              currency            }          ]        },        {          id: 'plan-pro-year',          displayIntervals: [PriceInterval.Year],          trialDays: 7,          prices: [            {              id:                keys().NEXT_PUBLIC_BILLING_PRICE_PRO_YEAR_ID ||                'price-pro-year-id', // keep for marketing pages, so you only need to specify id in dashboard              interval: PriceInterval.Year,              type: PriceType.Recurring,              model: PriceModel.Flat,              cost: 199,              currency            }          ]        }      ]    },    {      id: 'lifetime',      name: 'Lifetime',      description: 'Buy once. Use forever.',      label: 'Get started',      features: [        Feature.AICustomerScoring,        Feature.SmartEmailAnalysis,        Feature.SentimentAnalysis,        Feature.LeadPredictions      ],      plans: [        {          id: 'lifetime',          displayIntervals: [PriceInterval.Month, PriceInterval.Year],          prices: [            {              id:                keys().NEXT_PUBLIC_BILLING_PRICE_LIFETIME_ID ||                'price-lifetime-id', // keep for marketing pages, so you only need to specify id in dashboard              type: PriceType.OneTime,              model: PriceModel.Flat, // only flat is supported for PriceType.OneTime              interval: undefined,              cost: 699,              currency            }          ]        }      ]    },    {      id: 'enterprise',      name: 'Enterprise',      description: 'Best for tailored requirements.',      label: 'Contact us',      isEnterprise: true,      features: [        Feature.AICustomerScoring,        Feature.SmartEmailAnalysis,        Feature.SentimentAnalysis,        Feature.LeadPredictions,        Feature.DataStorage,        Feature.ExtendedSupport      ],      // The idea is to keep the product and prices and use an admin panel to update the customer to enterprise.      // For enterprise you can have multiple products, you just need to set hidden: true on the other enterprise products.      plans: [        {          id: 'plan-enterprise-month',          displayIntervals: [PriceInterval.Month],          prices: [            {              id:                keys().NEXT_PUBLIC_BILLING_PRICE_ENTERPRISE_MONTH_ID ||                'price-enterprise-month-id', // keep for marketing pages, so you only need to specify id in dashboard              interval: PriceInterval.Month,              type: PriceType.Recurring,              model: PriceModel.Flat,              cost: 39,              currency            }          ]        },        {          id: 'plan-enterprise-year',          displayIntervals: [PriceInterval.Year],          prices: [            {              id:                keys().NEXT_PUBLIC_BILLING_PRICE_ENTERPRISE_YEAR_ID ||                'price-enterprise-year-id', // keep for marketing pages, so you only need to specify id in dashboard              interval: PriceInterval.Year,              type: PriceType.Recurring,              model: PriceModel.Flat,              cost: 399,              currency            }          ]        }      ]    }  ]});

Commits