Configure Stripe Billing for Your Next.js SaaS
Complete guide to setting up Stripe payments in Makerkit. Configure subscriptions, one-off payments, webhooks, and the Customer Portal for your Next.js Supabase application.
Stripe is the default billing provider in Makerkit. It offers the most flexibility with support for multiple line items, metered billing, and advanced subscription management.
Prerequisites
Before you start:
- Create a Stripe account
- Have your Stripe API keys ready (Dashboard → Developers → API keys)
- Install the Stripe CLI for local webhook testing
Step 1: Environment Variables
Add these variables to your .env.local file:
# Stripe API KeysSTRIPE_SECRET_KEY=sk_test_...STRIPE_WEBHOOK_SECRET=whsec_...NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...| Variable | Description | Where to Find |
|---|---|---|
STRIPE_SECRET_KEY | Server-side API key | Dashboard → Developers → API keys |
STRIPE_WEBHOOK_SECRET | Webhook signature verification | Generated by Stripe CLI or Dashboard |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | Client-side key (safe to expose) | Dashboard → Developers → API keys |
Never commit secret keys
Add STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET to .env.local only. Never add them to .env or commit them to your repository.
Step 2: Configure Billing Provider
Ensure Stripe is set as your billing provider:
NEXT_PUBLIC_BILLING_PROVIDER=stripeAnd in the database:
UPDATE public.config SET billing_provider = 'stripe';Step 3: Create Products in Stripe
- Go to Stripe Dashboard → Products
- Click Add product
- Configure your product:
- Name: "Pro Plan", "Starter Plan", etc.
- Pricing: Add prices for monthly and yearly intervals
- Price ID: Copy the
price_xxxID for your billing schema
Important: The Price ID (e.g., price_1NNwYHI1i3VnbZTqI2UzaHIe) must match the id field in your billing schema's line items.
Step 4: Set Up Local Webhooks with Stripe CLI
The Stripe CLI forwards webhook events from Stripe to your local development server.
Using Docker (Recommended)
First, log in to Stripe:
docker run --rm -it --name=stripe \ -v ~/.config/stripe:/root/.config/stripe \ stripe/stripe-cli:latest loginThis opens a browser window to authenticate. Complete the login process.
Then start listening for webhooks:
pnpm run stripe:listenOr manually:
docker run --rm -it --name=stripe \ -v ~/.config/stripe:/root/.config/stripe \ stripe/stripe-cli:latest listen \ --forward-to http://host.docker.internal:3000/api/billing/webhookUsing Stripe CLI Directly
If you prefer installing Stripe CLI globally:
# macOSbrew install stripe/stripe-cli/stripe# Loginstripe login# Listen for webhooksstripe listen --forward-to localhost:3000/api/billing/webhookCopy the Webhook Secret
When you start listening, the CLI displays a webhook signing secret:
> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxCopy this value and add it to your .env.local:
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxRe-run after restart
The webhook secret changes each time you restart the Stripe CLI. Update your .env.local accordingly.
Linux Troubleshooting
If webhooks aren't reaching your app on Linux, try adding --network=host:
docker run --rm -it --name=stripe \ -v ~/.config/stripe:/root/.config/stripe \ stripe/stripe-cli:latest listen \ --network=host \ --forward-to http://localhost:3000/api/billing/webhookStep 5: Configure Customer Portal
The Stripe Customer Portal lets users manage their subscriptions, payment methods, and invoices.
- Go to Stripe Dashboard → Settings → Billing → Customer portal
- Configure these settings:
Payment methods:
- Allow customers to update payment methods: ✅
Subscriptions:
- Allow customers to switch plans: ✅
- Choose products customers can switch between
- Configure proration behavior
Cancellations:
- Allow customers to cancel subscriptions: ✅
- Configure cancellation behavior (immediate vs. end of period)
Invoices:
- Allow customers to view invoice history: ✅

Step 6: Production Webhooks
When deploying to production, configure webhooks in the Stripe Dashboard:
- Go to Stripe Dashboard → Developers → Webhooks
- Click Add endpoint
- Enter your webhook URL:
https://yourdomain.com/api/billing/webhook - Select events to listen for:
Required events:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deleted
For one-off payments (optional):
checkout.session.async_payment_failedcheckout.session.async_payment_succeeded
- Click Add endpoint
- Copy the signing secret and add it to your production environment variables
Use a public URL
Webhook URLs must be publicly accessible. Vercel preview deployments with authentication enabled won't work. Test by visiting the URL in an incognito browser window.
Free Trials Without Credit Card
Allow users to start a trial without entering payment information:
STRIPE_ENABLE_TRIAL_WITHOUT_CC=trueWhen enabled, users can start a subscription with a trial period and won't be charged until the trial ends. They'll need to add a payment method before the trial expires.
You must also set trialDays in your billing schema:
{ id: 'pro-monthly', name: 'Pro Monthly', paymentType: 'recurring', interval: 'month', trialDays: 14, // 14-day free trial lineItems: [/* ... */],}Migrating Existing Subscriptions
If you're migrating to Makerkit with existing Stripe subscriptions, you need to add metadata to each subscription.
Makerkit expects this metadata on subscriptions:
{ "accountId": "uuid-of-the-account"}Option 1: Add metadata manually
Use the Stripe Dashboard or a migration script to add the accountId metadata to existing subscriptions.
Option 2: Modify the webhook handler
If you can't update metadata, modify the webhook handler to look up accounts by customer ID:
packages/billing/stripe/src/services/stripe-webhook-handler.service.ts
// Instead of:const accountId = subscription.metadata.accountId as string;// Query your database:const { data: customer } = await supabase .from('billing_customers') .select('account_id') .eq('customer_id', subscription.customer) .single();const accountId = customer?.account_id;Common Issues
Webhooks not received
- Check the CLI is running:
pnpm run stripe:listenshould show "Ready!" - Verify the secret: Copy the new webhook secret after each CLI restart
- Check the account: Ensure you're logged into the correct Stripe account
- Check the URL: The webhook endpoint is
/api/billing/webhook
"No such price" error
The Price ID in your billing schema doesn't exist in Stripe. Verify:
- You're using test mode keys with test mode prices (or live with live)
- The Price ID is copied correctly from Stripe Dashboard
Subscription not appearing in database
- Check webhook logs in Stripe Dashboard → Developers → Webhooks
- Look for errors in your application logs
- Verify the
accountIdis correctly passed in checkout metadata
Customer Portal not loading
- Ensure the Customer Portal is configured in Stripe Dashboard
- Check that the customer has a valid subscription
- Verify the
customerIdis correct
Testing Checklist
Before going live:
- [ ] Test subscription checkout with test card
4242 4242 4242 4242 - [ ] Verify subscription appears in user's billing section
- [ ] Test subscription upgrade/downgrade via Customer Portal
- [ ] Test subscription cancellation
- [ ] Verify webhook events are processed correctly
- [ ] Test with failing card
4000 0000 0000 0002to verify error handling - [ ] For trials: test trial expiration and conversion to paid
Related Documentation
- Billing Overview - Architecture and concepts
- Billing Schema - Configure your pricing
- Webhooks - Custom webhook handling
- Metered Usage - Report usage to Stripe
- Per-Seat Billing - Team-based pricing