Kit ships with a complete payment system built on Lemon Squeezy as the Merchant of Record. The system supports two pricing models — credit-based for AI-intensive applications and classic SaaS for traditional subscription tiers — switchable with a single environment variable.
This page covers the architecture and core concepts. For setup instructions, see Lemon Squeezy Setup. For plan configuration, see Subscription Plans.
Dual Pricing Architecture
Kit supports two distinct pricing models, selectable at deployment time via the
NEXT_PUBLIC_PRICING_MODEL environment variable:NEXT_PUBLIC_PRICING_MODEL
|
|--- "credit_based" ──> Users purchase credits, consumed per AI operation
| Monthly credit allocation per tier
| Bonus credit top-up purchases
| Per-operation cost tracking
|
|--- "classic_saas" ──> Traditional subscription tiers
Monthly + yearly billing periods
Feature gates per tier
Trial period support
Only one model is active at a time. The pricing configuration system validates at startup that all required environment variables for the active model are present:
src/lib/pricing/config.ts — Dual Model Header
/**
* Pricing Configuration - DUAL Model
*
* Central configuration system for the dual pricing model.
* Supports both credit-based and classic SaaS pricing.
*
* IMPORTANT: Only ONE model can be active at a time based on NEXT_PUBLIC_PRICING_MODEL env var.
*/
The pricing model is set once when you deploy your application. Switching between models requires reconfiguring your Lemon Squeezy products and variant IDs. Both models share the same subscription infrastructure (tiers, webhooks, database) — only the billing logic and UI differ.
Credit-Based Model
Designed for AI-heavy applications where usage varies per user. Each tier includes a monthly credit allocation (Free: 500, Basic: 1,500, Pro: 5,000, Enterprise: 15,000). Credits are deducted per AI operation with atomic database transactions. Users can purchase bonus credit top-ups. Monthly billing only — no yearly option.
Classic SaaS Model
Traditional subscription pricing with monthly and yearly billing periods. Each tier unlocks a set of features. Includes trial period support with a
hasUsedTrial flag to prevent abuse. Yearly billing offers a discount. Enterprise tier is contact-only (no self-service checkout).Payment Flow
Every payment in Kit follows this flow — from the checkout button to the database update:
User clicks "Subscribe"
|
v
Checkout URL (Lemon Squeezy hosted)
|--- custom_data: { userId: "db_user_id" }
|--- variant_id: selected plan variant
|
v
Lemon Squeezy processes payment
|--- Handles tax calculation (Merchant of Record)
|--- Manages payment method
|--- Creates subscription
|
v
Webhook POST to /api/webhooks/lemonsqueezy
|
|--- 1. Verify HMAC-SHA256 signature
|--- 2. Parse event type (subscription_created, etc.)
|--- 3. Extract userId from custom_data
|--- 4. Route to event handler
|
v
Database updated
|--- Subscription record created/updated
|--- User tier synchronized
|--- Credits initialized (credit-based model)
|--- Welcome email sent (non-blocking)
The key detail is
custom_data — when generating the checkout URL, Kit passes the database user ID. This is how the webhook handler knows which user the subscription belongs to.Lemon Squeezy Integration
Kit uses Lemon Squeezy as the payment provider for a specific reason: it operates as a Merchant of Record. This means Lemon Squeezy handles all tax calculation, collection, and remittance — you never need to worry about VAT, sales tax, or tax compliance across jurisdictions.
The integration uses the official
@lemonsqueezy/lemonsqueezy.js SDK with lazy initialization:src/lib/payments/lemonsqueezy-client.ts — API Client
import {
lemonSqueezySetup,
listProducts,
listVariants,
getProduct,
getVariant,
getSubscription,
updateSubscription,
cancelSubscription,
createCheckout,
type Variant,
type Product,
type Subscription,
type Checkout,
type NewCheckout,
} from '@lemonsqueezy/lemonsqueezy.js'
import crypto from 'crypto'
// Initialize Lemon Squeezy client lazily
let isInitialized = false
const initializeLemonSqueezy = () => {
if (isInitialized) return
const apiKey = process.env.LEMONSQUEEZY_API_KEY
if (!apiKey) {
throw new Error('LEMONSQUEEZY_API_KEY is not set in environment variables')
}
lemonSqueezySetup({
apiKey,
onError: (error) => {
console.error('Lemon Squeezy API error:', error)
},
})
isInitialized = true
}
The client wraps all SDK functions with an
ensureInitialized guard, so the API key is only read from the environment when the first payment operation occurs — not at import time. This prevents build-time errors when environment variables are not yet set.During development, enable test mode in the Lemon Squeezy dashboard. Test mode uses separate products, variants, and webhook secrets. Kit auto-detects test mode via
process.env.NODE_ENV === 'development' or LEMONSQUEEZY_TEST_MODE=true.Key Concepts
Variants
Lemon Squeezy uses variants to represent different pricing options within a product. In Kit, each combination of tier + billing period is a separate variant:
- Classic SaaS: 6 variants (Basic/Pro/Enterprise x Monthly/Yearly)
- Credit-Based: 3 variants (Basic/Pro/Enterprise x Monthly only)
Each variant has a unique ID from the Lemon Squeezy dashboard, stored as environment variables.
Subscriptions
A subscription links a user to a variant. Kit stores subscriptions in the database with the Lemon Squeezy subscription ID, variant ID, status, and billing period dates. The subscription status drives feature access throughout the application.
Tiers
Kit defines four tiers — Free, Basic, Pro, and Enterprise. Every user starts on the Free tier. The tier is determined from the subscription's variant ID using the centralized
getTierLevel() and getTierName() functions. Tier changes trigger credit adjustments in the credit-based model.Checkout URLs
Checkout is handled entirely by Lemon Squeezy's hosted checkout page. Kit generates checkout URLs with the correct variant ID and passes
custom_data containing the user's database ID. After payment, Lemon Squeezy redirects back to your application and sends webhook events.Directory Structure
The payment system spans several directories:
apps/boilerplate/src/
├── lib/
│ ├── payments/
│ │ ├── config.ts # Variant ID mapping, tier detection
│ │ ├── lemonsqueezy-client.ts # API client with lazy init
│ │ ├── subscription.ts # Subscription management, customer portal
│ │ └── types.ts # Shared payment types
│ ├── pricing/
│ │ ├── config.ts # Dual pricing model configuration
│ │ └── types.ts # Pricing types (PricingModel, PricingConfig)
│ ├── credits/
│ │ ├── config.ts # Credit system feature flag, tier defaults
│ │ ├── credit-manager.ts # Atomic deductions, refunds, resets
│ │ ├── credit-costs.ts # Per-operation cost definitions
│ │ └── initialization.ts # Lazy credit initialization
│ └── webhooks/
│ └── credit-helpers.ts # Credit update logic for webhooks
├── app/
│ ├── api/
│ │ └── webhooks/
│ │ └── lemonsqueezy/
│ │ ├── route.ts # Webhook endpoint with signature verification
│ │ ├── handlers/ # 11 modular event handlers
│ │ ├── lib/ # Signature verification, helpers
│ │ └── types.ts # Webhook payload types
│ └── (dashboard)/
│ └── dashboard/
│ └── billing/ # Billing UI (plan selection, portal)
Environment Variables
| Variable | Required | Model | Purpose |
|---|---|---|---|
LEMONSQUEEZY_API_KEY | Yes | Both | Server-side API key for Lemon Squeezy SDK |
LEMONSQUEEZY_STORE_ID | Yes | Both | Your Lemon Squeezy store identifier |
LEMONSQUEEZY_WEBHOOK_SECRET | Yes | Both | HMAC secret for webhook signature verification |
NEXT_PUBLIC_PRICING_MODEL | Yes | Both | credit_based or classic_saas |
NEXT_PUBLIC_LEMONSQUEEZY_BASIC_MONTHLY_VARIANT_ID | Yes | Classic | Basic tier monthly variant ID |
NEXT_PUBLIC_LEMONSQUEEZY_BASIC_YEARLY_VARIANT_ID | Yes | Classic | Basic tier yearly variant ID |
NEXT_PUBLIC_LEMONSQUEEZY_PRO_MONTHLY_VARIANT_ID | Yes | Classic | Pro tier monthly variant ID |
NEXT_PUBLIC_LEMONSQUEEZY_PRO_YEARLY_VARIANT_ID | Yes | Classic | Pro tier yearly variant ID |
NEXT_PUBLIC_LEMONSQUEEZY_ENTERPRISE_MONTHLY_VARIANT_ID | Yes | Classic | Enterprise tier monthly variant ID |
NEXT_PUBLIC_CREDIT_BASIC_MONTHLY_VARIANT_ID | Yes | Credit | Basic tier credit variant ID |
NEXT_PUBLIC_CREDIT_PRO_MONTHLY_VARIANT_ID | Yes | Credit | Pro tier credit variant ID |
NEXT_PUBLIC_CREDIT_ENTERPRISE_MONTHLY_VARIANT_ID | Yes | Credit | Enterprise tier credit variant ID |
CURRENCY | No | Both | Currency code (default: EUR) |
For the full environment variable reference including credit-specific variables, see Environment Variables.
Key Files
| File | Purpose |
|---|---|
apps/boilerplate/src/lib/payments/config.ts | Variant ID mapping, getTierLevel(), getTierName(), getBillingPeriod() |
apps/boilerplate/src/lib/payments/lemonsqueezy-client.ts | API client, signature verification helper, test mode detection |
apps/boilerplate/src/lib/payments/subscription.ts | Subscription queries, getSubscriptionTier(), customer portal URL |
apps/boilerplate/src/lib/pricing/config.ts | Dual pricing config loader with validation and caching |
apps/boilerplate/src/lib/credits/config.ts | Credit system feature flag (isCreditSystemEnabled()) |
apps/boilerplate/src/lib/credits/credit-manager.ts | Atomic credit deductions with SELECT FOR UPDATE |
apps/boilerplate/src/lib/credits/credit-costs.ts | Per-operation cost table (17 operation types) |
apps/boilerplate/src/app/api/webhooks/lemonsqueezy/route.ts | Webhook endpoint — signature verification and event routing |
apps/boilerplate/src/app/api/webhooks/lemonsqueezy/handlers/ | 11 modular event handlers (one file per event) |