Stripe Subscription Templates: Build a Next.js Billing Boilerplate
Subscription billing is the backbone of SaaS businesses. Setting up Stripe subscriptions correctly — with webhooks, proration, trial periods, upgrade/downgrade handling, and customer portal integration — is one of the most complex parts of building a SaaS product. A single mistake in webhook handling can lead to billing errors, angry customers, and lost revenue.
A Stripe subscription template or Next.js billing boilerplate eliminates this complexity by providing production-ready billing infrastructure that handles every edge case.
Why Billing Is So Complex
Subscription billing seems simple at first: charge users monthly and let them upgrade or cancel. In practice, production billing requires handling:
- Subscription creation with trial periods
- Plan upgrades with prorated charges
- Plan downgrades with credits
- Cancellation at period end vs immediate
- Failed payment recovery with dunning
- Invoice generation and email delivery
- Tax calculation and collection
- Multi-currency support
- Usage-based billing for metered products
- Coupon and discount application
Each of these scenarios requires careful Stripe API handling, webhook processing with idempotency, and database synchronization.
What a Stripe Subscription Template Includes
A production-ready Next.js billing boilerplate should handle:
Checkout Flow
Stripe Checkout or custom Elements UI for collecting payment details. The template should support both one-time payments and recurring subscriptions with plan selection.
Webhook Processing
Stripe sends webhooks for dozens of events — subscription created, updated, deleted, payment succeeded, payment failed, invoice finalized. A proper webhook handler verifies signatures, processes events idempotently, and updates your database.
Customer Portal
Stripe's hosted customer portal lets users manage their subscription — upgrade, downgrade, cancel, update payment method, and download invoices. The template should generate the portal link and redirect users after authentication.
Setting Up Stripe Billing
Here is how to configure a Stripe subscription template:
git clone https://github.com/breafio/saas-starter-kit.git my-saas
cd my-saas
pnpm install
cp .env.example .env.localEnvironment Configuration
# .env.local
STRIPE_SECRET_KEY=sk_live_your_stripe_secret
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_your_publishable_key
STRIPE_PRICE_STARTER=price_starter_monthly
STRIPE_PRICE_PRO=price_pro_monthly
STRIPE_PRICE_ENTERPRISE=price_enterprise_monthlySubscription API Route
// app/api/create-subscription/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(req: Request) {
const session = await auth()
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const { priceId } = await req.json()
// Create or retrieve the Stripe customer
let customerId: string
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { stripeCustomerId: true, email: true, name: true },
})
if (user?.stripeCustomerId) {
customerId = user.stripeCustomerId
} else {
const customer = await stripe.customers.create({
email: user!.email!,
name: user!.name ?? undefined,
metadata: { userId: session.user.id },
})
customerId = customer.id
await prisma.user.update({
where: { id: session.user.id },
data: { stripeCustomerId: customer.id },
})
}
// Create checkout session
const checkout = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
subscription_data: {
metadata: { userId: session.user.id },
},
})
return NextResponse.json({ url: checkout.url })
}Webhook Handler
Webhook processing is the most critical part of billing infrastructure:
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { prisma } from '@/lib/prisma'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
export async function POST(req: Request) {
const body = await req.text()
const signature = req.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
} catch {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
const subscriptionId = session.subscription as string
const customerId = session.customer as string
const userId = session.metadata?.userId
if (userId) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId)
await prisma.user.update({
where: { id: userId },
data: {
stripeCustomerId: customerId,
stripeSubscriptionId: subscriptionId,
stripeSubscriptionStatus: subscription.status,
stripePriceId: subscription.items.data[0].price.id,
},
})
}
break
}
case 'invoice.payment_succeeded': {
const invoice = event.data.object as Stripe.Invoice
if (invoice.subscription) {
const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string)
const userId = subscription.metadata?.userId
if (userId) {
await prisma.user.update({
where: { id: userId },
data: { stripeSubscriptionStatus: subscription.status },
})
}
}
break
}
case 'customer.subscription.updated':
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
const userId = subscription.metadata?.userId
if (userId) {
await prisma.user.update({
where: { id: userId },
data: {
stripeSubscriptionStatus: subscription.status,
stripePriceId: subscription.items.data[0]?.price.id ?? null,
stripeSubscriptionId: subscription.id,
},
})
}
break
}
}
return NextResponse.json({ received: true })
}Customer Portal
Let users manage their billing without building UI:
// app/api/portal/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function GET() {
const session = await auth()
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { stripeCustomerId: true },
})
if (!user?.stripeCustomerId) {
return NextResponse.json({ error: 'No customer found' }, { status: 400 })
}
const portal = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
})
return NextResponse.json({ url: portal.url })
}Subscription Status Check
Middleware to gate premium features:
// lib/stripe/access.ts
import { prisma } from '@/lib/prisma'
const ALLOWED_STATUSES = ['active', 'trialing', 'past_due']
export async function checkSubscriptionAccess(userId: string, requiredPlan?: string) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { stripeSubscriptionStatus: true, stripePriceId: true },
})
if (!user) return false
if (!ALLOWED_STATUSES.includes(user.stripeSubscriptionStatus!)) return false
if (requiredPlan && user.stripePriceId !== requiredPlan) return false
return true
}The Bottom Line
Subscription billing is the most critical infrastructure in your SaaS. A single bug can cause revenue loss, customer churn, and support headaches. A production-ready Stripe subscription template handles every edge case — webhook processing, proration, dunning, customer portal — so you never have to debug billing code while customers are being charged incorrectly.
Browse all 200+ templates and starter kits and find the perfect billing foundation for your SaaS.
Related Template
Try this production-ready starter kit to build your project faster.
Shop All Products
Wireless Headphones
Leather Backpack
Smart Watch
Sunglasses
E-Commerce Starter
Build your online store the smart way
Ready to Build?
Get started with our production-ready starter kits and ship your project faster.
Browse Starter Kits