Next.js Supabase V3 (#463)
Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
632
docs/billing/billing-schema.mdoc
Normal file
632
docs/billing/billing-schema.mdoc
Normal file
@@ -0,0 +1,632 @@
|
||||
---
|
||||
status: "published"
|
||||
label: "Billing Schema"
|
||||
title: "Configure SaaS Pricing Plans with the Billing Schema"
|
||||
order: 1
|
||||
description: "Define your SaaS pricing with Makerkit's billing schema. Configure products, plans, flat subscriptions, per-seat pricing, metered usage, and one-off payments for Stripe, Lemon Squeezy, or Paddle."
|
||||
---
|
||||
|
||||
The billing schema defines your products and pricing in a single configuration file. This schema drives the pricing table UI, checkout sessions, and subscription management across all supported providers (Stripe, Lemon Squeezy, Paddle).
|
||||
|
||||
## Schema Structure
|
||||
|
||||
The schema has three levels:
|
||||
|
||||
```
|
||||
Products (what you sell)
|
||||
└── Plans (pricing options: monthly, yearly)
|
||||
└── Line Items (how you charge: flat, per-seat, metered)
|
||||
```
|
||||
|
||||
**Example:** A "Pro" product might have "Pro Monthly" and "Pro Yearly" plans. Each plan has line items defining the actual charges.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Create or edit `apps/web/config/billing.config.ts`:
|
||||
|
||||
```tsx
|
||||
import { createBillingSchema } from '@kit/billing';
|
||||
|
||||
const provider = process.env.NEXT_PUBLIC_BILLING_PROVIDER ?? 'stripe';
|
||||
|
||||
export default createBillingSchema({
|
||||
provider,
|
||||
products: [
|
||||
{
|
||||
id: 'pro',
|
||||
name: 'Pro',
|
||||
description: 'For growing teams',
|
||||
currency: 'USD',
|
||||
badge: 'Popular',
|
||||
plans: [
|
||||
{
|
||||
id: 'pro-monthly',
|
||||
name: 'Pro Monthly',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_xxxxxxxxxxxxx', // Your Stripe Price ID
|
||||
name: 'Pro Plan',
|
||||
cost: 29,
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pro-yearly',
|
||||
name: 'Pro Yearly',
|
||||
paymentType: 'recurring',
|
||||
interval: 'year',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_yyyyyyyyyyyyy', // Your Stripe Price ID
|
||||
name: 'Pro Plan',
|
||||
cost: 290,
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
{% alert type="warning" title="Match IDs exactly" %}
|
||||
Line item `id` values **must match** the Price IDs in your billing provider (Stripe, Lemon Squeezy, or Paddle). The schema validates this format but cannot verify the IDs exist in your provider account.
|
||||
{% /alert %}
|
||||
|
||||
## Setting the Billing Provider
|
||||
|
||||
Set the provider via environment variable:
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_BILLING_PROVIDER=stripe # or lemon-squeezy, paddle
|
||||
```
|
||||
|
||||
Also update the database configuration:
|
||||
|
||||
```sql
|
||||
UPDATE public.config SET billing_provider = 'stripe';
|
||||
```
|
||||
|
||||
The provider determines which API is called when creating checkouts, managing subscriptions, and processing webhooks.
|
||||
|
||||
## Products
|
||||
|
||||
Products represent what you're selling (e.g., "Starter", "Pro", "Enterprise"). Each product can have multiple plans with different billing intervals.
|
||||
|
||||
```tsx
|
||||
{
|
||||
id: 'pro',
|
||||
name: 'Pro',
|
||||
description: 'For growing teams',
|
||||
currency: 'USD',
|
||||
badge: 'Popular',
|
||||
highlighted: true,
|
||||
enableDiscountField: true,
|
||||
features: [
|
||||
'Unlimited projects',
|
||||
'Priority support',
|
||||
'Advanced analytics',
|
||||
],
|
||||
plans: [/* ... */],
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `id` | Yes | Unique identifier (your choice, not the provider's ID) |
|
||||
| `name` | Yes | Display name in pricing table |
|
||||
| `description` | Yes | Short description shown to users |
|
||||
| `currency` | Yes | ISO currency code (e.g., "USD", "EUR") |
|
||||
| `plans` | Yes | Array of pricing plans |
|
||||
| `badge` | No | Badge text (e.g., "Popular", "Best Value") |
|
||||
| `highlighted` | No | Visually highlight this product |
|
||||
| `enableDiscountField` | No | Show coupon/discount input at checkout |
|
||||
| `features` | No | Feature list for pricing table |
|
||||
| `hidden` | No | Hide from pricing table (for legacy plans) |
|
||||
|
||||
The `id` is your internal identifier. It doesn't need to match anything in Stripe or your payment provider.
|
||||
|
||||
## Plans
|
||||
|
||||
Plans define pricing options within a product. Typically, you'll have monthly and yearly variants.
|
||||
|
||||
```tsx
|
||||
{
|
||||
id: 'pro-monthly',
|
||||
name: 'Pro Monthly',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
trialDays: 14,
|
||||
lineItems: [/* ... */],
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `id` | Yes | Unique identifier (your choice) |
|
||||
| `name` | Yes | Display name |
|
||||
| `paymentType` | Yes | `'recurring'` or `'one-time'` |
|
||||
| `interval` | Recurring only | `'month'` or `'year'` |
|
||||
| `lineItems` | Yes | Array of line items (charges) |
|
||||
| `trialDays` | No | Free trial period in days |
|
||||
| `custom` | No | Mark as custom/enterprise plan (see below) |
|
||||
| `href` | Custom only | Link for custom plans |
|
||||
| `label` | Custom only | Button label for custom plans |
|
||||
| `buttonLabel` | No | Custom checkout button text |
|
||||
|
||||
**Plan ID validation:** The schema validates that plan IDs are unique across all products.
|
||||
|
||||
## Line Items
|
||||
|
||||
Line items define how you charge for a plan. Makerkit supports three types:
|
||||
|
||||
| Type | Use Case | Example |
|
||||
|------|----------|---------|
|
||||
| `flat` | Fixed recurring price | $29/month |
|
||||
| `per_seat` | Per-user pricing | $10/seat/month |
|
||||
| `metered` | Usage-based pricing | $0.01 per API call |
|
||||
|
||||
**Provider limitations:**
|
||||
|
||||
- **Stripe:** Supports multiple line items per plan (mix flat + per-seat + metered)
|
||||
- **Lemon Squeezy:** One line item per plan only
|
||||
- **Paddle:** Flat and per-seat only (no metered billing)
|
||||
|
||||
### Flat Subscriptions
|
||||
|
||||
The most common pricing model. A fixed amount charged at each billing interval.
|
||||
|
||||
```tsx
|
||||
{
|
||||
id: 'pro-monthly',
|
||||
name: 'Pro Monthly',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe', // Stripe Price ID
|
||||
name: 'Pro Plan',
|
||||
cost: 29,
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `id` | Yes | **Must match** your provider's Price ID |
|
||||
| `name` | Yes | Display name |
|
||||
| `cost` | Yes | Price (for UI display only) |
|
||||
| `type` | Yes | `'flat'` |
|
||||
|
||||
{% alert type="default" title="Cost is for display only" %}
|
||||
The `cost` field is used for the pricing table UI. The actual charge comes from your billing provider. Make sure they match to avoid confusing users.
|
||||
{% /alert %}
|
||||
|
||||
### Metered Billing
|
||||
|
||||
Charge based on usage (API calls, storage, tokens). You report usage through the billing API, and the provider calculates charges at the end of each billing period.
|
||||
|
||||
```tsx
|
||||
{
|
||||
id: 'api-monthly',
|
||||
name: 'API Monthly',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe',
|
||||
name: 'API Requests',
|
||||
cost: 0,
|
||||
type: 'metered',
|
||||
unit: 'requests',
|
||||
tiers: [
|
||||
{ upTo: 1000, cost: 0 }, // First 1000 free
|
||||
{ upTo: 10000, cost: 0.001 }, // $0.001 per request
|
||||
{ upTo: 'unlimited', cost: 0.0005 }, // Volume discount
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `id` | Yes | **Must match** your provider's Price ID |
|
||||
| `name` | Yes | Display name |
|
||||
| `cost` | Yes | Base cost (usually 0 for metered) |
|
||||
| `type` | Yes | `'metered'` |
|
||||
| `unit` | Yes | Unit label (e.g., "requests", "GBs", "tokens") |
|
||||
| `tiers` | Yes | Array of pricing tiers |
|
||||
|
||||
**Tier structure:**
|
||||
|
||||
```tsx
|
||||
{
|
||||
upTo: number | 'unlimited', // Usage threshold
|
||||
cost: number, // Cost per unit in this tier
|
||||
}
|
||||
```
|
||||
|
||||
The last tier should always have `upTo: 'unlimited'`.
|
||||
|
||||
{% alert type="warning" title="Provider-specific metered billing" %}
|
||||
Stripe and Lemon Squeezy handle metered billing differently. See the [metered usage guide](/docs/next-supabase-turbo/billing/metered-usage) for provider-specific implementation details.
|
||||
{% /alert %}
|
||||
|
||||
### Per-Seat Billing
|
||||
|
||||
Charge based on team size. Makerkit automatically updates seat counts when members are added or removed from a team account.
|
||||
|
||||
```tsx
|
||||
{
|
||||
id: 'team-monthly',
|
||||
name: 'Team Monthly',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe',
|
||||
name: 'Team Seats',
|
||||
cost: 0,
|
||||
type: 'per_seat',
|
||||
tiers: [
|
||||
{ upTo: 3, cost: 0 }, // First 3 seats free
|
||||
{ upTo: 10, cost: 12 }, // $12/seat for 4-10
|
||||
{ upTo: 'unlimited', cost: 10 }, // Volume discount
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `id` | Yes | **Must match** your provider's Price ID |
|
||||
| `name` | Yes | Display name |
|
||||
| `cost` | Yes | Base cost (usually 0 for tiered) |
|
||||
| `type` | Yes | `'per_seat'` |
|
||||
| `tiers` | Yes | Array of pricing tiers |
|
||||
|
||||
**Common patterns:**
|
||||
|
||||
```tsx
|
||||
// Free tier + flat per-seat
|
||||
tiers: [
|
||||
{ upTo: 5, cost: 0 }, // 5 free seats
|
||||
{ upTo: 'unlimited', cost: 15 },
|
||||
]
|
||||
|
||||
// Volume discounts
|
||||
tiers: [
|
||||
{ upTo: 10, cost: 20 }, // $20/seat for 1-10
|
||||
{ upTo: 50, cost: 15 }, // $15/seat for 11-50
|
||||
{ upTo: 'unlimited', cost: 10 },
|
||||
]
|
||||
|
||||
// Flat price (no tiers)
|
||||
tiers: [
|
||||
{ upTo: 'unlimited', cost: 10 },
|
||||
]
|
||||
```
|
||||
|
||||
Makerkit handles seat count updates automatically when:
|
||||
- A new member joins the team
|
||||
- A member is removed from the team
|
||||
- A member invitation is accepted
|
||||
|
||||
[Full per-seat billing guide →](/docs/next-supabase-turbo/billing/per-seat-billing)
|
||||
|
||||
### One-Off Payments
|
||||
|
||||
Single charges for lifetime access, add-ons, or credits. One-off payments are stored in the `orders` table instead of `subscriptions`.
|
||||
|
||||
```tsx
|
||||
{
|
||||
id: 'lifetime',
|
||||
name: 'Lifetime Access',
|
||||
paymentType: 'one-time',
|
||||
// No interval for one-time payments
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe',
|
||||
name: 'Lifetime Access',
|
||||
cost: 299,
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**Key differences from subscriptions:**
|
||||
|
||||
- `paymentType` must be `'one-time'`
|
||||
- No `interval` field
|
||||
- Line items can only be `type: 'flat'`
|
||||
- Data is stored in `orders` and `order_items` tables
|
||||
|
||||
[Full one-off payments guide →](/docs/next-supabase-turbo/billing/one-off-payments)
|
||||
|
||||
## Combining Line Items (Stripe Only)
|
||||
|
||||
With Stripe, you can combine multiple line items in a single plan. This is useful for hybrid pricing models:
|
||||
|
||||
```tsx
|
||||
{
|
||||
id: 'growth-monthly',
|
||||
name: 'Growth Monthly',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
lineItems: [
|
||||
// Base platform fee
|
||||
{
|
||||
id: 'price_base_fee',
|
||||
name: 'Platform Fee',
|
||||
cost: 49,
|
||||
type: 'flat',
|
||||
},
|
||||
// Per-seat charges
|
||||
{
|
||||
id: 'price_seats',
|
||||
name: 'Team Seats',
|
||||
cost: 0,
|
||||
type: 'per_seat',
|
||||
tiers: [
|
||||
{ upTo: 5, cost: 0 },
|
||||
{ upTo: 'unlimited', cost: 10 },
|
||||
],
|
||||
},
|
||||
// Usage-based charges
|
||||
{
|
||||
id: 'price_api',
|
||||
name: 'API Calls',
|
||||
cost: 0,
|
||||
type: 'metered',
|
||||
unit: 'calls',
|
||||
tiers: [
|
||||
{ upTo: 10000, cost: 0 },
|
||||
{ upTo: 'unlimited', cost: 0.001 },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
{% alert type="warning" title="Lemon Squeezy and Paddle limitations" %}
|
||||
Lemon Squeezy and Paddle only support one line item per plan. The schema validation will fail if you add multiple line items with these providers.
|
||||
{% /alert %}
|
||||
|
||||
## Custom Plans (Enterprise/Contact Us)
|
||||
|
||||
Display a plan in the pricing table without checkout functionality. Useful for enterprise tiers or "Contact Us" options.
|
||||
|
||||
```tsx
|
||||
{
|
||||
id: 'enterprise',
|
||||
name: 'Enterprise',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
custom: true,
|
||||
label: '$5,000+', // or 'common.contactUs' for i18n
|
||||
href: '/contact',
|
||||
buttonLabel: 'Contact Sales',
|
||||
lineItems: [], // Must be empty array
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `custom` | Yes | Set to `true` |
|
||||
| `label` | Yes | Price label (e.g., "Custom pricing", "$5,000+") |
|
||||
| `href` | Yes | Link destination (e.g., "/contact", "mailto:sales@...") |
|
||||
| `buttonLabel` | No | Custom CTA text |
|
||||
| `lineItems` | Yes | Must be empty array `[]` |
|
||||
|
||||
Custom plans appear in the pricing table but clicking them navigates to `href` instead of opening checkout.
|
||||
|
||||
## Legacy Plans
|
||||
|
||||
When you discontinue a plan but have existing subscribers, use the `hidden` flag to keep the plan in your schema without showing it in the pricing table:
|
||||
|
||||
```tsx
|
||||
{
|
||||
id: 'old-pro',
|
||||
name: 'Pro (Legacy)',
|
||||
description: 'This plan is no longer available',
|
||||
currency: 'USD',
|
||||
hidden: true, // Won't appear in pricing table
|
||||
plans: [
|
||||
{
|
||||
id: 'old-pro-monthly',
|
||||
name: 'Pro Monthly (Legacy)',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_legacy_xxx',
|
||||
name: 'Pro Plan',
|
||||
cost: 19,
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Hidden plans:
|
||||
- Don't appear in the pricing table
|
||||
- Still display correctly in the user's billing section
|
||||
- Allow existing subscribers to continue without issues
|
||||
|
||||
**If you remove a plan entirely:** Makerkit will attempt to fetch plan details from the billing provider. This works for `flat` line items only. For complex plans, keep them in your schema with `hidden: true`.
|
||||
|
||||
## Schema Validation
|
||||
|
||||
The `createBillingSchema` function validates your configuration and throws errors for common mistakes:
|
||||
|
||||
| Validation | Rule |
|
||||
|------------|------|
|
||||
| Unique Plan IDs | Plan IDs must be unique across all products |
|
||||
| Unique Line Item IDs | Line item IDs must be unique across all plans |
|
||||
| Provider constraints | Lemon Squeezy: max 1 line item per plan |
|
||||
| Required tiers | Metered and per-seat items require `tiers` array |
|
||||
| One-time payments | Must have `type: 'flat'` line items only |
|
||||
| Recurring payments | Must specify `interval: 'month'` or `'year'` |
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a full billing schema with multiple products and pricing models:
|
||||
|
||||
```tsx
|
||||
import { createBillingSchema } from '@kit/billing';
|
||||
|
||||
const provider = process.env.NEXT_PUBLIC_BILLING_PROVIDER ?? 'stripe';
|
||||
|
||||
export default createBillingSchema({
|
||||
provider,
|
||||
products: [
|
||||
// Free tier (custom plan, no billing)
|
||||
{
|
||||
id: 'free',
|
||||
name: 'Free',
|
||||
description: 'Get started for free',
|
||||
currency: 'USD',
|
||||
plans: [
|
||||
{
|
||||
id: 'free',
|
||||
name: 'Free',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
custom: true,
|
||||
label: '$0',
|
||||
href: '/auth/sign-up',
|
||||
buttonLabel: 'Get Started',
|
||||
lineItems: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Pro tier with monthly/yearly
|
||||
{
|
||||
id: 'pro',
|
||||
name: 'Pro',
|
||||
description: 'For professionals and small teams',
|
||||
currency: 'USD',
|
||||
badge: 'Popular',
|
||||
highlighted: true,
|
||||
features: [
|
||||
'Unlimited projects',
|
||||
'Priority support',
|
||||
'Advanced analytics',
|
||||
'Custom integrations',
|
||||
],
|
||||
plans: [
|
||||
{
|
||||
id: 'pro-monthly',
|
||||
name: 'Pro Monthly',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
trialDays: 14,
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_pro_monthly',
|
||||
name: 'Pro Plan',
|
||||
cost: 29,
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pro-yearly',
|
||||
name: 'Pro Yearly',
|
||||
paymentType: 'recurring',
|
||||
interval: 'year',
|
||||
trialDays: 14,
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_pro_yearly',
|
||||
name: 'Pro Plan',
|
||||
cost: 290,
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Team tier with per-seat pricing
|
||||
{
|
||||
id: 'team',
|
||||
name: 'Team',
|
||||
description: 'For growing teams',
|
||||
currency: 'USD',
|
||||
features: [
|
||||
'Everything in Pro',
|
||||
'Team management',
|
||||
'SSO authentication',
|
||||
'Audit logs',
|
||||
],
|
||||
plans: [
|
||||
{
|
||||
id: 'team-monthly',
|
||||
name: 'Team Monthly',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_team_monthly',
|
||||
name: 'Team Seats',
|
||||
cost: 0,
|
||||
type: 'per_seat',
|
||||
tiers: [
|
||||
{ upTo: 5, cost: 0 },
|
||||
{ upTo: 'unlimited', cost: 15 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Enterprise tier
|
||||
{
|
||||
id: 'enterprise',
|
||||
name: 'Enterprise',
|
||||
description: 'For large organizations',
|
||||
currency: 'USD',
|
||||
features: [
|
||||
'Everything in Team',
|
||||
'Dedicated support',
|
||||
'Custom contracts',
|
||||
'SLA guarantees',
|
||||
],
|
||||
plans: [
|
||||
{
|
||||
id: 'enterprise',
|
||||
name: 'Enterprise',
|
||||
paymentType: 'recurring',
|
||||
interval: 'month',
|
||||
custom: true,
|
||||
label: 'Custom',
|
||||
href: '/contact',
|
||||
buttonLabel: 'Contact Sales',
|
||||
lineItems: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Billing Overview](/docs/next-supabase-turbo/billing/overview) - Architecture and provider comparison
|
||||
- [Stripe Setup](/docs/next-supabase-turbo/billing/stripe) - Configure Stripe billing
|
||||
- [Lemon Squeezy Setup](/docs/next-supabase-turbo/billing/lemon-squeezy) - Configure Lemon Squeezy
|
||||
- [Paddle Setup](/docs/next-supabase-turbo/billing/paddle) - Configure Paddle
|
||||
- [Per-Seat Billing](/docs/next-supabase-turbo/billing/per-seat-billing) - Team-based pricing
|
||||
- [Metered Usage](/docs/next-supabase-turbo/billing/metered-usage) - Usage-based pricing
|
||||
- [One-Off Payments](/docs/next-supabase-turbo/billing/one-off-payments) - Lifetime deals and add-ons
|
||||
Reference in New Issue
Block a user