How to integrate subscriptions into your Next.js project (/app). The tutorial utilizes Next.js (v13.4) with the /app directory structure, NextAuth v4 for authentication, Prisma for database management, and Stripe for payment processing.
This is a Next.js project bootstrapped with create-next-app
.
Run the following command to create a new Next.js app:
npx create-next-app@latest
Setup Prisma (documentation):
npm install prisma --save-dev
npx prisma init
schema.prisma
.npx prisma db push
npm install @prisma/client
npx prisma generate
lib/prisma.ts
file. In this case, I placed it within the same Prisma folder, prisma/prisma.ts
. (documentation)Setup NextAuth (documentation):
npm install next-auth @auth/prisma-adapter
/api/auth/[...nextauth]/route.ts
API route:
NextAuth()
before handler
in a variable that is exported, so we can use it later. It should look like this:
export const authOptions: NextAuthOptions = {} // here go all the options.
prisma
from prisma.ts
and add the PrismaAdapter
(Prisma Adapter)GoogleProvider
) (Google Provider):
http://localhost:YOUR_PORT
& http://localhost
(for deployed app, add the appropriate URL)http://localhost:YOUR_PORT/api/auth/callback/google
(for deployed app, add the URL with the same route: /api/auth/callback/google
)main
in a Provider component that contains SessionProvider
(this component must be tagged with 'use client').useSession
, signIn
, signOut
from "next-auth/react" (this component must be tagged with 'use client').Setup Stripe
Create a Stripe account
.env.example
to see how your .env
should end upInstall Stripe and stripe-js
npm install stripe --save
npm install @stripe/stripe-js
Modify schema.prisma
User
model:
stripeCustomerId String? @unique
isActive Boolean @default(false)
npx prisma db push
npx prisma generate
Go to /api/auth/[...nextauth]/route.ts
providers
, add secret: process.env.NEXTAUTH_SECRET,
callbacks:{}
, set up a callback function to add necessary values to the session object. This ensures that when a session is checked (useSession
, getSession
, getServerSession
), the required values are available. The callback should look like this:
callbacks: {
async session({ session, user }) {
session!.user!.id = user.id;
session!.user!.stripeCustomerId = user.stripeCustomerId;
session!.user!.isActive = user.isActive;
return session;
},
},
types.d.ts
file at the same level as the src/
folder with the following content:
import { DefaultUser } from 'next-auth';
declare module 'next-auth' {
interface Session {
user?: DefaultUser & { id: string; stripeCustomerId: string; isActive: boolean };
}
interface User extends DefaultUser {
stripeCustomerId: string;
isActive: boolean;
}
}
events:{}
to automatically create an account in the Stripe dashboard when a user logs in for the first time. Later, the stripeCustomerId
will be added to that user's account in our database. It should look like this:
events: {
createUser: async ({ user }) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2022-11-15",
});
await stripe.customers.create({
email: user.email!,
name: user.name!,
})
.then(async (customer) => {
return prisma.user.update({
where: { id: user.id },
data: {
stripeCustomerId: customer.id,
},
});
});
},
},
If you have data in your database, delete it (you can use npx prisma studio
to view and modify your database).
At this point, you should be able to log in and see how a user is created in your database with the added values. In your Stripe dashboard, you should also see that a new customer was created.
Now we will work on the checkout:
/api/stripe/checkout-session/route.ts
API route:
POST
function and import NextRequest
and NextResponse
. Declare a body
variable and initialize Stripe and getServerSession
. It should look like this:
import { NextRequest, NextResponse } from "next/server";
import { authOptions } from "../../auth/[...nextauth]/route";
import { getServerSession } from "next-auth";
import Stripe from "stripe";
export async function POST(req: NextRequest) {
const body = await req.json();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2022-11-15",
});
const session = await getServerSession(authOptions);
}
if (!session?.user) {
return NextResponse.json(
{
error: {
code: "no-access",
message: "You are not signed in.",
},
},
{ status: 401 }
);
}
const checkoutSession = await stripe.checkout.sessions.create({
mode: "subscription",
customer: session.user.stripeCustomerId,
line_items: [
{
price: body,
quantity: 1,
},
],
success_url: process.env.NEXT_PUBLIC_WEBSITE_URL + `/success`,
cancel_url: process.env.NEXT_PUBLIC_WEBSITE_URL + `/error`,
subscription_data: {
metadata: {
payingUserId: session.user.id,
},
},
});
if (!checkoutSession.url) {
return NextResponse.json(
{
error: {
code: "stripe-error",
message: "Could not create checkout session",
},
},
{ status: 500 }
);
}
return NextResponse.json({ session: checkoutSession }, { status: 200 });
<button
className='bg-slate-100 hover:bg-slate-200 text-black px-6 py-2 rounded-md capitalize font-bold mt-1'
onClick={() => handleCreateCheckoutSession(plan)}
>
Go To Checkout
</button>
handleCreateCheckoutSession(productId)
async function that receives productId
. I'll just declare a variable plan
that contains a product ID.
const res = await fetch(`/api/stripe/checkout-session`, {
method: "POST",
body: JSON.stringify(productId),
headers: {
"Content-Type": "application/json",
},
});
const checkoutSession = await res.json().then((value) => {
return value.session;
});
/app/utils/getStripe.ts
file. Here, we'll declare a reusable stripePromise
. If stripePromise
already exists, we won't create a new one but use the already created instance (read more) . The code should look like this:
import { Stripe, loadStripe } from '@stripe/stripe-js';
let stripePromise: Promise<Stripe | null>;
const getStripe = () => {
if (!stripePromise) {
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
}
return stripePromise;
};
export default getStripe;
handleCreateCheckoutSession()
, we'll continue by declaring a stripe
variable and await getStripe()
. Then, we'll use it for the redirect. Here's an example:
const stripe = await getStripe();
const { error } = await stripe!.redirectToCheckout({
sessionId: checkoutSession.id,
});
// If `redirectToCheckout` fails due to a browser or network
// error, display the localized error message to your customer
// using `error.message`.
console.warn(error.message);
isActive
value of the User
in the database if the user has paid or the subscription has been canceled.Let's start with the Stripe webhook.
/api/webhooks/route.ts
.
Initialize Stripe, the webhookSecret
variable, and the webhookHandler
async arrow function, and export it as POST. It should look like this:
import Stripe from "stripe";
import prisma from "../../../../prisma/prisma";
import { NextRequest, NextResponse } from "next/server";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2022-11-15",
});
const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET!;
const webhookHandler = async (req: NextRequest) => {
// We are going to add things here
};
export { webhookHandler as POST };
Inside a try{}catch{}
block, initialize variables to save the request in text form, the "stripe-signature" header, and a let
variable that is initially undefined. This last variable is used to save the Stripe webhook event that will be created. The code should look like this:
try {
const buf = await req.text();
const sig = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
// Rest of the code goes here
// Return a response to acknowledge receipt of the event.
return NextResponse.json({ received: true });
} catch {
// If an error occurs
return NextResponse.json(
{
error: {
message: `Method Not Allowed`,
},
},
{ status: 405 }
).headers.set("Allow", "POST");
}
Inside a try{}catch{}
block, we will attempt to build the event (read more). If an error occurs, we catch it and handle it accordingly. The code should look like this:
try {
event = stripe.webhooks.constructEvent(buf, sig, webhookSecret);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Unknown error";
// On error, log and return the error message.
if (!(err instanceof Error)) console.log(err);
console.log(` Error message: ${errorMessage}`);
return NextResponse.json(
{
error: {
message: `Webhook Error: ${errorMessage}`,
},
},
{ status: 400 }
);
}
After that, we add a switch
statement to handle the event types sent by the Stripe webhook to this endpoint. We will handle "customer.subscription.created" and "customer.subscription.deleted" event types. In each case, we will look for the user in our database that has the same stripeCustomerId as the event and update the user's isActive value. The code should look like this:
// Getting the data we want from the event
const subscription = event.data.object as Stripe.Subscription;
switch (event.type) {
case "customer.subscription.created":
await prisma.user.update({
// Find the customer in our database with the Stripe customer ID linked to this purchase
where: {
stripeCustomerId: subscription.customer as string,
},
// Update that customer so their status is now active
data: {
isActive: true,
},
});
break;
case "customer.subscription.deleted":
await prisma.user.update({
// Find the customer in our database with the Stripe customer ID linked to this purchase
where: {
stripeCustomerId: subscription.customer as string,
},
// Update that customer so their status is now active
data: {
isActive: false,
},
});
break;
default:
console.warn(` Unhandled event type: ${event.type}`);
break;
}
To test this API endpoint locally, you'll have to install the Stripe CLI on your machine. Stripe CLI
http://localhost:3000/api/webhooks
. Copy the webhook secret key that it gives you and add it to your .env
file.Now you should be able to go through the login/logout, checkout, and, if successful, change the isActive
value to true and if the subscription gets canceled, change it to false.
Deploying it
Checkout the .env.example
file in this repository. It should show you all the necessary environment variables needed for this project. If you are deploying to Vercel, you won't need "NEXTAUTH_URL". You can quickly generate the NEXTAUTH_SECRET
by running openssl rand -base64 32
in your terminal.
Connect to your GitHub repo and deploying the project.
Now, for the webhook to work, you need to go to your Stripe Developer Dashboard > Webhooks and add an endpoint. The URL will be https://YOUR_DEPLOYMENT_URL/api/webhooks
, and it should listen for customer.subscription.created
and customer.subscription.deleted
events.
STRIPE_WEBHOOK_SECRET
.Make sure you have added the deployment URL to your Google OAuth Client.
After all that, everything should work! .
In the last stage, I encountered a problem with the webhook. The Stripe webhooks dashboard displayed the following error:
Webhook Error: Error: No signatures found matching the expected signature for the payload. Are you passing the raw request body you received from Stripe?
Learn more about webhook signing and explore webhook integration examples for various frameworks at https://github.com/stripe/stripe-node#webhook-signing
The project in this repository closely follows the tutorial in terms of the concepts explained, but it includes some modifications to its user interface. The project has been structured to include login, subscription, and dashboard pages. Additionally, the useSession hook is utilized to verify if the user has permission to access a specific page.
Feel free to give the project a emoji and contact me through any of the social media platforms shown in my profile.
First, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
Open http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying app/page.tsx
. The page auto-updates as you edit the file.
This project uses next/font
to automatically optimize and load Inter, a custom Google Font.
To learn more about Next.js, take a look at the following resources:
You can check out the Next.js GitHub repository - your feedback and contributions are welcome!
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more details.