Billing Configuration

Configure your pricing plans, products, and billing settings

The billing system is configured through a centralized configuration file that defines your products, plans, features, and pricing.

Before you venture into updating the billing configuration, you should create a Stripe or Polar account and configure the billing provider in the environment variables - otherwise, you won't be able to actually use the billing system - as it requires secret keys and actual IDs for the products and plans.

You can read more about how to configure the billing provider in

Configuration File

The main billing configuration is located at packages/billing/config/src/config.ts.

By default, the billing configuration has some placeholder products and plans (just to give you an idea of how to configure it) - you will want to replace them with your own.

Have you already set up the billing provider and configured the environment variables? Good! We can now start configuring the billing configuration.

At a very basic level, the billing configuration is an object with a products array. Each product has a name, description, currency, features, and plans array:

packages/billing/config/src/config.ts

import { BillingConfig } from '@kit/billing';
export const billingConfig: BillingConfig = {
products: [
{
id: 'starter',
name: 'Starter',
description: 'Perfect for individuals and small teams',
currency: 'USD',
badge: 'Value',
features: [
'Up to 3 team members',
'Core features',
'Email support',
],
plans: [
{
name: 'starter-monthly',
planId: process.env.STRIPE_PRICE_STARTER_MONTHLY!,
displayName: 'Starter Monthly',
interval: 'month',
cost: 9.99,
limits: { seats: 3 },
},
],
},
],
};

Product Configuration

Each product represents a pricing tier with multiple billing intervals.

Product Fields

FieldTypeRequiredDescription
idstringYesUnique product identifier
namestringYesDisplay name
descriptionstringYesShort description
currencystringYesCurrency code (USD, EUR, etc.)
featuresstring[]YesList of features included
badgestringNoBadge text (e.g., "Popular")
highlightedbooleanNoHighlight this product in UI

Example Product

Below is an example of a product configuration:

{
id: 'pro',
name: 'Pro',
badge: 'Popular',
highlighted: true,
description: 'Best for growing teams and professionals',
currency: 'USD',
features: [
'Up to 10 team members',
'All Starter features',
'Priority support',
'Advanced analytics',
],
plans: [
// ... plan configuration
],
}

Plan Configuration

Each product can have multiple plans with different billing intervals.

Important: Plan Naming Flexibility

The name field is used as the Better Auth plan identifier and can be any string you choose. The UI uses displayName and interval to determine how the plan is displayed - it does not parse the plan name.

This means you have complete freedom to name your plans:

{
name: 'founder-yearly', // Better Auth identifier (can be anything)
displayName: 'Founder', // UI displays "Founder"
interval: 'year', // UI shows "(Annual)"
cost: 99.99,
// Result: UI displays "Founder (Annual)"
}
// Other valid naming examples:
name: 'vip-annual' // Works perfectly
name: 'custom-plan-1' // Works perfectly
name: 'lifetime-deal' // Works perfectly

Key Point: You can rename plans freely without breaking the UI. The system trusts your metadata (displayName and interval), not naming conventions.

Plan Fields

FieldTypeRequiredDescription
namestringYesUnique plan identifier (Better Auth ID - can be any string)
planIdstringYesProvider plan identifier (Stripe Price ID like price_…, or Polar Product ID like prod_…)
displayNamestringRecommendedDisplay name in UI (if omitted, falls back to capitalized name)
intervalstringYesmonth or year - controls UI display
costnumberYesPrice amount for display purposes
limitsobjectNoPlan limits (seats, storage, etc.) - enforced in your app
freeTrialobjectNoFree trial configuration (e.g. { days: 14 })

Example Plans

plans: [
{
name: 'pro-monthly',
planId: process.env.STRIPE_PRICE_PRO_MONTHLY!,
displayName: 'Pro Monthly',
interval: 'month',
cost: 19.99,
limits: { seats: 10 },
},
{
name: 'pro-yearly',
planId: process.env.STRIPE_PRICE_PRO_YEARLY!,
displayName: 'Pro Yearly',
interval: 'year',
cost: 199.99,
limits: { seats: 10 },
},
]

Important notes:

  • Only the active provider's configuration is used at runtime (set via NEXT_PUBLIC_BILLING_PROVIDER env var)
  • Use environment variables for IDs to easily switch between test/production environments

Plan IDs (Provider-Specific)

The planId field means different things depending on the provider:

  • Stripe: a Price ID (price_…)
  • Polar: a Product ID (prod_…)

You can name the environment variables however you want. The key is that planId must be the correct identifier for the active provider.

Stripe example (Price IDs)

You can use environment variables to store the plan IDs so that you can easily switch between test/production environments:

STRIPE_PRICE_STARTER_MONTHLY=price_...
STRIPE_PRICE_STARTER_YEARLY=price_...
STRIPE_PRICE_PRO_MONTHLY=price_...
STRIPE_PRICE_PRO_YEARLY=price_...

Then reference them in your config:

planId: process.env.STRIPE_PRICE_STARTER_MONTHLY!,

Polar example (Product IDs)

Similarly, you can use environment variables to store the product IDs when using Polar:

POLAR_PRODUCT_STARTER_MONTHLY=prod_...
POLAR_PRODUCT_STARTER_YEARLY=prod_...

Then reference them in your config:

planId: process.env.POLAR_PRODUCT_STARTER_MONTHLY!,

Free Trials

Add a free trial to any plan by setting freeTrial:

{
name: 'pro-monthly',
planId: process.env.STRIPE_PRICE_PRO_MONTHLY!,
interval: 'month',
cost: 19.99,
freeTrial: { days: 14 },
}

Notes:

  • Trials are handled by Better Auth (including multi-trial prevention).
  • The subscription status will typically be trialing during the trial.
  • In Polar, please specify the free trial during the product setup

Plan Limits

Define usage limits for each plan:

limits: {
seats: 10, // Maximum team members
projects: 50, // Maximum projects
storage: 100, // Storage in GB
apiCalls: 10000, // API calls per month
}

Complete Example

Here's a complete three-tier pricing configuration:

import { BillingConfig } from '@kit/billing';
export const billingConfig: BillingConfig = {
products: [
{
id: 'starter',
name: 'Starter',
description: 'Perfect for individuals and small teams',
currency: 'USD',
badge: 'Value',
features: [
'Up to 3 team members',
'Core features',
'Email support',
'14-day free trial',
],
plans: [
{
name: 'starter-monthly',
planId: process.env.STRIPE_PRICE_STARTER_MONTHLY!,
displayName: 'Starter Monthly',
interval: 'month',
cost: 9.99,
limits: { seats: 3 },
},
{
name: 'starter-yearly',
planId: process.env.STRIPE_PRICE_STARTER_YEARLY!,
displayName: 'Starter Yearly',
interval: 'year',
cost: 99.99,
limits: { seats: 3 },
},
],
},
{
id: 'pro',
name: 'Pro',
badge: 'Popular',
highlighted: true,
description: 'Best for growing teams',
currency: 'USD',
features: [
'Up to 10 team members',
'All Starter features',
'Priority support',
'Advanced analytics',
],
plans: [
{
name: 'pro-monthly',
planId: process.env.STRIPE_PRICE_PRO_MONTHLY!,
displayName: 'Pro Monthly',
interval: 'month',
cost: 19.99,
limits: { seats: 10 },
},
{
name: 'pro-yearly',
planId: process.env.STRIPE_PRICE_PRO_YEARLY!,
displayName: 'Pro Yearly',
interval: 'year',
cost: 199.99,
limits: { seats: 10 },
},
],
},
{
id: 'enterprise',
name: 'Enterprise',
description: 'For large organizations',
currency: 'USD',
features: [
'Unlimited team members',
'All Pro features',
'Dedicated support',
'Custom integrations',
],
plans: [
{
name: 'enterprise-monthly',
planId: process.env.STRIPE_PRICE_ENTERPRISE_MONTHLY!,
displayName: 'Enterprise Monthly',
interval: 'month',
cost: 49.99,
limits: { seats: null }, // Unlimited
},
],
},
],
};

Supported vs. Reserved Fields

The billing configuration includes several fields in the TypeScript types. Here's what's currently supported:

Supported Fields (Ready to Use)

  • name - Plan identifier
  • planId - Provider plan identifier (Stripe price ID or Polar product ID)
  • displayName - UI display name
  • interval - Billing interval (month or year)
  • cost - Price for display
  • limits - Feature limits (seats, storage, etc.)
  • hidden - Hide product from UI (for grandfathered plans)

Hidden Products (Grandfathered Plans)

You can hide products from the plan picker UI while keeping them active for existing subscribers:

{
id: 'legacy-pro',
name: 'Legacy Pro',
description: 'Original Pro plan for early customers',
currency: 'USD',
hidden: true, // Hides from plan picker UI
features: [
'All Pro features',
'Grandfathered pricing',
],
plans: [
{
name: 'legacy-pro-monthly',
planId: process.env.STRIPE_PRICE_LEGACY_PRO_MONTHLY!,
displayName: 'Legacy Pro',
interval: 'month',
cost: 9.99, // Old pricing
},
],
}

Use Cases:

  • Grandfathered pricing for early customers
  • Deprecated plans still in use
  • Custom enterprise agreements
  • Limited-time promotional pricing

Hidden products remain fully functional for existing subscriptions but won't appear when users browse available plans.