TutorialsApr 25, 2026 · 15 min read

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

  1. Payment Failures: Listen to the invoice.payment_failed webhook event and send an automated warning email to the user.
  2. Grace Periods: If a renewal fails, Stripe can retries payment. Provide a 3-day grace period before locking the user's account dashboard.
  3. Cancellations: Listen to customer.subscription.deleted to revoke access immediately upon subscription cancellation.
Loading views...

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