Sabo includes a production-ready Stripe integration with subscription management, checkout sessions, customer portal, and webhook handling. This guide walks you through setup, configuration, and testing.
Overview
Sabo’s Stripe integration provides:- Subscription checkout with Stripe Checkout hosted pages
- Plan management via centralized configuration in
src/lib/payments/plans.ts - Customer portal for subscription management, payment methods, and invoices
- Webhook handling for subscription lifecycle events and payment tracking
- Database sync with automatic updates to
user_subscriptionsandpayment_historytables
Quick Start
Get Stripe API keys
Create a Stripe account (or use test mode) and copy your API keys.
- Visit Stripe Dashboard → API Keys
- Ensure you’re in Test Mode (toggle in top right)
- Copy your Publishable key (
pk_test_...) and Secret key (sk_test_...)
Create products and prices
Create the product and prices directly in the Stripe Dashboard:After updating the values, restart your dev server so the new environment variables are loaded.
- Go to Stripe Dashboard → Products and click Add product.
- Name the product “Pro” (or any plan name that matches your
plans.tsconfiguration). - Under Pricing, add two recurring prices:
- Monthly:
$12.00, billing period Monthly - Yearly:
$120.00, billing period Yearly
- Monthly:
- Save the product and copy the generated price IDs (
price_...).
.env.local file:.env.local
Set up webhook forwarding
Webhooks notify your app when subscription events occur (created, updated, cancelled, payments). Choose the approach that fits your environment:
- Option A · Stripe CLI (local development)
- Option B · Stripe Dashboard (deployed environments)
- Install the Stripe CLI (see docs) and log in:
- Forward events to your local app (keep this terminal running):
- Copy the webhook secret from the CLI output (
whsec_...) and add it to.env.local:
.env.local
Use two terminals: one for
pnpm dev, another for stripe listen. Restart Stripe CLI whenever you restart the dev server.Test the integration
Start your development server and test the complete flow:
- Visit
http://localhost:3000/pricing - Click “Upgrade to Pro”
- If not logged in, you’ll be redirected to sign in
- After authentication, you’ll be redirected to Stripe Checkout
- Use test card
4242 4242 4242 4242(any future date, any CVC) - Complete checkout and return to the success page 🎉
- Visit
/dashboard/settings/billingto see your subscription
Verify webhook events are received in the Stripe CLI terminal with “200 OK” responses.
Configuration
Plans Configuration
Sabo centralizes plan configuration insrc/lib/payments/plans.ts. This file defines all subscription plans, pricing, features, and Stripe price IDs.
src/lib/payments/plans.ts (excerpt)
src/components/marketing/pricing.tsx) automatically reads from this configuration.
Helper Functions
The same file also exports helper utilities used by pricing UI, API routes, and webhooks:src/lib/payments/plans.ts (excerpt)
pricing.tsx and the Stripe webhook handler to keep plan lookups consistent across the app.
API Endpoints
Sabo provides three Stripe API endpoints for subscription management. This section offers a quick overview.Quick Reference
| Endpoint | Purpose | Authentication | Documentation |
|---|---|---|---|
POST /api/checkout_sessions | Create Stripe Checkout session for subscriptions | Required | Full API docs → |
POST /api/customer_portal | Open Stripe Customer Portal for subscription management | Required | Full API docs → |
POST /api/webhooks/stripe | Handle Stripe webhook events | Webhook signature | Details below |
Common Usage Pattern
Here’s how the pricing page initiates checkout:src/components/marketing/pricing.tsx (excerpt)
src/app/(dashboard)/dashboard/settings/billing/page.tsx (excerpt)
Need to surface subscription status, cancel buttons, or invoices inside Sabo? Customize the billing settings UI described in Dashboard → Settings; it consumes the same subscription data that Stripe webhooks write.
Webhook Handler
The webhook endpoint (POST /api/webhooks/stripe) is critical for keeping your database in sync with Stripe subscription events.
Supported Events
| Event | Trigger | Action |
|---|---|---|
customer.subscription.created | New subscription started | Creates user_subscriptions record |
customer.subscription.updated | Subscription changed (plan, status, cancellation) | Updates user_subscriptions |
customer.subscription.deleted | Subscription ended | Marks subscription as deleted |
invoice.payment_succeeded | Payment successful | Records payment in payment_history |
invoice.payment_failed | Payment failed | Records failed payment |
How It Works
src/app/api/webhooks/stripe/route.ts (simplified)
Stripe invoices don’t always include customer metadata directly. The production handler retrieves the subscription (and its metadata) when processing payment events to reliably recover
user_id, even for complex invoices (e.g., those generated off-cycle or retried).Key Implementation Details
Signature Verification
Signature Verification
Webhooks verify requests are from Stripe using the Without verification, anyone could send fake events to your endpoint. Always verify signatures in production.
STRIPE_WEBHOOK_SECRET:User ID from Metadata
User ID from Metadata
When creating checkout sessions, we store Webhooks use this metadata to link Stripe subscriptions to your Supabase users.
user_id in metadata:Plan Name Lookup
Plan Name Lookup
Webhooks use This ensures plan names stay consistent between your UI and database.
getPlanByPriceId() helper to determine plan names:RLS Bypass with Service Client
RLS Bypass with Service Client
Webhooks need Without this, webhooks can’t write to
SUPABASE_SECRET_KEY to bypass Row Level Security (RLS):user_subscriptions or payment_history tables.Database Schema
Stripe webhooks automatically sync subscription data to two Supabase tables:user_subscriptions
Stores current subscription state for each user.| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
user_id | uuid | Foreign key to auth.users |
stripe_customer_id | text | Stripe customer ID (cus_...) |
stripe_subscription_id | text | Stripe subscription ID (sub_...) |
stripe_price_id | text | Current price ID |
plan_name | text | Human-readable plan name (from plans.ts) |
status | text | active, trialing, canceled, past_due, etc. |
billing_cycle | text | month or year |
current_period_start | timestamptz | Start of current billing period |
current_period_end | timestamptz | End of current billing period |
cancel_at_period_end | boolean | Will cancel at end of period |
cancel_at | timestamptz | Scheduled cancellation date |
canceled_at | timestamptz | When user cancelled |
cancellation_reason | text | Why cancelled |
trial_start / trial_end | timestamptz | Trial period dates |
created_at / updated_at | timestamptz | Record timestamps |
payment_history
Stores all payment transactions for audit and billing history.| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
user_id | uuid | Foreign key to auth.users |
stripe_subscription_id | text | Related subscription |
stripe_payment_intent_id | text | Stripe payment intent ID (pi_...) |
amount | integer | Amount in cents (e.g., 1200 = $12.00) |
currency | text | Currency code (usd) |
status | text | succeeded or failed |
description | text | Payment description |
invoice_url | text | Stripe hosted invoice URL |
created_at | timestamptz | Payment timestamp |
Both tables are automatically created by the Supabase migration in
supabase/migrations/20240101000000_create_user_profiles.sql.Testing
Test Cards
Stripe provides test cards for various scenarios:| Card Number | Scenario |
|---|---|
4242 4242 4242 4242 | Successful payment |
4000 0000 0000 0002 | Declined payment |
4000 0025 0000 3155 | Requires authentication (3D Secure) |
4000 0000 0000 0341 | Attaches but fails on subsequent charges |
Testing Checklist
1. Complete Checkout Flow
1. Complete Checkout Flow
Test the full user journey from pricing to dashboard:
- Visit
/pricingwhile logged out - Click “Upgrade to Pro” → redirected to
/sign-in - Sign in and return to pricing
- Click “Upgrade to Pro” → redirected to Stripe Checkout
- Complete payment with test card
- Return to success page
- Check
/dashboard/settings/billingfor subscription details
- Checkout session opens with correct price
- Email is pre-filled
- Success page shows after payment
- Subscription appears in billing dashboard
2. Webhook Processing
2. Webhook Processing
Verify webhooks are received and processed correctly:
- Check Stripe CLI terminal for webhook events:
- Check Supabase tables:
user_subscriptions: New row with subscription datapayment_history: Payment record withstatus: "succeeded"
- Verify plan name matches
plans.tsconfiguration
- 400 errors: Wrong
STRIPE_WEBHOOK_SECRET - 500 errors: Missing
SUPABASE_SECRET_KEY - Empty plan_name: Price ID not in
plans.ts
3. Customer Portal
3. Customer Portal
Test subscription management via Stripe Customer Portal:
- Go to
/dashboard/settings/billing - Click “Manage Subscription”
- Verify redirect to Stripe Customer Portal
- Test updating payment method
- Test cancelling subscription
- Check webhook events fire (
customer.subscription.updated) - Verify cancellation reflected in dashboard
- Portal loads with correct subscription
- Changes sync back to Supabase within seconds
cancel_at_period_endupdates totrue
4. Yearly vs Monthly Toggle
4. Yearly vs Monthly Toggle
Test billing cycle switching:
- On
/pricing, toggle between Monthly and Yearly - Verify prices update (yearly shows discount)
- Click “Upgrade to Pro” for yearly
- Verify Stripe Checkout shows yearly price ($120/year)
- Complete checkout
- Check
user_subscriptions.billing_cycleis"year"
- Price toggle animates smoothly
- Checkout URL includes yearly price ID
- Webhook records correct billing cycle
Production Deployment
Switch to live mode
In Stripe Dashboard, toggle from Test Mode to Live Mode (top right). Create new products and prices in live mode, or copy test mode products.Update
.env (production environment):Create production webhook
In Stripe Dashboard → Webhooks, add a new endpoint:
- Endpoint URL:
https://yourdomain.com/api/webhooks/stripe - Events to send: Select all:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeededinvoice.payment_failed
- Copy signing secret and add to production environment as
STRIPE_WEBHOOK_SECRET
Test with real payment
Before going live, test with a real small-amount payment:
- Create a test plan with a low price ($1 monthly)
- Complete checkout with a real card
- Verify webhook processing works
- Cancel the test subscription
- Switch back to your production pricing
Configure Customer Portal
Customize the Stripe Customer Portal for your brand:
- Go to Stripe Dashboard → Settings → Customer portal
- Configure:
- Branding: Logo, colors, fonts
- Features: Which actions customers can take (cancel, update payment, view invoices)
- Business information: Support email, terms, privacy policy
- (Optional) Create a custom portal configuration and set
STRIPE_CUSTOMER_PORTAL_CONFIG_IDenvironment variable
Test the portal at
/dashboard/settings/billing after configuration.Advanced Topics
Handling One-Time Payments
By default, Sabo processes recurring subscriptions. To accept one-time payments (lifetime access, credits, digital products):Create one-time price in Stripe
In Stripe Dashboard, create a new price with “One time” pricing model instead of “Recurring”.
Custom Trial Periods
To add trial periods to subscriptions:src/app/api/checkout_sessions/route.ts
user_subscriptions table.
Promotional Codes
Sabo enables promotional codes by default:src/app/api/checkout_sessions/route.ts
Troubleshooting
401 Unauthorized on checkout
401 Unauthorized on checkout
400 Bad Request - Invalid price ID
400 Bad Request - Invalid price ID
Cause: Price ID is
null, undefined, or doesn’t exist in Stripe.Fix:- Verify price IDs are set in
.env.local - Run
npx tsx scripts/setup-stripe-products.tsto create products - Restart dev server after changing env vars
- Check price IDs match those in Stripe Dashboard
Webhook signature verification failed
Webhook signature verification failed
Cause: Wrong
STRIPE_WEBHOOK_SECRET or request not from Stripe.Fix:- For local: Copy webhook secret from Stripe CLI output
- For production: Copy signing secret from Stripe Dashboard webhook settings
- Ensure webhook URL is exactly
/api/webhooks/stripe - Check Stripe CLI is running:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Subscription created but not in database
Subscription created but not in database
Cause: Missing
SUPABASE_SECRET_KEY or webhook processing error.Fix:- Add
SUPABASE_SECRET_KEYto environment (get from Supabase Dashboard → Settings → API) - Check webhook handler logs in terminal for errors
- Verify
user_idis in subscription metadata - Check Supabase table permissions (RLS policies)
Plan name shows as null or 'Unknown Plan'
Plan name shows as null or 'Unknown Plan'
Cause: Price ID not found in
plans.ts.Fix:- Add price ID to
stripePriceIdsinplans.ts - Ensure environment variables match plan configuration
- Webhook uses
getPlanByPriceId()to look up plan names
Customer portal not working
Customer portal not working
Cause: No subscription found or missing customer ID.Fix:
- User must have an active subscription first
- Check
user_subscriptionstable hasstripe_customer_id - Verify subscription status is not
canceledorincomplete - Test creating a new subscription first