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.
| Tier | Monthly Price | Yearly Price | Credits/Month | Feature Level |
|---|---|---|---|---|
| Free | €0 | — | 500 | Basic access, community support |
| Basic | €9.90 | €99.00 | 1,500 | Core features, email support |
| Pro | €19.90 | €199.00 | 5,000 | Advanced features, priority support |
| Enterprise | €39.90 | €399.00 | 15,000 | All features, dedicated support |
Prices and credit allocations are configurable via environment variables. The table above shows the defaults.
Enterprise is a contact-only tier — there is no self-service yearly checkout variant. Enterprise customers are provisioned manually or through a custom sales flow. The monthly variant exists for API-based provisioning when needed.
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 Variable | Tier | Billing |
|---|---|---|
NEXT_PUBLIC_LEMONSQUEEZY_BASIC_MONTHLY_VARIANT_ID | Basic | Monthly |
NEXT_PUBLIC_LEMONSQUEEZY_BASIC_YEARLY_VARIANT_ID | Basic | Yearly |
NEXT_PUBLIC_LEMONSQUEEZY_PRO_MONTHLY_VARIANT_ID | Pro | Monthly |
NEXT_PUBLIC_LEMONSQUEEZY_PRO_YEARLY_VARIANT_ID | Pro | Yearly |
NEXT_PUBLIC_LEMONSQUEEZY_ENTERPRISE_MONTHLY_VARIANT_ID | Enterprise | Monthly |
Credit-Based Variants
Credit-based pricing uses 3 variant IDs — monthly only:
| Environment Variable | Tier | Billing |
|---|---|---|
NEXT_PUBLIC_CREDIT_BASIC_MONTHLY_VARIANT_ID | Basic | Monthly |
NEXT_PUBLIC_CREDIT_PRO_MONTHLY_VARIANT_ID | Pro | Monthly |
NEXT_PUBLIC_CREDIT_ENTERPRISE_MONTHLY_VARIANT_ID | Enterprise | Monthly |
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
}
| Function | Returns | Usage |
|---|---|---|
getTierLevel(variantId) | 0-3 (0 = free, 1 = basic, 2 = pro, 3 = enterprise) | Upgrade/downgrade comparison |
getTierName(variantId) | 'basic', 'pro', 'enterprise', or null | UI display, feature gates |
getBillingPeriod(variantId) | 'monthly', 'yearly', or null | Billing 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:
- Status is
activeoron_trial— full access regardless of expiration - Status is
cancelledbutcurrentPeriodEndis 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():| Resource | Free | Basic | Pro | Enterprise |
|---|---|---|---|---|
| Projects | 1 | 5 | 20 | Unlimited |
| Storage (MB) | 100 | 1,000 | 10,000 | Unlimited |
| API Calls | 1,000 | 10,000 | 100,000 | Unlimited |
| Team Members | 1 | 3 | 10 | Unlimited |
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:- Immediate access — The user gets the new tier immediately
- Prorated billing — Lemon Squeezy charges the difference for the remaining period
- Credit adjustment — In credit-based mode, the user receives the new tier's credit allocation
- Tier sync — The
User.tierfield is updated to reflect the new tier - Preference preserved — The user's
bonusCreditsAutoUsepreference (per-user bonus credit toggle) is unaffected by tier changes
Upgrades take effect immediately. The user does not need to wait for the next billing cycle. Lemon Squeezy calculates the prorated amount automatically based on the remaining days in the current billing period.
Downgrades
Downgrades are deferred to the end of the current billing period to ensure users get what they paid for:
- Deferred access — The user keeps their current tier until
currentPeriodEnd - Next period — At renewal, Lemon Squeezy charges the lower tier price
- Credit cap — In credit-based mode, credits are capped to the new tier's allocation at the next reset
- Tier sync — The
User.tieris updated when thesubscription_updatedwebhook fires at period end
During a trial period, users are limited to 2 downgrades. This prevents abuse where users repeatedly switch between free trials of different tiers. The
hasUsedTrial flag on the User model tracks whether a trial has been consumed.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:- Checks if the user already has
hasUsedTrial = true - If not, sets
hasUsedTrial = trueon the User record - Creates the subscription with status
on_trial - 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
activeviasubscription_updatedwebhook - Failed payment — Status changes to
expiredviasubscription_expiredwebhook - 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
The customer portal is fully hosted by Lemon Squeezy. No additional UI implementation is needed in your application — just provide a link or redirect to the portal URL.