Configuration

Configure billing for your application.

For the complete documentation index, see llms.txt. Prefer markdown by appending .md to documentation URLs or sending Accept: text/markdown.

The billing configuration schema mirrors the billing data your app needs, 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 shared across all web billing providers and lives in packages/billing/shared/src/config/schema.ts, with the default config exported from packages/billing/shared/src/config/index.ts. Some billing providers differ in what you can and cannot do. In those cases, the schema validates the supported shape, but it is still up to you to make sure your provider setup matches the data in your config.

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 active web billing provider, modify the exports in the packages/billing/web/src/providers directory. It defaults to Stripe.

export * from "./stripe";

It is important to set this correctly, because it determines which provider strategy and environment variables are used by @workspace/billing-web/server.

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: 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"
      ],
      variants: [],
    },
  ],
  ...
}) satisfies BillingConfig;

Let's break down the fields:

  • id: The internal identifier for the plan. In the default setup this uses the built-in BillingPlan enum (free, premium, enterprise). It does not need to match anything in the billing provider, but it should stay stable because it is used throughout the app for plan logic and access control.
  • 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, and they can represent flat, per-seat, or metered billing depending on their type.

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",
          type: BillingType.FLAT,
          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)
  • type: The billing type for this variant. If omitted on a custom variant it defaults to flat.

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). This is useful for grandfathered prices, mobile-only variants, or internal migration paths.

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"
      ],
      variants: [
        {
          id: "premium-monthly",
          label: "Contact us!",
          href: "/contact",
          type: BillingType.FLAT,
          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.Try TurboStarter