This guide will walk you through setting up Stripe payments and subscriptions for your application.
- Overview
- Prerequisites
- Database Setup
- Stripe Configuration
- Environment Variables
- Webhook Setup
- Testing
- Production Deployment
The Stripe integration provides:
- One-off purchases for credits
- Recurring subscriptions with multiple tiers
- Usage-based credit system for tracking feature usage
- Secure webhook handling for real-time payment updates
- RLS-protected database with secure RPC functions
Frontend → Next.js API Routes (Cloudflare Workers) → Stripe API
↓
Supabase Database
↑
Stripe Webhooks (via API Routes)
- Stripe Account: Sign up for Stripe
- Supabase Project: Active Supabase project with database access
- Cloudflare Account: For deploying Next.js API routes
Execute the SQL migrations in your Supabase SQL Editor in this order:
-
Profiles Table
# File: supabase/migrations/20250120_create_profiles_table.sqlThis creates the
profilestable with Stripe customer ID and credits tracking. -
Subscriptions & Pricing Tables
# File: supabase/migrations/20250120_create_subscriptions_table.sqlThis creates
subscriptions,products, andpricestables. -
RPC Functions
# File: supabase/migrations/20250120_create_rpc_functions.sqlThis creates secure functions for credit management.
After running migrations, verify the tables exist:
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('profiles', 'subscriptions', 'products', 'prices');- Go to Stripe Dashboard
- Copy your Secret key (starts with
sk_test_...) - Copy your Publishable key (starts with
pk_test_...)
-
Go to Products → Add Product
-
Create products for:
- Credit Packs (one-time payment)
- Example: "100 Credits Pack" - $9.99
- Add metadata:
{ "credits_amount": "100" }
- Subscription Tiers (recurring)
- Example: "Pro Plan" - $29/month
- Credit Packs (one-time payment)
-
Copy the Price ID for each (starts with
price_...)
# Create a one-time credit pack
stripe products create \
--name="100 Credits Pack" \
--description="100 credits for image processing"
stripe prices create \
--product=prod_XXX \
--unit-amount=999 \
--currency=usd \
--metadata[credits_amount]=100
# Create a subscription
stripe products create \
--name="Pro Plan" \
--description="Professional subscription tier"
stripe prices create \
--product=prod_YYY \
--unit-amount=2900 \
--currency=usd \
--recurring[interval]=monthThis project uses a split environment variable structure:
.env - Public variables (safe to commit examples):
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
# App
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# Stripe Price IDs are now configured in shared/config/stripe.ts
# See shared/config/stripe.ts for Price ID configuration.env.prod - Server-side secrets (NEVER commit):
# Supabase
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Stripe
STRIPE_SECRET_KEY=sk_test_your-secret-key
STRIPE_WEBHOOK_SECRET=whsec_your-webhook-secretSecurity Notes:
⚠️ NEVER commit.env.prodto version control⚠️ SUPABASE_SERVICE_ROLE_KEYandSTRIPE_SECRET_KEYare server-side only⚠️ OnlyNEXT_PUBLIC_*variables are exposed to the client
Set environment variables in Cloudflare Dashboard:
- Go to Workers & Pages → Your Project → Settings → Environment Variables
- Add:
SUPABASE_SERVICE_ROLE_KEYSTRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETNEXT_PUBLIC_BASE_URL
Webhooks ensure your database stays in sync with Stripe events.
-
Install Stripe CLI
# macOS brew install stripe/stripe-cli/stripe # Linux wget https://github.com/stripe/stripe-cli/releases/download/v1.18.0/stripe_1.18.0_linux_x86_64.tar.gz tar -xvf stripe_1.18.0_linux_x86_64.tar.gz
-
Login to Stripe
stripe login
-
Get Webhook Secret
The first time you run
yarn dev, the Stripe CLI will output a webhook signing secret (starts withwhsec_...). Copy this and add it to.env.prod:STRIPE_WEBHOOK_SECRET=whsec_your-webhook-secret
-
Start Development (includes automatic webhook forwarding)
yarn dev
This runs Next.js, Wrangler, and Stripe CLI concurrently:
next dev- Next.js development serverwrangler pages dev- Cloudflare Pages proxystripe listen- Forwards webhooks tolocalhost:3000/api/webhooks/stripe
-
Test a webhook
stripe trigger checkout.session.completed
- Go to Stripe Webhooks
- Click Add endpoint
- Set URL:
https://your-domain.com/api/webhooks/stripe - Select events to listen to:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeededinvoice.payment_failed
- Copy the Signing secret and add to Cloudflare environment variables
Stripe Price IDs are configured in src/config/stripe.ts. This file provides:
STRIPE_PRICES- Maps environment variables to price IDsCREDIT_PACKS- Configuration for one-time credit purchasesSUBSCRIPTION_PLANS- Configuration for recurring subscriptions
The pricing page (app/pricing/page.tsx) uses these configurations automatically.
| Page | Path | Description |
|---|---|---|
| Pricing | /pricing |
Displays all credit packs and subscription plans |
| Success | /success |
Landing page after successful payment |
| Canceled | /canceled |
Landing page after canceled payment |
| Billing | /dashboard/billing |
User's billing dashboard with credits and subscription info |
The Stripe Customer Portal allows users to:
- Update payment methods
- View invoices
- Cancel/modify subscriptions
Access it from the Billing page via "Manage Subscription" button.
import { StripeService } from '@/lib/stripe';
// Create checkout session for 100 credits
const { url } = await StripeService.createCheckoutSession('price_XXX', {
metadata: {
credits_amount: '100',
},
});
// Redirect user to Stripe Checkout
window.location.href = url;import { StripeService } from '@/lib/stripe';
// Create subscription checkout
await StripeService.redirectToCheckout('price_YYY');After successful payment:
-- Check profile was updated
SELECT * FROM profiles WHERE id = 'user-uuid';
-- Check credits were added
SELECT credits_balance FROM profiles WHERE id = 'user-uuid';
-- Check subscription was created
SELECT * FROM subscriptions WHERE user_id = 'user-uuid';- Run all database migrations in production Supabase
- Set all environment variables in Cloudflare Dashboard
- Create products and prices in Live Mode Stripe Dashboard
- Configure production webhook endpoint
- Test with real payment in Test Mode first
- Switch to Live Mode keys
- Monitor webhook deliveries in Stripe Dashboard
- Stripe Dashboard: Monitor payments and subscriptions
- Webhook Logs: Check webhook delivery status
- Supabase Logs: Monitor database operations and RPC calls
- Cloudflare Logs: Check API route performance
import { StripeService } from '@/lib/stripe';
function BuyCreditsButton() {
const handleBuyCredits = async () => {
try {
await StripeService.redirectToCheckout('price_XXX', {
metadata: {
credits_amount: '100',
},
successUrl: `${window.location.origin}/success`,
cancelUrl: `${window.location.origin}/canceled`,
});
} catch (error) {
console.error('Checkout error:', error);
}
};
return <button onClick={handleBuyCredits}>Buy 100 Credits - $9.99</button>;
}import { StripeService } from '@/lib/stripe';
import { useEffect, useState } from 'react';
function CreditsDisplay() {
const [profile, setProfile] = useState(null);
useEffect(() => {
StripeService.getUserProfile().then(setProfile);
}, []);
return <div>Credits: {profile?.credits_balance || 0}</div>;
}import { StripeService } from '@/lib/stripe';
async function processImage() {
const hasCredits = await StripeService.hasSufficientCredits(1);
if (!hasCredits) {
alert('Insufficient credits. Please purchase more.');
return;
}
try {
// Perform the action
await performImageProcessing();
// Deduct credits
const newBalance = await StripeService.decrementCredits(1);
console.log(`New balance: ${newBalance} credits`);
} catch (error) {
console.error('Processing error:', error);
}
}import { StripeService } from '@/lib/stripe';
function ManageSubscriptionButton() {
const [loading, setLoading] = useState(false);
const handleManageSubscription = async () => {
try {
setLoading(true);
await StripeService.redirectToPortal();
} catch (error) {
console.error('Portal error:', error);
alert('Failed to open billing portal');
} finally {
setLoading(false);
}
};
return (
<button onClick={handleManageSubscription} disabled={loading}>
{loading ? 'Opening...' : 'Manage Subscription'}
</button>
);
}- Ensure
STRIPE_WEBHOOK_SECRETis set correctly - Check that you're using the correct secret for your environment (test vs live)
- Verify the webhook endpoint URL matches your configuration
- Check Stripe webhook logs for delivery status
- Verify the
credits_amountmetadata is set in the checkout session - Check Supabase logs for RPC function errors
- Ensure the database trigger creates profiles on user signup
- Manually create profile:
INSERT INTO profiles (id) VALUES ('user-uuid')
- Never expose secret keys - Only use in server-side code
- Verify webhook signatures - Always validate Stripe signatures
- Use RLS policies - Prevent direct database modifications
- Use RPC functions - Secure credit operations with SECURITY DEFINER
- Validate user input - Always validate price IDs and amounts
- Monitor webhook deliveries - Set up alerts for failed webhooks
- Stripe Docs: https://stripe.com/docs
- Supabase Docs: https://supabase.com/docs
- GitHub Issues: [Report issues here]
See main project LICENSE file.