Payments
Set up Stripe payments and subscription management
Payments
This boilerplate uses Stripe for payments, supporting one-time purchases and subscriptions.
Configuration
1. Create a Stripe Account
- Go to stripe.com and create an account
- Get your API keys from Developers → API keys
2. Set Environment Variables
Add to your .env.local:
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_PRO_PRICE_ID=price_...3. Create Products in Stripe
- Go to Products → Add product
- Create your pricing plans
- Copy the Price IDs
How It Works
Checkout Session
Create a checkout session to redirect users to Stripe:
// src/lib/stripe/actions.ts
"use server";
import { getStripe } from "@/lib/stripe/client";
import { createClient } from "@/lib/supabase/server";
export async function createCheckoutSession(priceId: string) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error("Not authenticated");
const stripe = getStripe();
const session = await stripe.checkout.sessions.create({
customer_email: user.email,
line_items: [{ price: priceId, quantity: 1 }],
mode: "payment", // or "subscription"
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
metadata: {
user_id: user.id,
},
});
redirect(session.url!);
}Webhooks
Stripe sends events to your webhook endpoint. The boilerplate handles:
checkout.session.completed- Payment successfulcustomer.subscription.updated- Subscription changedcustomer.subscription.deleted- Subscription canceled
// src/app/api/webhooks/stripe/route.ts
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("stripe-signature")!;
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
switch (event.type) {
case "checkout.session.completed":
// Handle successful payment
break;
case "customer.subscription.deleted":
// Handle subscription cancellation
break;
}
return new Response("OK");
}Customer Portal
Allow users to manage their subscription:
export async function createPortalSession() {
const stripe = getStripe();
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/billing`,
});
redirect(session.url);
}Database Schema
Subscriptions are stored in the subscriptions table:
create table subscriptions (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users on delete cascade,
status text not null,
stripe_customer_id text,
stripe_subscription_id text,
stripe_price_id text,
current_period_end timestamptz,
cancel_at_period_end boolean default false,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Enable RLS
alter table subscriptions enable row level security;
create policy "Users can view own subscription"
on subscriptions for select using (auth.uid() = user_id);Hooks
Use the useSubscription hook to check subscription status:
import { useSubscription } from "@/hooks/use-subscription";
export function PremiumFeature() {
const { isSubscribed, loading } = useSubscription();
if (loading) return <Skeleton />;
if (!isSubscribed) return <UpgradePrompt />;
return <FeatureContent />;
}Testing
Use Stripe test mode with these test cards:
| Card | Scenario |
|---|---|
| 4242 4242 4242 4242 | Success |
| 4000 0000 0000 0002 | Declined |
| 4000 0000 0000 3220 | 3D Secure |
Local Webhook Testing
Use Stripe CLI to forward webhooks locally:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks
stripe listen --forward-to localhost:3000/api/webhooks/stripe