Payments Overview

Dual pricing model with Lemon Squeezy — credit-based and classic SaaS subscription architecture

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.
 */

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.

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

VariableRequiredModelPurpose
LEMONSQUEEZY_API_KEYYesBothServer-side API key for Lemon Squeezy SDK
LEMONSQUEEZY_STORE_IDYesBothYour Lemon Squeezy store identifier
LEMONSQUEEZY_WEBHOOK_SECRETYesBothHMAC secret for webhook signature verification
NEXT_PUBLIC_PRICING_MODELYesBothcredit_based or classic_saas
NEXT_PUBLIC_LEMONSQUEEZY_BASIC_MONTHLY_VARIANT_IDYesClassicBasic tier monthly variant ID
NEXT_PUBLIC_LEMONSQUEEZY_BASIC_YEARLY_VARIANT_IDYesClassicBasic tier yearly variant ID
NEXT_PUBLIC_LEMONSQUEEZY_PRO_MONTHLY_VARIANT_IDYesClassicPro tier monthly variant ID
NEXT_PUBLIC_LEMONSQUEEZY_PRO_YEARLY_VARIANT_IDYesClassicPro tier yearly variant ID
NEXT_PUBLIC_LEMONSQUEEZY_ENTERPRISE_MONTHLY_VARIANT_IDYesClassicEnterprise tier monthly variant ID
NEXT_PUBLIC_CREDIT_BASIC_MONTHLY_VARIANT_IDYesCreditBasic tier credit variant ID
NEXT_PUBLIC_CREDIT_PRO_MONTHLY_VARIANT_IDYesCreditPro tier credit variant ID
NEXT_PUBLIC_CREDIT_ENTERPRISE_MONTHLY_VARIANT_IDYesCreditEnterprise tier credit variant ID
CURRENCYNoBothCurrency code (default: EUR)
For the full environment variable reference including credit-specific variables, see Environment Variables.

Key Files

FilePurpose
apps/boilerplate/src/lib/payments/config.tsVariant ID mapping, getTierLevel(), getTierName(), getBillingPeriod()
apps/boilerplate/src/lib/payments/lemonsqueezy-client.tsAPI client, signature verification helper, test mode detection
apps/boilerplate/src/lib/payments/subscription.tsSubscription queries, getSubscriptionTier(), customer portal URL
apps/boilerplate/src/lib/pricing/config.tsDual pricing config loader with validation and caching
apps/boilerplate/src/lib/credits/config.tsCredit system feature flag (isCreditSystemEnabled())
apps/boilerplate/src/lib/credits/credit-manager.tsAtomic credit deductions with SELECT FOR UPDATE
apps/boilerplate/src/lib/credits/credit-costs.tsPer-operation cost table (17 operation types)
apps/boilerplate/src/app/api/webhooks/lemonsqueezy/route.tsWebhook endpoint — signature verification and event routing
apps/boilerplate/src/app/api/webhooks/lemonsqueezy/handlers/11 modular event handlers (one file per event)