Stripe Subscription in Next Js
In this guide, you will learn how to add Stripe subscriptions to a Next.js project with TypeScript and App Router.
Prerequisites
In order to create the entire flow it will be necessary to have a database and authentication in order to associate the Stripe subscriptions with users.
Step 1: Create an empty Next.js project
To get started, copy and run the following command to create an empty Next.js project with the App Router and TypeScript.
npx create-next-app@latest
Step 2: Install the Stripe SDK
Run the following command to install the Stripe SDK.
npm install stripe @stripe/stripe-js
Step 3: Get the Stripe API key
Login to the Stripe dashboard in order to get your Stripe secret key. Create a new Stripe project. Next, click on Developers > API keys. Then, copy your test Stripe secret key.
Then, create a .env file for your environment variables and add your Stripe secret API key.
STRIPE_SECRET_KEY = sk_12345;
NEXT_PUBLIC_STRIPE_PUBLIC_KEY = sk_122455;
Next, create a folder called lib and a file inside that folder called stripe.ts
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2023-10-16",
typescript: true,
});
Step 4: Add subscription plans
In the Stripe dashboard, in the menu on the left, navigate to Products: More > Product catalog. Select Add product in order to create a subscription plan. Copy the price ID of the plan, we will use it later in the Stripe checkout to identify which plan the user wants to purchase.
Note: Do not copy the product ID. Each product can have multiple prices associated with it for example a monthly price as well as an annual price. You want to copy the price ID and pass it later to Stripe checkout.
Step 5:Create .env file
STRIPE_MONTHLY_PRICE_ID=<get_from_stripe>
STRIPE_YEARLY_PRICE_ID=<get_from_stripe>
STRIPE_SECRET_KEY=<get_from_stripe>
STRIPE_WEBHOOK_SECRET=<get_from_stripe>
NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL_URL=<get_from_stripe>
NEXT_PUBLIC_BASE_URL ="http://localhost:3000"
Step 5:Create a Pricing Page.
"use client";
import BoldHeading from "@/components/BoldHeading";
import { Check, Loader2 } from "lucide-react";
import axios from "axios";
import React, { useState } from "react";
import { loadStripe } from "@stripe/stripe-js";
import toast from "react-hot-toast";
import { Button } from "@/components/ui/button";
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);
export default function page() {
// const user = await currentUser();
const [loading, setLoading] = useState(false);
async function handleStripe(priceId: string) {
setLoading(true);
try {
const res = await axios.post(`/api/checkout`, {
priceId,
});
if (res.data) {
setLoading(false);
console.log("data", res.data);
}
if (res.data.sessionId) {
const stripe = await stripePromise;
console.log("stripe", stripe);
const response = await stripe?.redirectToCheckout({
sessionId: res.data.sessionId,
});
console.log("response", response);
return response;
} else {
console.error("Failed to create checkout session");
toast.error("Failed to create checkout session");
return;
}
} catch (error) {
setLoading(false);
console.log(error);
}
}
const plans = [
{
title: "Starter",
description: "Best option for personal use & for your next project",
price: 29,
priceId: "price_1PfZLO0748fNmgXYOppKYz3B",
features: [
" Individual configuration",
"No setup, or hidden fees",
"Team size: 1 developer",
"Premium support: 6 months",
"Free updates: 6 months",
],
},
{
title: "Company",
description: "Relevant for multiple users, extended & premium support.",
price: 99,
priceId: "price_1PfZOr0748fNmgXYbfUQgaN3",
features: [
" Individual configuration",
"No setup, or hidden fees",
"Team size: 10 developers",
"Premium support: 24 months",
"Free updates: 24 months",
],
},
{
title: "Enterprise",
description:
"Best for large scale uses and extended redistribution rights.",
price: 499,
priceId: "price_1PfZQ50748fNmgXYdBzwfE5x",
features: [
"Individual configuration",
"No setup, or hidden fees",
"Team size: 100+ developers",
"Premium support: 36 months",
"Free updates: 36 months",
],
},
];
return (
<section className="bg-white dark:bg-gray-900">
<div className="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
<div className="mx-auto max-w-screen-md text-center mb-8 lg:mb-12">
<BoldHeading heading="Pricing" />
<p className="mb-5 pt-3 font-light text-gray-500 sm:text-xl dark:text-gray-400">
This is Our simplified pricing , save 25% off for every package for
a limited period of time
</p>
</div>
<div className="space-y-8 lg:grid lg:grid-cols-3 sm:gap-6 xl:gap-10 lg:space-y-0">
{plans.map((plan, i) => {
return (
<div className="flex flex-col p-6 mx-auto max-w-lg text-center text-gray-900 bg-white rounded-lg border border-gray-100 shadow dark:border-gray-600 xl:p-8 dark:bg-gray-800 dark:text-white">
<h3 className="mb-4 text-2xl font-semibold">{plan.title}</h3>
<p className="font-light text-gray-500 sm:text-lg dark:text-gray-400">
{plan.description}
</p>
<div className="flex justify-center items-baseline my-8">
<span className="mr-2 text-5xl font-extrabold">
${plan.price}
</span>
<span className="text-gray-500 dark:text-gray-400">
/month
</span>
</div>
<ul role="list" className="mb-8 space-y-4 text-left">
{plan.features.map((feature, i) => {
return (
<li key={i} className="flex items-center space-x-3">
<Check className="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400" />
<span>{feature}</span>
</li>
);
})}
</ul>
<button
onClick={() => handleStripe(plan.priceId)}
className="text-white bg-tertiary-600 hover:bg-tertiary-700 focus:ring-4 focus:ring-tertiary-200 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:text-white dark:focus:ring-tertiary-900"
>
Get started
</button>
</div>
);
})}
</div>
<div className="py-4 text-center">
{loading && (
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing Please wait
</Button>
)}
</div>
</div>
</section>
);
}
Step 6: Create a Stripe checkout endpoint
We will be using the Stripe hosted checkout page to allow our users to purchase a subscription plan.
In order to redirect users to this page, we will need to setup an API endpoint that will get the ID of the price associated with our subscription.
We will also need to pass a user ID to the Stripe checkout session as metadata in order to retrieve it later when we receive the webhook event.
// api/payments/create-checkout-session/route.ts
import getCurrentUser from "@/lib/getCurrentUser";
// import { stripe } from "@/lib/getStripe";
import { NextRequest, NextResponse } from "next/server";
// import Stripe from "stripe";
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
// const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: NextRequest) {
const { priceId } = await req.json();
const user = await getCurrentUser();
const userId = user.id;
const email = user.email;
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
metadata: { userId, email, priceId },
mode: "subscription",
customer_email: email,
billing_address_collection: "auto",
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}&&userId=${userId}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cancel`,
// subscription_data: {
// trial_settings: {
// end_behavior: {
// missing_payment_method: "cancel",
// },
// },
// trial_period_days: 7,
// },
});
return NextResponse.json({ sessionId: session.id });
// return NextResponse.json(user);
} catch (error) {
console.error("Error creating checkout session:", error);
return NextResponse.json({ error });
}
}
Notice the subscription_data object, which tells Stripe to add a trial period of 7 days. It’s a great way to let your users test the product for free before making the decision to purchase a premium plan.
Step7: Create prisma Schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
enum Plan {
free
premium
}
enum SubscriptionPeriod {
monthly
yearly
}
model User {
id String @id @default(cuid()) @map("_id")
email String @unique
name String?
image String?
plan Plan @default(free)
customerId String? @unique // Stripe customer ID, this will be important when we need to delete the subscription
Subscription Subscription?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Subscription {
id String @id @default(cuid()) @map("_id")
userId String @unique
plan Plan
period SubscriptionPeriod
startDate DateTime @default(now())
endDate DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
User User @relation(fields: [userId], references: [id])
}
Step 8: Setup a webhook event handler
When the user successfully completes the checkout process, Stripe will send webhook events to a webhook event handler, which is an API endpoint on our server that will handle these events by updating our database.
First, in the Stripe dashboard go to Developers > Webhooks > Test in local environment. Then, make sure to install the Stripe CLI on your operating system. Once installed, run the stripe login command in your IDE as well as the listen command to forward Stripe webhook events to /webhook.
stripe login
stripe listen --forward-to localhost:3000/webhook
After running these commands, Stripe should provide you with a webhook secret. Make sure to copy it and put it in a .env file as STRIPE_WEBHOOK_SECRET. We will need it later.
STRIPE_WEBHOOK_SECRET = whsec_1234;
Now, we can create our webhook event handler API endpoint that will listen to requests at /webhook and process them. Create a folder in the app directory called api/webhook. Add a route.ts file inside.
import prisma from "@/db/prisma";
import { stripe } from "@/lib/stripe";
import Stripe from "stripe";
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(req: Request) {
const body = await req.text();
const sig = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, WEBHOOK_SECRET);
} catch (err: any) {
console.error("Webhook signature verification failed.", err.message);
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
// Handle the event
try {
switch (event.type) {
case "checkout.session.completed":
const session = await stripe.checkout.sessions.retrieve(
(event.data.object as Stripe.Checkout.Session).id,
{
expand: ["line_items"],
}
);
const customerId = session.customer as string;
const customerDetails = session.customer_details;
if (customerDetails?.email) {
const user = await prisma.user.findUnique({
where: { email: customerDetails.email },
});
if (!user) throw new Error("User not found");
if (!user.customerId) {
await prisma.user.update({
where: { id: user.id },
data: { customerId },
});
}
const lineItems = session.line_items?.data || [];
for (const item of lineItems) {
const priceId = item.price?.id;
const isSubscription = item.price?.type === "recurring";
if (isSubscription) {
let endDate = new Date();
if (priceId === process.env.STRIPE_YEARLY_PRICE_ID!) {
endDate.setFullYear(endDate.getFullYear() + 1); // 1 year from now
} else if (priceId === process.env.STRIPE_MONTHLY_PRICE_ID!) {
endDate.setMonth(endDate.getMonth() + 1); // 1 month from now
} else {
throw new Error("Invalid priceId");
}
// it is gonna create the subscription if it does not exist already, but if it exists it will update it
await prisma.subscription.upsert({
where: { userId: user.id! },
create: {
userId: user.id,
startDate: new Date(),
endDate: endDate,
plan: "premium",
period:
priceId === process.env.STRIPE_YEARLY_PRICE_ID!
? "yearly"
: "monthly",
},
update: {
plan: "premium",
period:
priceId === process.env.STRIPE_YEARLY_PRICE_ID!
? "yearly"
: "monthly",
startDate: new Date(),
endDate: endDate,
},
});
await prisma.user.update({
where: { id: user.id },
data: { plan: "premium" },
});
} else {
// one_time_purchase
}
}
}
break;
case "customer.subscription.deleted": {
const subscription = await stripe.subscriptions.retrieve(
(event.data.object as Stripe.Subscription).id
);
const user = await prisma.user.findUnique({
where: { customerId: subscription.customer as string },
});
if (user) {
await prisma.user.update({
where: { id: user.id },
data: { plan: "free" },
});
} else {
console.error("User not found for the subscription deleted event.");
throw new Error("User not found for the subscription deleted event.");
}
break;
}
default:
console.log(`Unhandled event type ${event.type}`);
}
} catch (error) {
console.error("Error handling event", error);
return new Response("Webhook Error", { status: 400 });
}
return new Response("Webhook received", { status: 200 });
}
Steps to Create a Webhook Endpoint in Stripe
-
Log in to Your Stripe Dashboard:
-
Navigate to Webhooks Settings:
-
Once logged in, locate the “Developers” section in the dashboard. This section is typically accessible from the left sidebar menu. 3. Access Webhooks:
-
Within the “Developers” section, find and click on the “Webhooks” option. This action will take you to the webhooks settings page. 4. Add Endpoint:
-
On the webhooks settings page, locate the “Add endpoint” button and click on it to initiate the process of creating a new webhook endpoint. 4. Configure Webhook Endpoint:
-
In the “Add Endpoint” form, provide the following details:
-
Endpoint URL: Enter the URL of the server-side endpoint in your application that will receive webhook events from Stripe.
-
Events to Send: Select the types of events you want Stripe to send to this endpoint. You can choose from a list of predefined event types or select “All events” to receive notifications for all event types.
-
Version: Choose the version of the API you want to use for webhook events.
-
Signing Secret (Optional): Optionally, you can generate a signing secret that will be used to sign webhook events for added security. This is recommended to prevent unauthorized access to your webhook endpoint. 5. Save Endpoint:
-
After providing the necessary information, click on the “Add Endpoint” or “Save” button to create the webhook endpoint. 6. Click over the webhook, then click on reveal signing secret, past the signing secret in STRIPE_SECRET_WEBHOOK_KEY env variable.
In the webhook event handler, we listen for a specific event called checkout.session.completed. This event is sent by Stripe when the user completes the payment.
Our job is to update the database with the user and subscription. We also verify that the request came from Stripe using the headers.
Here is the Webhooks docs https://docs.stripe.com/webhooks (opens in a new tab)