Stripe
Stripe subscriptions with Next.js
How a Next.js App Router app creates a subscription Checkout session, stores the customer id, and gates a Pro plan.
A Stripe subscription in Next.js is a Checkout Session with mode: "subscription", plus a webhook that records the result. The Stripe Subscription Starter does both with the Stripe Node SDK 17. This page is the subscription flow. Checkout details are in Stripe Checkout with Next.js. Webhooks are in Stripe webhooks with Next.js.
Price id, not an amount
Subscriptions bill a Price you create in Stripe. The kit reads STRIPE_PRO_PRICE_ID. It does not hardcode an amount in the route. Create a recurring price in the Stripe Dashboard (test mode is enough locally) and put that id in .env.
The user row stores the link back:
model User {
id String @id @default(cuid())
email String @unique
plan String @default("free")
stripeCustomerId String? @unique
stripeSubscriptionId String?
}
plan starts as "free". Nothing in the UI sets it to "pro" except the webhook after Checkout completes.
Create the session from a route handler
The kit’s Checkout route is a GET handler so a normal link can start it. It refuses anonymous users, reuses stripeCustomerId when one exists, and otherwise creates a Customer with metadata.userId.
const session = await client.checkout.sessions.create({
mode: "subscription",
customer,
line_items: [{ price: process.env.STRIPE_PRO_PRICE_ID || "", quantity: 1 }],
success_url: `${process.env.APP_URL}/dashboard`,
cancel_url: `${process.env.APP_URL}/pricing`,
metadata: { userId: user.id }
});
return NextResponse.redirect(session.url || "/pricing");
metadata.userId is how the webhook finds the row if the customer id was just created. APP_URL must be the origin Stripe can redirect to, including the scheme.
Gating
After the webhook sets plan to "pro", a server component reads that column:
const user = await currentUser();
if (!user) redirect("/login");
const pro = user.plan === "pro";
Do not treat the success URL as proof of payment. The browser can open /dashboard if someone pastes it. The dashboard may render for a free user; paid actions check plan. The success redirect in this kit does not append {CHECKOUT_SESSION_ID} because the webhook, not the success page, writes the plan.
Versions
stripe is ^17.0.0 and next is ^15.0.0 in the starter’s package.json. The snippets above match that SDK: checkout.sessions.create and customers.create. They are not the older Charges API.
Want to skip the setup? The Stripe Subscription Starter already includes Checkout, webhooks, the Customer Portal, and subscription state on the user row.