Launchpad supports two billing providers for subscriptions: Stripe and LemonSqueezy. The e-commerce shop module uses Stripe only (one-time purchases, not subscriptions).
Set PAYMENT_PROVIDER=stripe or PAYMENT_PROVIDER=lemonsqueezy in your .env.local. If you don't set it, it defaults to Stripe.
In the Stripe Dashboard, create two products: Pro and Team. For each, create two prices — one monthly, one yearly — that match the amounts defined in src/user-control/plans-config.ts:
- Pro: $29/month, $279/year
- Team: $79/month, $759/year
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_PRO_MONTHLY_PRICE_ID=price_...
STRIPE_PRO_YEARLY_PRICE_ID=price_...
STRIPE_PRO_PRODUCT_ID=prod_...
STRIPE_TEAM_MONTHLY_PRICE_ID=price_...
STRIPE_TEAM_YEARLY_PRICE_ID=price_...
STRIPE_TEAM_PRODUCT_ID=prod_...In the Stripe Dashboard, go to Developers → Webhooks → Add endpoint.
- Production endpoint URL:
https://yourdomain.com/api/webhooks/stripe - For local testing, use the Stripe CLI:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Select these events:
checkout.session.completedcustomer.subscription.updatedcustomer.subscription.deleted
Copy the Signing secret (starts with whsec_) into STRIPE_WEBHOOK_SECRET.
Use Stripe's test card: 4242 4242 4242 4242, any future expiry, any CVC. After a successful test checkout, the subscription status should update in your database and appear on the billing page.
To test cancellation: go through a test checkout, then use the customer portal to cancel. The customer.subscription.updated webhook will fire with cancel_at_period_end: true.
PAYMENT_PROVIDER=lemonsqueezyIn your LemonSqueezy dashboard, create a Pro product and a Team product. For each, create monthly and yearly variants. Note the variant IDs (not product IDs) for each.
LS_API_KEY=...
LS_STORE_ID=...
LS_PRO_MONTHLY_VARIANT_ID=...
LS_PRO_YEARLY_VARIANT_ID=...
LS_PRO_PRODUCT_ID=...
LS_TEAM_MONTHLY_VARIANT_ID=...
LS_TEAM_YEARLY_VARIANT_ID=...
LS_TEAM_PRODUCT_ID=...In LemonSqueezy → Settings → Webhooks → Add webhook:
- Endpoint URL:
https://yourdomain.com/api/webhooks/lemonsqueezy - Select events:
subscription_created,subscription_updated,subscription_cancelled,subscription_expired,subscription_payment_success,subscription_payment_failed
Copy the signing secret into LS_WEBHOOK_SECRET.
LemonSqueezy billing portal note: Unlike Stripe, LemonSqueezy doesn't have a "create portal session" API call. Instead, every subscription webhook includes a signed customer portal URL. Launchpad stores this URL when the subscription is created and uses it on the billing page. This means the billing portal link is only available after at least one subscription webhook has been received.
The shop module uses a completely separate checkout flow from subscription billing. Products are created through your dashboard (/dashboard/products), not pre-configured in the codebase.
You only need:
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...These can be the same values as your subscription Stripe keys — both flows share the same webhook endpoint. The webhook handler dispatches internally based on whether metadata.orderId (shop checkout) or metadata.userId (subscription checkout) is present.
Change PAYMENT_PROVIDER and restart. No code changes needed. You can keep both sets of environment variables populated and switch freely between them.
When you're ready to move from test mode to live:
- Replace
sk_test_...keys withsk_live_...keys in your production environment - Update your webhook endpoints in the Stripe/LemonSqueezy dashboard to point at your production URL
- Create new webhook entries (or update existing ones) with the production URLs — the signing secrets will change
- Run a real end-to-end transaction with a real card to verify everything is working
Keep test mode active in your local .env.local — only the production environment should ever use live keys.