# Configure Stripe Billing for Your Tanstack Start SaaS

> Complete guide to setting up Stripe payments in Makerkit. Configure subscriptions, one-off payments, webhooks, and the Customer Portal for your Tanstack Start Supabase application.

*Canonical: https://makerkit.dev/docs/tanstack-supabase/billing/stripe*

---

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_...
VITE_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 |
| `VITE_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
VITE_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.updated`
- `customer.subscription.deleted`
- `invoice.paid`

**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 `VITE_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: [/* ... */],
}
```

### Trial Eligibility (One Trial Per User)

Free trials are granted **once per user identity**, not once per account. When a
user starts checkout, Makerkit checks whether they already have billing history
(a billing customer record) on **any account they own** — across both their
personal account and any team accounts they own. If they do, the trial period is
removed and they are charged immediately.

This prevents a user from obtaining repeated free trials by creating additional
team accounts (each account would otherwise be treated as a brand-new customer
and granted its own trial). Because the check is identity-wide, a user who has
already trialed or subscribed on their personal account will not receive another
trial when subscribing a team account they own, and vice versa.

First-time users with no prior billing history are unaffected and receive the
trial as configured by `trialDays`.

{% alert type="warning" title="Lemon Squeezy trials are configured differently" %}
This eligibility check covers the Stripe checkout path, where `trialDays` is
sent to the provider at checkout. Lemon Squeezy trials are configured on the
variant in the Lemon Squeezy dashboard and are not suppressed by this check.
{% /alert %}

## 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/tanstack-supabase/billing/overview) - Architecture and concepts
- [Billing Schema](/docs/tanstack-supabase/billing/billing-schema) - Configure your pricing
- [Webhooks](/docs/tanstack-supabase/billing/billing-webhooks) - Custom webhook handling
- [Metered Usage](/docs/tanstack-supabase/billing/metered-usage) - Report usage to Stripe
- [Per-Seat Billing](/docs/tanstack-supabase/billing/per-seat-billing) - Team-based pricing
