--- status: "published" label: "Stripe" title: "Configure Stripe Billing for Your Next.js SaaS" description: "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." order: 2 --- 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: 1. Create a [Stripe account](https://dashboard.stripe.com/register) 2. Have your Stripe API keys ready (Dashboard → Developers → API keys) 3. Install the Stripe CLI for local webhook testing ## Step 1: Environment Variables Add these variables to your `.env.local` file: ```bash # Stripe API Keys STRIPE_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 | | `STRIPE_UI_MODE` | Checkout UI mode: `embedded_page` (default) or `hosted_page` (optional) | - | {% alert type="error" title="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. {% /alert %} ## Step 2: Configure Billing Provider Ensure Stripe is set as your billing provider: ```bash NEXT_PUBLIC_BILLING_PROVIDER=stripe ``` And in the database: ```sql UPDATE public.config SET billing_provider = 'stripe'; ``` ## Step 3: Create Products in Stripe 1. Go to Stripe Dashboard → Products 2. Click **Add product** 3. Configure your product: - **Name**: "Pro Plan", "Starter Plan", etc. - **Pricing**: Add prices for monthly and yearly intervals - **Price ID**: Copy the `price_xxx` ID 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: ```bash docker run --rm -it --name=stripe \ -v ~/.config/stripe:/root/.config/stripe \ stripe/stripe-cli:latest login ``` This opens a browser window to authenticate. Complete the login process. Then start listening for webhooks: ```bash pnpm run stripe:listen ``` Or manually: ```bash 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/webhook ``` ### Using Stripe CLI Directly If you prefer installing Stripe CLI globally: ```bash # macOS brew install stripe/stripe-cli/stripe # Login stripe login # Listen for webhooks stripe listen --forward-to localhost:3000/api/billing/webhook ``` ### Copy the Webhook Secret When you start listening, the CLI displays a webhook signing secret: ``` > Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxx ``` Copy this value and add it to your `.env.local`: ```bash STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx ``` {% alert type="default" title="Re-run after restart" %} The webhook secret changes each time you restart the Stripe CLI. Update your `.env.local` accordingly. {% /alert %} ### Linux Troubleshooting If webhooks aren't reaching your app on Linux, try adding `--network=host`: ```bash 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/webhook ``` ## Step 5: Configure Customer Portal The Stripe Customer Portal lets users manage their subscriptions, payment methods, and invoices. 1. Go to Stripe Dashboard → Settings → Billing → Customer portal 2. 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: ✅ {% img src="/assets/images/docs/stripe-customer-portal.webp" width="2712" height="1870" /%} ## Step 6: Production Webhooks When deploying to production, configure webhooks in the Stripe Dashboard: 1. Go to Stripe Dashboard → Developers → Webhooks 2. Click **Add endpoint** 3. Enter your webhook URL: `https://yourdomain.com/api/billing/webhook` 4. Select events to listen for: **Required events:** - `checkout.session.completed` - `customer.subscription.created` - `customer.subscription.updated` - `customer.subscription.deleted` **For one-off payments (optional):** - `checkout.session.async_payment_failed` - `checkout.session.async_payment_succeeded` 5. Click **Add endpoint** 6. Copy the signing secret and add it to your production environment variables {% alert type="warning" title="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. {% /alert %} ## Checkout UI Mode Stripe supports two checkout UI modes: - **`embedded_page`** (default): Embeds the checkout form directly in your application as a dialog popup. Requires `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`. - **`hosted_page`**: Redirects users to a Stripe-hosted checkout page. The publishable key is not required in this mode. Configure this with the `STRIPE_UI_MODE` environment variable: ```bash STRIPE_UI_MODE=hosted_page ``` If not set, it defaults to `embedded_page`. ## Free Trials Without Credit Card Allow users to start a trial without entering payment information: ```bash STRIPE_ENABLE_TRIAL_WITHOUT_CC=true ``` When 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: ```tsx { 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: ```json { "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: ```tsx {% title="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 1. **Check the CLI is running:** `pnpm run stripe:listen` should show "Ready!" 2. **Verify the secret:** Copy the new webhook secret after each CLI restart 3. **Check the account:** Ensure you're logged into the correct Stripe account 4. **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: 1. You're using test mode keys with test mode prices (or live with live) 2. The Price ID is copied correctly from Stripe Dashboard ### Subscription not appearing in database 1. Check webhook logs in Stripe Dashboard → Developers → Webhooks 2. Look for errors in your application logs 3. Verify the `accountId` is correctly passed in checkout metadata ### Customer Portal not loading 1. Ensure the Customer Portal is configured in Stripe Dashboard 2. Check that the customer has a valid subscription 3. Verify the `customerId` is 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 0002` to verify error handling - [ ] For trials: test trial expiration and conversion to paid ## Related Documentation - [Billing Overview](/docs/next-supabase-turbo/billing/overview) - Architecture and concepts - [Billing Schema](/docs/next-supabase-turbo/billing/billing-schema) - Configure your pricing - [Webhooks](/docs/next-supabase-turbo/billing/billing-webhooks) - Custom webhook handling - [Metered Usage](/docs/next-supabase-turbo/billing/metered-usage) - Report usage to Stripe - [Per-Seat Billing](/docs/next-supabase-turbo/billing/per-seat-billing) - Team-based pricing