Saas Payment

In practice, the role-based access control system is often combined with a payment system, so that when the user pays for a subscription, their role will be automatically changed from free user to paid user, which allows then access to the premium content or services.

Here is how this system would work:

First of all, there should be a pricing page where users can select their desired payment plan, such as monthly subscription, annual subscription, onetime payment with lifetime access, and more.

When the user clicks on the Get This Plan button, they should be taken to the checkout page, where they can put in their credit card or other payment information. This page is usually hosted by a third-party payment platform, as we are going to discuss later.

The payment platform processes the payment, and sends an HTTP request back to your SaaS app. The request will be accepted and processed by an API endpoint, which will update the user information in the database accordingly, such as changing the user role from free user to paid user.

In this lesson, we are going to discuss how to create such a system using Stripe, the most popular online payment platform in the world.

Setting up Stripe

Go to stripe.com and create a new account. You will be asked to provide the name for your business and the country of operation. The name will be displayed to the user in the checkout page, as well as the invoice, and the country of operation may have tax implications.

You should be careful here, you can change the name in the future, but not the country of operation.

Create a new Stripe account

Click Create to continue.

It will take Stripe a few minutes to setup everything for you, and after that, you will be taken to the dashboard:

Stripe dashboard

By default, your business will be created inside a sandbox, which allows you to test everything before taking it online.

Next, direct your attention to the API keys section in the dashboard, and notice that there are two API keys, the publishable key and the secret key.

Stripe API keys

Save the secret key into your .env file. We are going to need it later.

.env

env
1. . .
2STRIPE_SECRET_KEY="<stripe_secret_key>"

Installing Stripe CLI and SDK

We also need to install the Stripe tools, Stripe CLI and SDK. Stripe CLI is a command line tool that sets up our local development environment to communicate with Stripe.

If you're using macOS or Linux, you can install the Stripe CLI with Homebrew, which is a package manager originally designed for macOS, but is now available on Linux as well.

On macOS, run the following command:

bash
1brew install stripe/stripe-cli/stripe

On Linux, run this command instead:

bash
1brew install stripe-cli

If you're working on Windows, you can either install it with Scoop, a package manager for Windows, by running the following commands:

bash
1scoop bucket add stripe https://github.com/stripe/scoop-stripe-cli.git
bash
1scoop install stripe

Or you can install manually by downloading the installer from GitHub.

Next, you need to connect the Stripe CLI to the Stripe account you just created. To do that, run the following command:

bash
1stripe login

You will be prompted with the following output:

text
1Your pairing code is: enjoy-enough-outwit-win
2This pairing code verifies your authentication with Stripe.
3Press Enter to open the browser or visit https://dashboard.stripe.com/stripecli/confirm_auth?t=THQdJfL3x12udFkNorJL8OF1iFlN8Az1 (^C to quit)

Stripe CLI login

Pay attention to the pairing code here, and then press Enter to open your browser. You will be take to this page:

Allow access

Make sure the right Stripe account is selected, and the paring code matches the one prompted in the terminal.

If everything looks right, click Allow access, and you will see the access granted page.

Access granted

Besides the Stripe CLI, you also need the stripe SDK package, which offers several APIs used to interact with Stripe from your app. The package can be installed with the following command:

bash
1npm install stripe --save

We'll demonstrate how to use the package later.

Extending the User table

In order to implement our payment system, we need to expand our User table again.

prisma
1model User {
2  id                 String     @id @default(cuid())
3  name               String?
4  email              String?    @unique
5  emailVerified      DateTime?
6  image              String?
7  role               String?    @default("free user")
8  accounts           Account[]
9  sessions           Session[]
10
11  stripeCustomerId   String?    @unique
12  subscriptionId     String?
13  subscriptionStatus String?
14
15  createdAt DateTime @default(now())
16  updatedAt DateTime @updatedAt
17}

We are adding three more columns, stripeCustomerId, subscriptionId, and subscriptionStatus.

Whenever a user registers an account on your SaaS platform, you should make sure a corresponding customer is created on Stripe. This Stripe customer will have a unique ID, which will be saved in this stripeCustomerId column.

When the user purchases a subscription, Stripe will also return an ID for this subscription. We need to save it inside our database so that we can make updates, such as cancelling and resuming the subscription.

The current status of the subscription will be saved in the subscriptionStatus entry.

Lastly, don't forget to apply the changes by running the following command:

bash
1npx prisma migrate dev

In the next a few lessons, we are going to cover how to perform these actions via Stripe, but first, let's start with creating a new Stripe customer as the user signs up.

Creating new Stripe customer

Go back to our auth.js and add an events object. As a reminder, this is where all the configurations for Auth.js goes.

libs/auth.js

javascript
1. . .
2
3// Import Stripe
4import Stripe from "stripe";
5
6const resend = new ResendClient(process.env.AUTH_RESEND_KEY);
7
8export const { handlers, signIn, signOut, auth } = NextAuth({
9  adapter: PrismaAdapter(prisma),
10  providers: [. . .],
11  pages: {. . .},
12  session: {. . .},
13  callbacks: {. . .},
14
15  events: {. . .},
16});

This events object allows us to define event hooks, which will be executed when certain actions are performed. For example, the createUser event hook will be activated when a new user is created.

This createUser will be passed a user parameter, containing the information about the user that was just created, which allows us to create a new Stripe customer using this information:

javascript
1createUser: async ({ user }) => {
2  // Create a new stripe customer
3  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
4
5  await stripe.customers
6    .create({
7      email: user.email,
8    })
9    .then(async (customer) => {
10      return prisma.user.update({
11        where: { id: user.id },
12        data: {
13          stripeCustomerId: customer.id,
14        },
15      });
16    })
17    .catch((error) => {
18      console.log(error);
19    });
20},

Line 3 initializes a new Stripe instance using the STRIPE_SECRET_KEY we just saved in our .env file:

javascript
1createUser: async ({ user }) => {
2  // Create a new stripe customer
3  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
4
5  await stripe.customers
6    .create({
7      email: user.email,
8    })
9    .then(async (customer) => {
10      return prisma.user.update({
11        where: { id: user.id },
12        data: {
13          stripeCustomerId: customer.id,
14        },
15      });
16    })
17    .catch((error) => {
18      console.log(error);
19    });
20},

Lines 5 to 8 creates a new Stripe customer using stripe.customers.create():

javascript
1createUser: async ({ user }) => {
2  // Create a new stripe customer
3  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
4
5  await stripe.customers
6    .create({
7      email: user.email,
8    })
9    .then(async (customer) => {
10      return prisma.user.update({
11        where: { id: user.id },
12        data: {
13          stripeCustomerId: customer.id,
14        },
15      });
16    })
17    .catch((error) => {
18      console.log(error);
19    });
20},

After the customer is successfully created, save the customer ID into our database:

javascript
1createUser: async ({ user }) => {
2  // Create a new stripe customer
3  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
4
5  await stripe.customers
6    .create({
7      email: user.email,
8    })
9    .then(async (customer) => {
10      return prisma.user.update({
11        where: { id: user.id },
12        data: {
13          stripeCustomerId: customer.id,
14        },
15      });
16    })
17    .catch((error) => {
18      console.log(error);
19    });
20},

If anything goes wrong in this process, we are logging the error to the console, but you can define other actions you wish to take, such as sending a notification to yourself using Resend.

As a side note, if you go to Stripe's official documentation, you will see the stripe package being imported and initialized like this:

javascript
1const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);

However, this is, in fact, an outdated syntax. It is generally recommended to use the import statement instead:

javascript
1import Stripe from "stripe";
2
3const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);