Configuration

Configure billing for your application.

The billing configuration schema replicates your billing provider's schema, so that:

  • we can display the data in the UI (pricing table, billing section, etc.)
  • we can create the correct checkout session
  • some features can work correctly (e.g. feature-based access)

It is common to all billing providers and is defined in packages/billing/shared/src/config/index.ts. Some billing providers differ in what you can (and cannot) do. In these cases, the schema will try to validate and enforce the rules - but it's still up to you to make sure the data is correct.

The schema is based on a few entities:

  • Plans: The main products you are selling (e.g. "Free", "Premium", etc.)
  • Variants: The purchasable pricing options for a plan (one-time or recurring)
  • Features: The list of features included in a plan (used for the UI and access control)
  • Discounts: Optional discounts that apply to specific variants
index.ts
type BillingConfig = {
  plans: BillingConfigPlan[];
  discounts?: BillingConfigDiscount[];
};

Getting the schema right is important!

Getting the IDs of your plans and variants is extremely important, as these are used to:

  • create the correct checkout
  • manage your customers billing data

Please take it easy while you configure this, do one step at a time, and test it thoroughly.

Billing provider

To set the billing provider, modify the exports in the packages/billing/src/providers directory. It defaults to Stripe.

export * from "./stripe";

It's important to set it correctly, as this is used to determine the correct API calls and environment variables used during the communication with the billing provider.

Plans

Plans are the main products you are selling. They are defined by the following fields:

index.ts
export const config = billingConfigSchema.parse({
  ...
  plans: [
    {
      id: PricingPlanType.PREMIUM,
      name: "Premium",
      description: "Become a power user and gain benefits",
      badge: "Bestseller",
      features: [
        "Unlimited projects",
        "Priority support",
        "Advanced integrations",
        "Team collaboration",
        "Analytics dashboard"
      ],
      variants: [],
    },
  ],
  ...
}) satisfies BillingConfig;

Let's break down the fields:

  • id: The unique identifier for the plan (e.g. free, pro, enterprise, etc.). This is chosen by you, it doesn't need to be the same one as the one in the provider. It's also used to determine the access level of the plan.
  • name: The name of the plan
  • description: The description of the plan
  • badge: A badge to display on the product (e.g. "Bestseller", "Popular", etc.). Can be null.
  • features: The list of included features for the plan- variants: The list of purchasable variants for this plan (see below)

Most of these fields populate the pricing table UI.

Variants

Variants are the purchasable options for a plan. They can be one-time or recurring.

index.ts
export const config = billingConfigSchema.parse({
  ...
  plans: [
    {
      id: BillingPlan.PREMIUM,
      name: "Premium",
      description: "Become a power user and gain benefits",
      badge: "Bestseller",
      variants: [
        {
          /* πŸ‘‡ This is the `priceId` from the provider (e.g. Stripe), `variantId` (e.g. Lemon Squeezy) or `productId` (e.g. Polar) */
          id: "price_1PpZAAFQH4McJDTlig6Fxsyy",
          cost: 1900,
          currency: "usd",
          model: BillingModel.RECURRING,
          interval: RecurringInterval.MONTH,
          trialDays: 7,
          hidden: false,
        },
      ],
    },
  ],
  ...
}) satisfies BillingConfig;

Let's break down the fields:

  • id: The unique identifier for the variant. This must match the corresponding identifier in the billing provider.
  • cost: The price amount in the smallest currency unit (e.g. cents). Displayed values are typically divided by 100.
  • currency: The currency code for the price (defaults to usd)

Set the correct currency on your billing provider

Make sure you have the same currency set on your billing provider (e.g. as a store currency on Lemon Squeezy).

  • model: The billing model for this variant (one-time or recurring)
  • interval: The interval for recurring variants (e.g. month, year)
  • trialDays: Trial length in days for recurring variants (optional)
  • hidden: Whether this variant should be hidden from the pricing table (defaults to false) Could be used to grandfather some variants without complicated migrations.

The cost is used only for UI purposes. The billing provider will handle the actual billing - therefore, please make sure the cost is correctly set in the billing provider.

Set the correct variant ID!

Make sure the id matches the correct identifier in the billing provider. This is critical, as it’s used to identify the correct variant when creating a checkout session.

Custom variants

Sometimes - you want to display a variant in the pricing table - but not actually have it in the billing provider. This is common for custom plans, free plans that don't require the billing provider subscription, or plans that are not yet available.

To do so, let's add the custom flag to the variant:

index.ts
{
  id: "enterprise-monthly",
  label: "Contact us!",
  href: "/contact",
  model: BillingModel.RECURRING,
  interval: RecurringInterval.MONTH,
  custom: true, 
}

Here's the full example:

index.ts
export const config = billingConfigSchema.parse({
  ...
  plans: [
    {
      id: BillingPlan.PREMIUM,
      name: "Premium",
      description: "Become a power user and gain benefits",
      badge: "Bestseller",
      features: [
        "Unlimited projects",
        "Priority support",
        "Advanced integrations",
        "Team collaboration",
        "Analytics dashboard"
      ],
      prices: [
        {
          id: "premium-monthly",
          label: "Contact us!",
          href: "/contact",
          model: BillingModel.RECURRING,
          interval: RecurringInterval.MONTH,
          custom: true, 
        },
      ],
    },
  ],
  ...
}) satisfies BillingConfig;

As you can see, the plan now has a custom variant. The UI will display it in the pricing table, but it won't be available for purchase.

We do this by using the following fields:

  • custom: A flag to indicate that the plan is custom. This will prevent the plan from being available for purchase. It's set to false by default.
  • label: Displayed in the pricing table instead of a numeric amount.
  • href: The link to the page where the user goes when they click on the variant. This is used in the pricing table.

Translations supported!

All labels and descriptions can be translated using the internationalization feature. The UI will display the correct translation based on the user's locale.

index.ts
label: "common:contactUs",

To make strings translatable, make sure to provide the translation key in the config.

Discounts

Sometimes, you want to offer a discount to your users. This is done by adding a discount to the discounts array and pointing it at specific variant IDs via appliesTo.

index.ts
export const config = billingConfigSchema.parse({
  ...
  discounts: [
    {
      code: "50OFF",
      type: BillingDiscountType.PERCENT,
      off: 50,
      appliesTo: [
        "price_1PpUagFQH4McJDTlHwsCzOmyT6",
      ],
    },
  ],
  ...
}) satisfies BillingConfig;

Let's break down the fields:

  • code: The discount/promo code (e.g. "50OFF"). This must match the code configured in the billing provider.
  • type: The type of the discount (e.g. percent, amount, etc.)
  • off: The discount value (e.g. 50 for 50% off when type is percent)
  • appliesTo: The list of variant IDs this discount applies to

This data allows you to display the correct banner in the UI (e.g. β€œ10% off for the first 100 customers!”) and apply the discount to the correct variant at checkout.

Adding more products, plans and discounts

Simply add more plans, variants, and discounts to the config. The UI should handle most traditional cases; if you have a more complex billing setup, you may need to adjust the UI accordingly.

How is this guide?

Last updated on

On this page

Ship your startup everywhere. In minutes.Get TurboStarter