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
- 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_...)
Add Stripe keys to environment
.env.local file:Create products and prices
- 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:Set up webhook forwarding
- 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:
pnpm dev, another for stripe listen. Restart Stripe CLI whenever you restart the dev server.Test the integration
- 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
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/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: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: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
user_id, even for complex invoices (e.g., those generated off-cycle or retried).Key Implementation Details
Signature Verification
Signature Verification
STRIPE_WEBHOOK_SECRET:User ID from Metadata
User ID from Metadata
user_id in metadata:Plan Name Lookup
Plan Name Lookup
getPlanByPriceId() helper to determine plan names:RLS Bypass with Service Client
RLS Bypass with Service Client
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 |
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
- 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
- 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
- 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
- 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
.env (production environment):Create production webhook
- 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
- 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
- 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
/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
Add to plans.ts
Modify checkout API
src/app/api/checkout_sessions/route.ts to detect one-time vs subscription:Custom Trial Periods
To add trial periods to subscriptions:user_subscriptions table.
Promotional Codes
Sabo enables promotional codes by default:Troubleshooting
401 Unauthorized on checkout
401 Unauthorized on checkout
400 Bad Request - Invalid price ID
400 Bad Request - Invalid price ID
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
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
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'
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
- 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