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
- the Stripe Setup page
- and the Polar Setup page
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
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique product identifier |
name | string | Yes | Display name |
description | string | Yes | Short description |
currency | string | Yes | Currency code (USD, EUR, etc.) |
features | string[] | Yes | List of features included |
badge | string | No | Badge text (e.g., "Popular") |
highlighted | boolean | No | Highlight 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 perfectlyname: 'custom-plan-1' // Works perfectlyname: 'lifetime-deal' // Works perfectlyKey Point: You can rename plans freely without breaking the UI. The system trusts your metadata (displayName and interval), not naming conventions.
Plan Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique plan identifier (Better Auth ID - can be any string) |
planId | string | Yes | Provider plan identifier (Stripe Price ID like price_…, or Polar Product ID like prod_…) |
displayName | string | Recommended | Display name in UI (if omitted, falls back to capitalized name) |
interval | string | Yes | month or year - controls UI display |
cost | number | Yes | Price amount for display purposes |
limits | object | No | Plan limits (seats, storage, etc.) - enforced in your app |
freeTrial | object | No | Free 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_PROVIDERenv 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
trialingduring 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 identifierplanId- Provider plan identifier (Stripe price ID or Polar product ID)displayName- UI display nameinterval- Billing interval (monthoryear)cost- Price for displaylimits- 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.