Building a SaaS Billing Portal in Next.js with Lemon Squeezy
In this tutorial, you’ll learn how to build a comprehensive billing portal for a SaaS application using Next.js and Lemon Squeezy.
Billing portal essentials
Adding billing features to a SaaS application involves several key components. Here are the essential features you’ll need to build:
- UI for selecting plans and signing up
- a way to sync plan data from the billing provider
- a way to listen to webhooks to sync subscription data
- UI for changing plans
- UI for cancelling/resuming subscriptions
- UI for pausing/unpausing subscriptions
- UI for updating payment methods
In this tutorial we’ll group these into several topics:
- Introduction
- Signing up and selecting plans
- Syncing data
- Displaying subscription data
- Updating subscriptions
Before we jump into this code-heavy tutorial, keep in mind that Lemon Squeezy offers a drop-in no code Customer Portal, which does everything covered in this tutorial out of the box. Read more about the Customer Portal and then make sure to check out our tutorial for adding Customer Portal to your application.
Introduction
Code from this tutorial is a simplified version of a fully-functioning Lemon Squeezy Next.js billing app template. It is built as a skeleton project that you can fork and use to build your SaaS application on top of.
The app is built using the following stack and tools:
Create a billing page
In your /app
folder, create a new folder called dashboard
and then inside that folder create anoher one called billing
, finally create a new file called page.tsx
, which will be the main page for all billing-related UI.
Install lemonsqueezy.js
We will be using the official Lemon Squeezy JS SDK in this tutorial. It fully covers the Lemon Squeezy API.
You can install it with PNPM or your preferred package manager:
Set up the API
Now, we need a Lemon Squeezy API key to make requests to the Lemon Squeezy API. Go to app.lemonsqueezy.com/settings/api to create a new key, then save it somewhere in the app, for example in an .env
file:
For more information on how to create an API key, see the Developer Guide.
You will also need to save the ID of the store you’re selling from. Lemon Squeezy API keys are tied to users, not stores, and API requests will pull data from all stores you have access to if you have more than one. You can find your store ID at Settings » Stores.
After obtaining the store ID, your .env
file could look like this:
Signing up and selecting plans
This part of the billing portal lets users view your available plans and start a subscription.
With Lemon Squeezy, starting a subscription means sending users to a checkout URL. Once they pay, the subscription is started and they should then have access to your application.
Get the plans from Lemon Squeezy
Before moving on with the tutorial, make sure that you have already created your products and variants in Lemon Squeezy.
To get the plans from Lemon Squeezy, we will use the listProducts
method from the Lemon Squeezy SDK. We will also include the variants in the response.
Then, we need a Plan
model in the database to store the products and variations data. Here’s an example of what the model could look like (Drizzle ORM and PostgreSQL are used in this tutorial):
We will use the listProducts
function with include: ['variants']
to get the products and variants from Lemon Squeezy. We will then parse the data and store the plans in our database.
For the simplicity of this tutorial, the sync function is designed to execute on the initial page load if the plans have not been synchronized already, but can be easily adapted to run as a background job or an API endpoint that listens for webhook events.
Before we start sending API requests to Lemon Squeezy we need to configure the SDK with Lemon Squeezy API key. To do that we can create a new file called lemonsqueezy.ts
in the /src/config
folder.
Now, let’s use the helper function in our code.
The syncPlans
function gets Product data from Lemon Squeezy API filtered by storeID
, that we set earlier in the .env
file. We are also using the additional include
parameter to query related variants in the same API request.
Then, we loop through all the variants and pick the relevant data from the response and save it into the database using the helper function _addVariant
.
Now, you can go to http://localhost:3000/dashboard/billing
to sync your plan information to your database.
Show plans in the app
Now, we are ready to display the plans in our app. Before we start, we can export types for our Drizzle models by using $inferInsert
.
Now, let’s create a new component called Plan
to display the plan information. This component will be used in the Plans
component, which will display all the plans.
The final code adds more styling to the Plan
component, but for this tutorial we’ll keep it simple.
Now, let’s create the Plans
component to fetch the plans and display it in the app.
Then we can add this component to our billing page to show the plans. We also need to wrap the Plans
component in a Suspense
component to show a loading state while the plans are being fetched.
At this point you should see your plans on the dashboard/billing
page.
Add sessions
Before we get too far, we should start adding session information in our app so that logged in users can be linked to purchased subscriptions.
In this tutorial we are using Auth.js v5 and GitHub for login.
Following the Auth.js installation guide, we create a src/app/auth.ts
file to configure the authentication options with Drizzle Adapter and GitHub credentials.
And the authConfig
object is defined in a separate file, src/config/auth.ts
:
Auth.js will automatically configure GitHub login as long as you provide the AUTH_GITHUB_ID
and AUTH_GITHUB_SECRET
in your .env
file.
You can find the additional information on how to obtain GitHub credentials in the GitHub documentation.
The authorized
callback is used to verify if the request is authorized to access a page via Next.js Middleware. It is called before a request is completed, and it receives an object with the auth
and request
properties. The auth
property contains the user’s session, and the request
property contains the incoming request.
For that to work correctly we need to create a new file middleware.ts
in the /src
folder and add the following code:
View full auth code on Github.
Creating checkouts and letting customers subscribe
Now we are ready to let customers click on the “Sign up” buttons in our plan table and start paying for access to the application.
To do this we’ll create a new component called SignupButton
, which you can then drop into the Plan
component.
We also need to define the getCheckoutURL
function in the actions.ts
file. This function will create a new checkout and return the URL of the checkout page.
Now when you click on a “Sign up” button, a checkout will be created and loaded in a modal 🎉
Customers can now purchase your subscriptions, but how will your application know about this? Enter, webhooks.
Syncing subscription data
Action in your Lemon Squeezy store can trigger webhooks. Events like new subscriptions, updated subscriptions and new subscription payments can all be sent to your application so that your data is up to date with what’s happening in your store.
To set up webhooks, go to Settings » Webhooks in Lemon Squeezy. Create a webhook and select at minimum subscription_created
and subscription_updated
from the list. This will make sure you always have the latest information about every subscription in your app’s data.
In the URL field, you will need to add a publicly-accessible URL. During development you can use a service like ngrok or LocalCan. Whatever domain you use, add /billing/webhook
at the end, as this will be the path to our webhook ednpoint.
You will need to add a signing secret in the webhook form, too. This can be any random string. Make sure to copy this secret and add it into your environment variables, for example as LEMONSQUEEZY_WEBHOOK_SECRET
. Read about signing requests.
Create webhooks via the SDK
Alternatively, you can use the Lemon Squeezy SDK to create webhooks. The demo app provides a button on the Dashboard page that sends an API request to create a webhook.
Create a webhook endpoint
Now we need to create an API endpoint in our app to listen for webhook events, making sure to check that any incoming data was actually sent from Lemon Squeezy. When deployed, the endpoint will be available publicly to anyone on the internet so we use the signing secret from before to check against the X-Signature
header.
In this code we verify the signature and then save the event into the database. You don’t have to save these events, but it adds a fail-safe in case the event isn’t processed properly. It’s also important to return a 200
response quickly so that Lemon Squeezy knows the request was successful.
Drizzle model:
We also need a Subcription model to save subscription data in your application:
Here’s the basic endpoint code:
To process the webhook event data, add a processWebhookEvent
server action that will update the database. If there are any errors, the exception message will be saved into the database making it easier to figure out the issue and re-process the event.
Now when a customer goes through the checkout, their subscription data will be saved automatically into your database behind the scenes.
Displaying subscription data
The next step is to allow existing customers to view their current subscription details.
Let’s add UI into our billing page to show the current plan, the price the customer is paying as well as the next renewal date.
This is what we’ll end up with:
Here is the updated billing page:
Note that if a user has an active subscription, they will see the subscription details and the Plans will not be shown. If they don’t have any subscriptions, they will see the plans.
Updating subscriptions
Now that customers can sign up and view their subscriptions, we need to add typical billing features like changing plans, cancelling subscriptions and updating billing details.
Cancel, pause and update payment method
First, let’s add functions for handling:
- subscription cancellation
- subscription pausing
- subscription resuming
and also one for getting the update_payment_method
and customer_portal
URLs from the Lemon Squeezy API.
And now for the links in the UI, we will add them in a dropdown menu in the SubscriptionsActionsDropdown
component. We will include the following links:
- “Pause payments” link.
- “Unpause payments” link, shown only if the subscription is paused.
- “Customer portal” link which opens the Lemon Squeezy customer portal in a new tab. This is an example how you can use the Lemon Squeezy API to get the customer portal URL.
- “Update payment method” link which opens the Lemon Squeezy update payment method modal.
- “Cancel subscription” link.
Now we can update our Subscriptions
component to display these links, and show different messaging and options depending on the subscription’s status
value.
The code above uses several helper functions to format the subscription data and display the correct messaging and options. View the full code on Github.
Adding the ability to change plans
The final step is to add a way for customers to change their plan. This should only be available to active
and on_trial
subscriptions (you may have noticed the “Change plan” buttons in the code above).
Let’s create a new page at src/app/dashboard/billing/change-plans
. This page displays the similar plans picker that we built before.
and the ChangePlans
component:
Now let’s create a function to actually make the plan change when the change plan button is clicked.
Extending the portal
This tutorial covers the basics of a billing portal but there are a few extra features you could add.
For example, you could listen to subscription_payment_*
webhook events and save invoice data locally so you can display a handy billing history.
Or you could save card_brand
and card_last_four
into the app’s Subscription
model to display those details to your customers.
Read through our API documentation to familiarize yourself with what’s possible.
Read more
Now you have a fully-functioning billing portal built right into your Next.js application, ready to serve customers with Lemon Squeezy-powered subscriptions.
You can view the full Next.js Billing demo app code, including the snippets from this turtorial at github.com/lmsqueezy/nextjs-billing.
If you would like to read more about Lemon Squeezy’s billing capabilities, head over to our Help Docs.
Make sure to also read through our Developer Guide and Tutorials for more tips.