Subscription Plans

Configure pricing tiers, feature gates, plan upgrades and downgrades, and billing periods

Kit ships with four subscription tiers — Free, Basic, Pro, and Enterprise. Each tier maps to Lemon Squeezy variant IDs, and the application uses these IDs to determine feature access, credit allocation, and billing behavior.
This page covers tier configuration, plan changes, and the trial system. For credit allocation per tier, see Credit System. For the initial Lemon Squeezy setup, see Setup.

Tier Configuration

Every user starts on the Free tier. Paid tiers are activated when a subscription is created via Lemon Squeezy webhooks.
TierMonthly PriceYearly PriceCredits/MonthFeature Level
Free€0500Basic access, community support
Basic€9.90€99.001,500Core features, email support
Pro€19.90€199.005,000Advanced features, priority support
Enterprise€39.90€399.0015,000All features, dedicated support
Prices and credit allocations are configurable via environment variables. The table above shows the defaults.

Variant ID Mapping

The config.ts file maps environment variable variant IDs to a structured configuration. This is the central source of truth for all tier detection in the application:
src/lib/payments/config.ts — Variant ID Mapping
export const variantIds: PricingTierConfig = {
  basic: {
    monthly: process.env.NEXT_PUBLIC_LEMONSQUEEZY_BASIC_MONTHLY_VARIANT_ID,
    yearly: process.env.NEXT_PUBLIC_LEMONSQUEEZY_BASIC_YEARLY_VARIANT_ID,
  },
  pro: {
    monthly: process.env.NEXT_PUBLIC_LEMONSQUEEZY_PRO_MONTHLY_VARIANT_ID,
    yearly: process.env.NEXT_PUBLIC_LEMONSQUEEZY_PRO_YEARLY_VARIANT_ID,
  },
  enterprise: {
    // Enterprise is contact-only tier (no direct checkout, no yearly variant)
    monthly: process.env.NEXT_PUBLIC_LEMONSQUEEZY_ENTERPRISE_MONTHLY_VARIANT_ID,
  },
}

/**
 * Credit-Based Model Variant IDs
 * Credit-based pricing only supports monthly billing (no yearly option)
 */
export const creditBasedVariantIds: PricingTierConfig = {
  basic: {
    monthly: process.env.NEXT_PUBLIC_CREDIT_BASIC_MONTHLY_VARIANT_ID,
    yearly: undefined, // Credit-based only supports monthly
  },
  pro: {
    monthly: process.env.NEXT_PUBLIC_CREDIT_PRO_MONTHLY_VARIANT_ID,
    yearly: undefined,
  },
  enterprise: {
    monthly: process.env.NEXT_PUBLIC_CREDIT_ENTERPRISE_MONTHLY_VARIANT_ID,
    yearly: undefined,
  },
}

Classic SaaS Variants

Classic SaaS uses 6 variant IDs — monthly and yearly for each paid tier:
Environment VariableTierBilling
NEXT_PUBLIC_LEMONSQUEEZY_BASIC_MONTHLY_VARIANT_IDBasicMonthly
NEXT_PUBLIC_LEMONSQUEEZY_BASIC_YEARLY_VARIANT_IDBasicYearly
NEXT_PUBLIC_LEMONSQUEEZY_PRO_MONTHLY_VARIANT_IDProMonthly
NEXT_PUBLIC_LEMONSQUEEZY_PRO_YEARLY_VARIANT_IDProYearly
NEXT_PUBLIC_LEMONSQUEEZY_ENTERPRISE_MONTHLY_VARIANT_IDEnterpriseMonthly

Credit-Based Variants

Credit-based pricing uses 3 variant IDs — monthly only:
Environment VariableTierBilling
NEXT_PUBLIC_CREDIT_BASIC_MONTHLY_VARIANT_IDBasicMonthly
NEXT_PUBLIC_CREDIT_PRO_MONTHLY_VARIANT_IDProMonthly
NEXT_PUBLIC_CREDIT_ENTERPRISE_MONTHLY_VARIANT_IDEnterpriseMonthly

Tier Detection

Three functions in config.ts handle all tier detection throughout the application. They check both pricing models automatically:
src/lib/payments/config.ts — getTierLevel()
export function getTierLevel(variantId: string): number {
  // Check basic tier (Credit-Based first, then Classic SaaS)
  if (
    variantId === creditBasedVariantIds.basic.monthly ||
    variantId === variantIds.basic.monthly ||
    variantId === variantIds.basic.yearly
  ) {
    return 1
  }

  // Check pro tier (Credit-Based first, then Classic SaaS)
  if (
    variantId === creditBasedVariantIds.pro.monthly ||
    variantId === variantIds.pro.monthly ||
    variantId === variantIds.pro.yearly
  ) {
    return 2
  }

  // Check enterprise tier (Credit-Based first, then Classic SaaS)
  if (
    variantId === creditBasedVariantIds.enterprise.monthly ||
    variantId === variantIds.enterprise.monthly ||
    variantId === variantIds.enterprise.yearly
  ) {
    return 3
  }

  // Unknown variant ID - return 0 (free tier)
  return 0
}
FunctionReturnsUsage
getTierLevel(variantId)0-3 (0 = free, 1 = basic, 2 = pro, 3 = enterprise)Upgrade/downgrade comparison
getTierName(variantId)'basic', 'pro', 'enterprise', or nullUI display, feature gates
getBillingPeriod(variantId)'monthly', 'yearly', or nullBilling period display
These functions check credit-based variant IDs first, then classic SaaS variants. This means they work correctly regardless of which pricing model is active.

Subscription Tier Resolution

The getSubscriptionTier() function in subscription.ts combines variant detection with subscription status to determine a user's effective tier:
typescript
import { getSubscriptionTier } from '@/lib/payments/subscription'

// In a Server Component or API route
const subscription = await getUserSubscription(userId)
const tier = getSubscriptionTier(subscription)
// Returns: 'free' | 'basic' | 'pro' | 'enterprise'
A subscription grants access if:
  1. Status is active or on_trial — full access regardless of expiration
  2. Status is cancelled but currentPeriodEnd is in the future — grace period access
This means cancelled users keep their paid features until their billing period ends.

Feature Gates

Use the tier to conditionally enable features throughout your application:
typescript
import { getSubscriptionTier } from '@/lib/payments/subscription'
import { getUserSubscription } from '@/lib/payments/subscription'

// Server Component example
export default async function DashboardPage() {
  const subscription = await getUserSubscription(userId)
  const tier = getSubscriptionTier(subscription)

  return (
    <div>
      {/* Feature available to all paid tiers */}
      {tier !== 'free' && <AdvancedAnalytics />}

      {/* Feature only for Pro and Enterprise */}
      {['pro', 'enterprise'].includes(tier) && <PrioritySupport />}

      {/* Enterprise-only feature */}
      {tier === 'enterprise' && <CustomIntegrations />}
    </div>
  )
}

Subscription Limits

Kit defines default resource limits per tier. These are returned by getSubscriptionLimits():
ResourceFreeBasicProEnterprise
Projects1520Unlimited
Storage (MB)1001,00010,000Unlimited
API Calls1,00010,000100,000Unlimited
Team Members1310Unlimited
Customize these limits in apps/boilerplate/src/lib/payments/subscription.ts to match your product's feature set.

Plan Changes

Upgrades

When a user upgrades to a higher tier, Lemon Squeezy handles the billing proration automatically. Kit processes the upgrade through the subscription_updated webhook:
  1. Immediate access — The user gets the new tier immediately
  2. Prorated billing — Lemon Squeezy charges the difference for the remaining period
  3. Credit adjustment — In credit-based mode, the user receives the new tier's credit allocation
  4. Tier sync — The User.tier field is updated to reflect the new tier
  5. Preference preserved — The user's bonusCreditsAutoUse preference (per-user bonus credit toggle) is unaffected by tier changes

Downgrades

Downgrades are deferred to the end of the current billing period to ensure users get what they paid for:
  1. Deferred access — The user keeps their current tier until currentPeriodEnd
  2. Next period — At renewal, Lemon Squeezy charges the lower tier price
  3. Credit cap — In credit-based mode, credits are capped to the new tier's allocation at the next reset
  4. Tier sync — The User.tier is updated when the subscription_updated webhook fires at period end

Billing Period Toggle

Users on classic SaaS can switch between monthly and yearly billing. The toggleBillingPeriod() function resolves the correct variant ID for the switch:
src/lib/payments/config.ts — toggleBillingPeriod()
export function toggleBillingPeriod(
  variantId: string,
  toYearly: boolean
): string | null {
  const tierName = getTierName(variantId)

  if (!tierName) {
    return null
  }

  const tier = variantIds[tierName as keyof PricingTierConfig]

  if (toYearly) {
    return tier.yearly || null
  } else {
    return tier.monthly || null
  }
}
The function looks up the current tier from the variant ID, then returns the corresponding monthly or yearly variant. If the tier does not have a yearly variant (like Enterprise), it returns null.

Trial System

Kit supports free trials with built-in abuse prevention:

Configuration

Trials are enabled by setting the TRIAL_DAYS environment variable:
bash
# Enable 14-day trial
TRIAL_DAYS="14"

# Disable trials (set to 0 or omit)
TRIAL_DAYS="0"

Trial Tracking

When a user starts a trial subscription, the subscription_created webhook handler:
  1. Checks if the user already has hasUsedTrial = true
  2. If not, sets hasUsedTrial = true on the User record
  3. Creates the subscription with status on_trial
  4. Initializes credits at the selected tier's allocation

Trial Conversion

At the end of the trial period, Lemon Squeezy handles the conversion:
  • Successful payment — Status changes to active via subscription_updated webhook
  • Failed payment — Status changes to expired via subscription_expired webhook
  • Cancelled during trial — Status changes to cancelled, grace period applies
The hasUsedTrial flag persists permanently. A user who cancels during a trial cannot start another trial on any tier.

Customer Portal

Lemon Squeezy provides a hosted customer portal for self-service subscription management. Users can update their payment method, view invoices, and cancel their subscription.

Portal URL Generation

Kit generates a signed portal URL via the Lemon Squeezy API. The URL is valid for 24 hours:
typescript
import { getCustomerPortal } from '@/lib/payments/subscription'

// In an API route or Server Action
const { portalUrl } = await getCustomerPortal(userId)
// Redirect user to portalUrl
The portal URL is fetched from the subscription's urls.customer_portal attribute, which Lemon Squeezy pre-signs for each API request.

Portal Capabilities

Through the portal, users can:
  • View and download invoices
  • Update payment method (credit card, PayPal)
  • Cancel their subscription
  • View billing history
  • Update billing address