Implementing Stripe Subscriptions in Next.js 15 — Complete Guide
Step-by-step tutorial for adding subscription billing to your Next.js app using Stripe Checkout, webhooks, billing portal, and the Stripe customer portal.
Subscription billing is the revenue backbone of B2B and consumer SaaS platforms. Here is the complete architectural guide for building robust Stripe integrations in Next.js 15.
The Subscription Flow Architecture
1. User clicks "Upgrade" -> API Route creates Stripe Checkout Session -> User Redirected to Stripe
2. User pays on Stripe -> Stripe fires Webhook -> Next.js updates User DB with subscription status
3. User goes to Dashboard -> Checked via DB status -> Grants/denies access
Step 1: Stripe Checkout Route Handler
Create a Next.js API route handler to generate the Stripe checkout redirect URL:
// src/app/api/stripe/checkout/route.ts
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
export async function POST(req: Request) {
try {
const { priceId, email, userId } = await req.json();
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
billing_address_collection: "auto",
customer_email: email,
line_items: [{ price: priceId, quantity: 1 }],
mode: "subscription",
subscription_data: {
metadata: { userId },
},
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?billing=success`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
});
return NextResponse.json({ url: session.url });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
Step 2: Handle Stripe Webhooks
Do not rely on client-side redirects to update your database. Always listen to Stripe Webhooks to update subscription statuses:
// src/app/api/stripe/webhook/route.ts
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { headers } from "next/headers";
export async function POST(req: Request) {
const body = await req.text();
const signature = (await headers()).get("Stripe-Signature");
let event;
try {
event = stripe.webhooks.constructEvent(
body,
signature!,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err: any) {
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
const session = event.data.object as any;
if (event.type === "checkout.session.completed") {
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
const userId = session.subscription_data.metadata.userId;
// Update database with subscription details
await db.user.update({
where: { id: userId },
data: {
stripeSubscriptionId: subscription.id,
stripeCustomerId: session.customer as string,
stripePriceId: subscription.items.data[0].price.id,
subscriptionPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
return NextResponse.json({ received: true });
}
Step 3: Database Verification Middleware
To guard premium routes, check the customer's subscription validity in Next.js middleware or server components:
// src/lib/subscription.ts
export async function getSubscriptionStatus(userId: string) {
const user = await db.user.findUnique({
where: { id: userId },
select: {
stripeSubscriptionId: true,
subscriptionPeriodEnd: true,
},
});
if (!user || !user.stripeSubscriptionId) {
return { isActive: false };
}
// Check if current date is before period end date
const isActive = user.subscriptionPeriodEnd
? new Date(user.subscriptionPeriodEnd).getTime() > Date.now()
: false;
return { isActive };
}
Key Edge Cases to Handle
- Payment Failures: Listen to the
invoice.payment_failedwebhook event and send an automated warning email to the user. - Grace Periods: If a renewal fails, Stripe can retries payment. Provide a 3-day grace period before locking the user's account dashboard.
- Cancellations: Listen to
customer.subscription.deletedto revoke access immediately upon subscription cancellation.
Ready to build something amazing?
Stop guessing and start building. Book a call with our technical experts to discuss your project requirements, architecture, and timeline.
Book a Free Consultation