Billing Providers

Configure and use the unified billing client for payment processing

The SaaS Kit uses a provider abstraction for billing, allowing you to swap payment processors without changing application code.

Supported providers:

  • Stripe - Stripe is a full-featured payment processor with per-organization customers, entitlements, and usage meters
  • Polar - Polar is an up-and-coming developer-focused billing provider that acts as a merchant of record for your business

We use a unified BillingClient API to abstract the different providers and their API calls. The client provides:

  • Core operations: checkout, portal, subscriptions, cancel/restore
  • Plan limits: check and enforce countable quotas (seats, projects, storage)
  • Entitlements: boolean feature gating (Stripe Entitlements)
  • Usage meters: usage-based billing (Stripe Billing Meters)

We provide raw access to the provider SDK as an escape hatch, in case the API doesn't cover all your requirements.

Configuration

Set the billing provider via environment variable:

# Use Stripe (default)
NEXT_PUBLIC_BILLING_PROVIDER=stripe
# Use Polar
NEXT_PUBLIC_BILLING_PROVIDER=polar

Currently supported: stripe, polar

You also need NEXT_PUBLIC_SITE_URL set (absolute URL) for server-side return URLs used by billing actions.

Using the Billing Client

Get Billing Client Instance

The getBilling function returns a unified BillingClient that provides access to all billing operations:

import { getBilling } from '@kit/billing-api';
import { auth } from '@kit/better-auth';
// In a server action or loader
const billing = await getBilling(auth);

The auth parameter is required and should be your Better Auth instance.

Context: referenceId and customerType

Many billing APIs are contextual. Always decide:

  • referenceId: user id (personal) or organization id (org billing)
  • customerType: 'user' or 'organization'

For Stripe, customerType is especially important for per-organization customers (so the provider can resolve organizations.stripeCustomerId).

Create Checkout Session

const result = await billing.checkout({
userId: user.id,
planId: 'pro-monthly',
successUrl: '/billing/success',
cancelUrl: '/billing/cancel',
referenceId: org.id, // For organization billing
customerType: 'organization',
customer: {
email: user.email,
name: user.name,
},
metadata: {
source: 'upgrade-page',
},
});
// Redirect to result.url

Customer Portal

const portal = await billing.portal({
referenceId: user.id, // Required - user or organization ID
customerType: 'user', // use 'organization' for org billing
returnUrl: '/settings/billing',
});
// Redirect to portal.url

List Subscriptions

const { subscriptions, pagination } = await billing.listSubscriptions({
referenceId: user.id, // or organization ID
customerType: 'user', // use 'organization' for org billing
page: 1,
limit: 10,
});

Cancel Subscription

subscriptionId is provider implementation-defined. In the Makerkit UI flows, the value passed to cancel/restore comes from subscription.providerSubscriptionId returned by billing.listSubscriptions().

await billing.cancelSubscription({
subscriptionId: 'sub_xxx',
referenceId: user.id,
customerType: 'user',
});

Plan Limits

The billing client provides methods to check and enforce plan limits configured in your billing config.

Seat-based (Quantity) Billing

Makerkit supports quantity-based (“per-seat”) checkout for organizations by deriving a seat count from the organization member count during checkout (with a minimum of 1).

See:

If you want to require billing before orgs can invite/add members, implement it via the Policy API (see “Requiring a subscription” in Seat enforcement for organization invites).

Get Plan Limits

const { limits, hasSubscription } = await billing.getPlanLimits(referenceId);
// limits is a Record<string, number | null>
// null means unlimited
console.log(limits.seats); // e.g., 10
console.log(limits.projects); // e.g., null (unlimited)

Check Plan Limit

Use this before allowing actions that consume limited resources:

const result = await billing.checkPlanLimit({
referenceId: user.id,
limitKey: 'seats',
currentUsage: currentMemberCount,
});
if (!result.allowed) {
throw new Error(`Seat limit reached (${result.current}/${result.limit})`);
}
// result shape:
// {
// allowed: boolean, // Can the action proceed?
// limit: number | null, // The limit (null = unlimited)
// current: number, // Current usage
// remaining: number | null, // Remaining capacity
// hasSubscription: boolean // Has active subscription?
// }

Find Plan by Name

Look up plan configuration from the billing config:

const plan = billing.findPlanByName('pro-monthly');
if (plan) {
console.log(plan.cost); // 19.99
console.log(plan.limits); // { seats: 10 }
console.log(plan.priceId); // Stripe price ID
}

Entitlements (Feature Flags)

Entitlements provide boolean feature gating using Stripe Entitlements. Methods return safe defaults when the provider doesn't support entitlements.

Check Entitlement

const { entitled, source } = await billing.checkEntitlement(
customerId,
'advanced-analytics'
);
if (!entitled) {
throw new Error('Upgrade to access advanced analytics');
}
// source is 'provider' or 'unsupported'

Get All Entitlements

const entitlements = await billing.listEntitlements(customerId);
// Returns Entitlement[] or empty array if unsupported
for (const e of entitlements) {
console.log(e.lookupKey); // 'api-access', 'advanced-analytics', etc.
}

Usage Meters

Usage meters enable usage-based billing. Stripe uses Stripe Billing Meters; Polar uses Polar meters. Methods return safe defaults when unsupported.

Polar limitation: the kit’s Polar integration returns all-time usage (no time-range filtering).

Record Usage

const result = await billing.recordUsage(
'api_calls', // event name
customerId, // Provider customer ID (e.g. Stripe cus_…)
1, // value
);
if (!result.recorded) {
console.error(result.error);
}

Get Usage

const usage = await billing.getUsage(
'meter_123', // meter ID
customerId,
);
// Returns MeterUsageSummary[] or empty array
if (usage.length > 0) {
console.log(`Used ${usage[0].aggregatedValue} API calls this period`);
}

Provider Capabilities

Check what the current provider supports:

const { capabilities } = billing;
if (capabilities.supportsRestore) {
// Show restore button for canceled subscriptions
}
if (capabilities.supportsEntitlements) {
// Use entitlement-based feature gating
}
// Available capabilities:
// - supportsRestore: boolean
// - supportsCancel: boolean
// - supportsPortal: boolean
// - supportsOrganizations: boolean
// - supportsEntitlements: boolean
// - supportsUsageMeters: boolean

Escape Hatches

For advanced use cases not covered by the unified API:

Get Raw Provider

const provider = billing.getProvider();
// Returns the underlying BillingProvider instance

Get Provider Client

const stripe = billing.getProviderClient() as Stripe;
// Access the raw Stripe SDK
const invoice = await stripe.invoices.retrieve('inv_xxx');

Stripe Provider

The default provider uses Stripe via Better Auth's Stripe plugin.

Setup

  1. Configure Stripe keys:
STRIPE_SECRET_KEY=sk_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
  1. Create products and prices in Stripe Dashboard

  2. Map prices in billing config:

packages/billing/config/src/config.ts

import { BillingConfig } from '@kit/billing';
export const billingConfig: BillingConfig = {
products: [
{
id: 'pro',
name: 'Pro Plan',
description: 'For growing teams',
currency: 'USD',
features: ['All features', 'Priority support'],
plans: [
{
name: 'pro-monthly',
planId: process.env.STRIPE_PRICE_PRO_MONTHLY!,
displayName: 'Pro Monthly',
interval: 'month',
cost: 29,
},
],
},
],
};

Webhook Events

Stripe webhooks are handled automatically by Better Auth. Key events:

EventHandler
checkout.session.completedCreates subscription
customer.subscription.updatedUpdates subscription status
customer.subscription.deletedMarks subscription canceled
invoice.payment_failedTriggers payment failed hook

Polar Provider

Polar is a developer-focused payment processor that integrates via Better Auth's Polar plugin.

Setup

  1. Configure Polar credentials:
NEXT_PUBLIC_BILLING_PROVIDER=polar
POLAR_ACCESS_TOKEN=polar_at_xxx
POLAR_ENVIRONMENT=sandbox # or 'production'
POLAR_WEBHOOK_SECRET=whsec_xxx # optional
  1. Create products in Polar Dashboard

  2. Map products in billing config:

packages/billing/config/src/config.ts

export const billingConfig: BillingConfig = {
products: [
{
id: 'pro',
name: 'Pro Plan',
description: 'For growing teams',
currency: 'USD',
features: ['All features', 'Priority support'],
plans: [
{
name: 'pro-monthly',
planId: process.env.POLAR_PRODUCT_PRO_MONTHLY!,
displayName: 'Pro Monthly',
interval: 'month',
cost: 29,
},
],
},
],
};

Benefits

Polar supports “benefits”, but Makerkit does not currently map benefits to BillingClient entitlements (entitlements are Stripe-only in this kit). If you need benefits, use the raw Polar SDK via billing.getProviderClient().

Provider Capabilities Comparison

CapabilityStripePolar
Checkout
Customer Portal
Cancel (API)❌ (via portal)
Restore (API)❌ (via portal)
Entitlements
Usage Meters
Organizations (per-org customers)⚠️ Limited (no org customer context)
Trial Periods

Use billing.capabilities to check what the active provider supports:

const { capabilities } = billing;
// Only show cancel button if provider supports it
if (capabilities.supportsCancel) {
// Show cancel button
}

Next: Billing Configuration