Writing the billing schema in the Next.js Supabase Kit
Learn how to configure the plans in your Makerkit application
The billing schema replicates your billing provider's schema, so that:
- we can display the data in the UI (pricing table, billing section, etc.)
- create the correct checkout session
- make some features work correctly - such as per-seat billing
The billing schema is common to all billing providers. Some billing providers have some differences in what you can or cannot do. In these cases, the schema will try to validate and enforce the rules - but it's up to you to make sure the data is correct.
The schema is based on three main entities:
- Products: The main product you are selling (e.g., "Pro Plan", "Starter Plan", etc.)
- Plans: The pricing plan for the product (e.g., "Monthly", "Yearly", etc.)
- Line Items: The line items for the plan (e.g., "flat subscription", "metered usage", "per seat", etc.)
Getting the schema right is important
Getting the IDs of your plans is extremely important - as these are used to:
- create the correct checkout
- populate the data in the DB
Please take it easy while you configure this, do one step at a time, and test it thoroughly.
Setting the Billing Provider
The billing provider is already set as process.env.NEXT_PUBLIC_BILLING_PROVIDER
and defaults to stripe
.
For clarity - this is set in the apps/web/config/billing.config.ts
file:
export default createBillingSchema({ // also update config.billing_provider in the DB to match the selected provider, // products configuration products: []});
We will now add the products to the configuration.
Products
Products are the main product you are selling. They are defined by the following fields:
export default createBillingSchema({ provider, products: [ { id: 'starter', name: 'Starter', description: 'The perfect plan to get started', currency: 'USD', badge: `Value`, plans: [], } ]});
Let's break down the fields:
- id: The unique identifier for the product. This is chosen by you, it doesn't need to be the same one as the one in the provider.
- name: The name of the product
- description: The description of the product
- currency: The currency of the product
- badge: A badge to display on the product (e.g., "Value", "Popular", etc.)
The majority of these fields are going to populate the pricing table in the UI.
Plans
Plans are the pricing plans for the product. They are defined by the following fields:
export default createBillingSchema({ provider, products: [ { id: 'starter', name: 'Starter', description: 'The perfect plan to get started', currency: 'USD', badge: `Value`, plans: [ { name: 'Starter Monthly', id: 'starter-monthly', trialDays: 7, paymentType: 'recurring', interval: 'month', lineItems: [], } ], } ]});
Let's break down the fields:
- name: The name of the plan
- id: The unique identifier for the plan. This is chosen by you, it doesn't need to be the same one as the one in the provider.
- trialDays: The number of days for the trial period
- paymentType: The payment type (e.g.,
recurring
,one-time
) - interval: The interval of the payment (e.g.,
month
,year
) - lineItems: The line items for the plan
Now, we will be looking at the line items. The line items are the items that make up the plan, and can be of different types:
- Flat Subscription: A flat subscription (e.g., $10/month) - specified as
flat
- Metered Billing: Metered billing (e.g., $0.10 per 1,000 requests) - specified as
metered
- Per-Seat Billing: Per-seat billing (e.g., $10 per seat) - specified as
per-seat
You can add one or more line items to the plan when using Stripe. When using Lemon Squeezy, you can only add one line item - but you can decorate it with the necessary metadata to achieve a similar result.
Flat Subscriptions
Flat subscriptions are defined by the following fields:
export default createBillingSchema({ provider, products: [ { id: 'starter', name: 'Starter', description: 'The perfect plan to get started', currency: 'USD', badge: `Value`, plans: [ { name: 'Starter Monthly', id: 'starter-monthly', trialDays: 7, paymentType: 'recurring', interval: 'month', lineItems: [ { id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe', name: 'Addon 2', cost: 9.99, type: 'flat', }, ], } ], } ]});
Let's break down the fields:
- id: The unique identifier for the line item. This must match the price ID in the billing provider. The schema will validate this, but please remember to set it correctly.
- name: The name of the line item
- cost: The cost of the line item
- type: The type of the line item (e.g.,
flat
,metered
,per-seat
). In this case, it'sflat
.
The cost is set for UI purposes. The billing provider will handle the actual billing - therefore, please make sure the cost is correctly set in the billing provider.
Metered Billing
Metered billing is defined by the following fields:
export default createBillingSchema({ provider, products: [ { id: 'starter', name: 'Starter', description: 'The perfect plan to get started', currency: 'USD', badge: `Value`, plans: [ { name: 'Starter Monthly', id: 'starter-monthly', trialDays: 7, paymentType: 'recurring', interval: 'month', lineItems: [ { id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe', name: 'Addon 2', cost: 0, type: 'metered', unit: 'GBs', tiers: [ { upTo: 10, cost: 0.1, }, { upTo: 100, cost: 0.05, }, { upTo: 'unlimited', cost: 0.01, } ] }, ], } ], } ]});
Let's break down the fields:
- id: The unique identifier for the line item. This must match the price ID in the billing provider. The schema will validate this, but please remember to set it correctly.
- name: The name of the line item
- cost: The cost of the line item. This can be set to
0
as the cost is calculated based on the tiers. - type: The type of the line item (e.g.,
flat
,metered
,per-seat
). In this case, it'smetered
. - unit: The unit of the line item (e.g.,
GBs
,requests
, etc.). You can use a translation key here. - tiers: The tiers of the line item. Each tier is defined by the following fields:
- upTo: The upper limit of the tier. If the usage is below this limit, the cost is calculated based on this tier.
- cost: The cost of the tier. This is the cost per unit.
The tiers data is used exclusively for UI purposes. The billing provider will handle the actual billing - therefore, please make sure the tiers are correctly set in the billing provider.
Per-Seat Billing
Per-seat billing is defined by the following fields:
export default createBillingSchema({ provider, products: [ { id: 'starter', name: 'Starter', description: 'The perfect plan to get started', currency: 'USD', badge: `Value`, plans: [ { name: 'Starter Monthly', id: 'starter-monthly', trialDays: 7, paymentType: 'recurring', interval: 'month', lineItems: [ { id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe', name: 'Addon 2', cost: 0, type: 'per_seat', tiers: [ { upTo: 3, cost: 0, }, { upTo: 5, cost: 7.99, }, { upTo: 'unlimited', cost: 5.99, } ] }, ], } ], } ]});
Let's break down the fields:
- id: The unique identifier for the line item. This must match the price ID in the billing provider. The schema will validate this, but please remember to set it correctly.
- name: The name of the line item
- cost: The cost of the line item. This can be set to
0
as the cost is calculated based on the tiers. - type: The type of the line item (e.g.,
flat
,metered
,per-seat
). In this case, it'sper-seat
. - tiers: The tiers of the line item. Each tier is defined by the following fields:
- upTo: The upper limit of the tier. If the usage is below this limit, the cost is calculated based on this tier.
- cost: The cost of the tier. This is the cost per unit.
If you set the first tier to 0
, it basically means that the first n
seats are free. This is a common practice in per-seat billing.
Please remember that the cost is set for UI purposes. The billing provider will handle the actual billing - therefore, please make sure the cost is correctly set in the billing provider.
One-Off Payments
One-off payments are defined by the following fields:
export default createBillingSchema({ provider, products: [ { id: 'starter', name: 'Starter', description: 'The perfect plan to get started', currency: 'USD', badge: `Value`, plans: [ { name: 'Starter Monthly', id: 'starter-monthly', paymentType: 'one-time', lineItems: [ { id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe', name: 'Addon 2', cost: 9.99, type: 'flat', }, ], } ], } ]});
Let's break down the fields:
- name: The name of the plan
- id: The unique identifier for the line item. This must match the price ID in the billing provider. The schema will validate this, but please remember to set it correctly.
- paymentType: The payment type (e.g.,
recurring
,one-time
). In this case, it'sone-time
. - lineItems: The line items for the plan
- id: The unique identifier for the line item. This must match the price ID in the billing provider. The schema will validate this, but please remember to set it correctly.
- name: The name of the line item
- cost: The cost of the line item
- type: The type of the line item (e.g.,
flat
). It can only beflat
for one-off payments.
Adding more Products, Plans, and Line Items
Simply add more products, plans, and line items to the arrays. The UI should be able to handle it in most traditional cases. If you have a more complex billing schema, you may need to adjust the UI accordingly.
Custom Plans
Sometimes - you want to display a plan 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 plan:
{ name: 'Enterprise Monthly', id: 'enterprise-monthly', paymentType: 'recurring', label: 'common:contactUs', href: '/contact', custom: true, interval: 'month', lineItems: [],}
Here's the full example:
export default createBillingSchema({ provider, products: [ { id: 'starter', name: 'Starter', description: 'The perfect plan to get started', currency: 'USD', badge: `Value`, plans: [ { name: 'Enterprise', id: 'enterprise', paymentType: 'recurring', label: 'common:contactUs', href: '/contact', custom: true, interval: 'month', lineItems: [], } ], } ]});
As you can see, the plan is now a custom plan. The UI will display the plan in the pricing table, but it won't be available for purchase.
We do this by adding the following fields:
- custom: A flag to indicate that the plan is custom. This will prevent the plan from being available for purchase.
- label: The translation key for the label. This is used to display the label in the pricing table.
- href: The link to the page where the user can contact you. This is used in the pricing table.
- lineItems: The line items for the plan. This is empty as there are no line items for the plan. It must be an empty array.
Custom Button Label
You can also provide a custom button label for the plan. This is done by adding the buttonLabel
field:
{ name: 'Enterprise', id: 'enterprise', paymentType: 'recurring', label: 'common:contactUs', href: '/contact', custom: true, interval: 'month', lineItems: [], buttonLabel: 'common:contactUs',}
As usual, strings can either be a translation key or a string. If it's a translation key, it will be translated using the i18n
library.