Configuring Metered Usage Billing | Next.js Supabase SaaS Kit Turbo
Metered usage billing is a billing model where you charge your customers based on their usage of your product. This model is common in APIs, where you charge based on the number of requests made to your API.
Metered usage billing is a billing model where you charge your customers based on their usage of your product. This model is common in APIs, where you charge based on the number of requests made to your API.
As we have already seen in the schema definition section, we can define a metered usage plan in the billing schema. This plan will charge the customer based on the number of requests they make to your API.
Providers Differences
NB: different providers (Stripe, Lemon Squeezy, etc.) have different ways of handling metered usage billing: we keep the API consistent across all providers - but the details of how you report usage to the billing provider may differ. Please read the provider's documentation to understand how to report usage to the billing provider.
Defining a Metered Usage Plan
Let's assume we have the following schema:
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, } ] }, ], } ], } ]});
What happens here is that we create a checkout in Stripe that charges the customer based on the number of GBs they use. The first 10 GBs are free, the next 90 GBs are charged at $0.05 per GB, and anything above 100 GBs is charged at $0.01 per GB.
When the checkout succeeds, we store two records:
- A
subscriptions
record that represents the subscription the customer has to the plan. This is the overall subscription record - and contains details like the customer's ID, the plan ID, the status of the subscription, etc. - A
subscription_items
record that represents the line item the customer is subscribed to. This is needed for reporting charges to the billing provider.
The billing service in Makerkit has a unified interface for interacting with your billing provider - whichever it may be. This means that you can switch from Stripe to Lemon Squeezy or any other billing provider without changing your code. As such - this is valid for all billing providers supported by Makerkit.
When we report a charge, we need three things:
- the subscription ID (and billing provider it is associated with) or the Customer ID (Stripe)
- the line item ID (Lemon Squeezy only)
- the next quantity of the line item
The billing service will then calculate the charge based on the quantity and the cost of the line item and charge the user accordingly.
On our side, we need to identify what the user is being charged for. This is why we store the subscription_items
record. This record contains the subscription_id
, the product_id
, the variant_id
(price ID in Stripe) and the line_item_id
- which we can use to identify what the user is being charged for. You can query the items using these details to retrieve the ID of the line item the user is being charged for.
We assume an account uses a function consumeApi
to consume the API. This (hypothetical) function will be called every time the user makes a request to the API. We can then use this function to report the usage to the billing provider.
export async function consumeApi(accountId: string): number {}
When the user makes a request to the API, we can call this function to report the usage to the billing provider.
async function apiHandler(accountId: string) { try { // assume consumeApi returns the number of requests made const quantity = await consumeApi(accountId); await reportUsage(accountId, quantity); } catch (error) { console.error(error); }}
NB: this is meta-code. These functions need to be implemented by you.
Reporting Usage in Stripe
Makerkit uses the newest usage reporting API in Stripe. This API allows you to report usage for a subscription item. You can report usage for a subscription item by calling the reportUsage
method on the billing service.
Unlike the previous version of the API, you don't need a subscription item ID, but the customer ID and the metric name you're reporting.
We assume you have created a metric in Stripe named api_requests
that you use to report the usage. Let's create a function that reports the usage for this metric caling it reportUsageForApiRequests
:
import { getBillingGatewayProvider } from '@kit/billing-gateway';import { getSupabaseServerClient } from '@kit/supabase/server-client';import { createAccountsApi } from '@kit/accounts/api';async function reportUsageForApiRequests( accountId: string, quantity: number) { // use the correct client: in this case, the server action client const client = getSupabaseServerClient(); const api = createAccountsApi(client); const subscription = await api.getSubscription(accountId); // if the subscription is not active, we don't report usage if (!subscription) { throw new Error('No active subscription found'); } // get the billing provider const service = await getBillingGatewayProvider(this.client); const customerId = await api.getCustomerId(accountId); if (!customerId) { throw new Error(`No customer ID found for account ${accountId}`); } // now we can report the usage to the billing provider return service.reportUsage({ id: customerId, eventName: 'api_requests', usage: { quantity, } });}
As you can see, we use the following parameters to report usage:
id
: the customer ID of the user/account in StripeeventName
: the name of the metric you're reportingusage
: the quantity of the metric you're reporting
NB: You need to implement the reportUsageForApiRequests
function in your code. The above is just an example.
Reporting Usage in Lemon Squeezy
In Lemon Squeezy, you need to report usage for a subscription item.
Given an account ID - we need to retrieve the subscription ID and the line item ID. We can then report the usage to the billing provider.
import { getBillingGatewayProvider } from '@kit/billing-gateway';import { getSupabaseServerClient } from '@kit/supabase/server-client';import { createAccountsApi } from '@kit/accounts/api';async function reportUsageForApiRequests( accountId: string, quantity: number) { // use the correct client: in this case, the server action client const client = getSupabaseServerClient(); const api = createAccountsApi(client); const subscription = await api.getSubscription(accountId); // if the subscription is not active, we don't report usage if (!subscription) { console.error('No active subscription found'); return; } // now, we need to find the line item the user is being charged for // let's use Supabase for this! // we use the product ID to identify the line item // in your case, you have more choices to identify the line item const { data: subscriptionItem, error } = await client.from('subscription_items') .select('id') .eq('subscription_id', subscription.id) .eq('product_id', 'starter-pro') .eq('type', 'metered') .single(); // get the billing provider const service = await getBillingGatewayProvider(this.client); // now we can report the usage to the billing provider return service.reportUsage({ id: subscriptionItem.id, usage: { quantity, action: 'increment' } });}