Overview
Codapult uses the adapter pattern for payments — switch between Stripe and LemonSqueezy by changing a single environment variable. No code changes required.
# .env.local
PAYMENT_PROVIDER="stripe" # default
# PAYMENT_PROVIDER="lemonsqueezy" # alternative
| Component | Location |
| ---------------- | --------------------------------- |
| Payment adapter | src/lib/payments/ |
| Plan definitions | src/lib/payments/plans.ts |
| Pricing display | src/config/marketing.ts |
| Server actions | src/lib/actions/billing.ts |
| Stripe webhook | POST /api/webhooks/stripe |
| LS webhook | POST /api/webhooks/lemonsqueezy |
Subscription Management
Your end users manage subscriptions from Dashboard → Billing:
- View current plan and usage
- Upgrade or downgrade plans
- Switch between monthly and yearly billing
- Add or remove add-ons
- Cancel subscription
- Access the billing portal (Stripe Customer Portal / LemonSqueezy)
- Download invoices
Plan Configuration
Plans are defined in src/lib/payments/plans.ts. Each plan specifies a name, price, interval, features, and limits:
export const plans = [
{
id: 'free',
name: 'Free',
price: { monthly: 0, yearly: 0 },
features: ['1 project', '100 AI messages/month', 'Community support'],
},
{
id: 'pro',
name: 'Pro',
price: { monthly: 19, yearly: 190 },
features: ['Unlimited projects', '5k AI messages', 'Priority support'],
},
{
id: 'enterprise',
name: 'Enterprise',
price: { monthly: 49, yearly: 490 },
features: ['Everything in Pro', 'Unlimited AI', 'SSO', 'SLA', 'Custom domain'],
},
] as const;
Update the pricing display for your marketing pages in src/config/marketing.ts.
Add-Ons
Add-ons are optional extras that extend a subscription. They are combined with the base plan in a single multi-line checkout session:
| Add-On | Description | | ---------------- | ---------------------------- | | Extra Storage | Additional storage per block | | Priority Support | Dedicated support channel | | Custom Domain | Use your own domain | | White Label | Remove platform branding |
Users select add-ons during checkout or add them later from the billing dashboard.
Seat-Based Pricing
Plans support per-seat pricing. Seats are automatically synced with organization members — when a new member joins, the seat count updates. Admins can set a max seat cap to control costs.
Usage-Based Billing
Plans can include tiered usage pricing for metered features like AI messages or API calls. When users exceed their plan's included quota, overage is billed at the configured rate.
The usage estimator on the pricing page helps your end users predict monthly costs based on expected usage.
Usage Credits
Set a default monthly credit allowance with the DEFAULT_MONTHLY_CREDITS environment variable. Credits reset automatically via a built-in cron job at the start of each billing cycle.
Stripe Connect
Codapult includes Stripe Connect support for marketplace features. Your platform can collect an application fee on transactions processed through connected accounts.
STRIPE_CONNECT_APPLICATION_FEE_PERCENT=10 # 10% platform fee
Webhooks
Payment providers notify your app of subscription events via webhooks:
| Provider | Endpoint |
| ------------ | --------------------------------- |
| Stripe | POST /api/webhooks/stripe |
| LemonSqueezy | POST /api/webhooks/lemonsqueezy |
Handled Events
| Event | Action |
| ------------------------------- | ----------------------------------- |
| checkout.session.completed | Activate subscription, link to user |
| customer.subscription.created | Record new subscription |
| customer.subscription.updated | Update plan, seats, or status |
| customer.subscription.deleted | Cancel and deactivate subscription |
Webhook payloads include metadata (userId, planId) set during checkout creation. All webhook deliveries are logged in the webhook_delivery table for debugging.
Server Actions
Billing mutations are handled by server actions in src/lib/actions/billing.ts:
createCheckout
Creates a payment checkout session and redirects the user to the provider's payment page.
// Used as a form action
<form action={createCheckout}>
<input type="hidden" name="planId" value="pro" />
<input type="hidden" name="interval" value="monthly" />
<input type="hidden" name="seats" value="5" />
<button type="submit">Subscribe</button>
</form>
openCustomerPortal
Redirects the user to the provider's billing portal where they can update payment methods, view invoices, and manage their subscription.
<form action={openCustomerPortal}>
<button type="submit">Manage Billing</button>
</form>
Environment Variables
| Variable | Description | Default |
| ---------------------------------------- | -------------------------------------------- | -------- |
| PAYMENT_PROVIDER | Payment adapter (stripe or lemonsqueezy) | stripe |
| STRIPE_SECRET_KEY | Stripe API secret key | — |
| STRIPE_WEBHOOK_SECRET | Stripe webhook signing secret | — |
| STRIPE_CONNECT_APPLICATION_FEE_PERCENT | Platform fee percentage for Connect | — |
| LEMONSQUEEZY_API_KEY | LemonSqueezy API key | — |
| LEMONSQUEEZY_WEBHOOK_SECRET | LemonSqueezy webhook signing secret | — |
| DEFAULT_MONTHLY_CREDITS | Monthly usage credit allowance | — |
Module Removal
The payments module is independently removable. See the Modules documentation for step-by-step removal instructions.
Next Steps
- Plans & Pricing — detailed plan configuration examples
- Teams & Organizations — seat-based billing tied to team members
- Database — schema conventions for billing tables