The complete guide to Stripe and Next.js

Learn everything you need to start collecting payments for your Next.js application with Stripe Checkout

21 min read
Cover Image for The complete guide to Stripe and Next.js

Stripe is a payment processor that allows you to accept credit card payments and manage subscriptions: it's one of the most loved services by developers because it makes it incredibly simple to implement a payment processor in your application simply and securely and start collecting payments from your users.

In this tutorial, we'll learn how to use Stripe and Next.js to collect payments for your SaaS. This tutorial will be the only resource you will ever need to add Stripe to your Next.js applications, similar to how our SaaS template for Next.js does.

Here are the topics we will cover:

  1. Creating Products and Prices from the Stripe Console
  2. Adding Stripe to a Next.js application
  3. Stripe Domain Model
  4. Creating a Stripe Checkout Session
  5. Writing Webhooks for handling asynchronous events from Stripe
  6. Managing the Stripe Checkout Portal after the user subscribed to a plan
  7. Writing a Permissions System based on the user's Subscription

This article assumes that:

  1. You have created a Next.js application
  2. You signed up for Stripe (it's free)
  3. You are comfortable using React and Typescript

How does Stripe Checkout works?

Stripe Checkout's strongest advantage is that it handles a lot of server-side and client-side logic for us, making it the ideal choice for handling user payments.

But how does it work? Let's see.

  1. Plan Selection: The flow starts from the client-side: the user picks a plan and gets redirected to the Stripe Portal, where they will be asked to enter a payment method
  2. Payment: Once in the Stripe Portal, the user can choose to finalize the payment or go back to the client.
  3. After Payment: When the transaction is executed, due to the asynchronous nature of payments, we have to wait and handle the results using webhooks, i.e. messages that Stripe sends to our servers asynchronously.
  4. Webhooks: Using webhooks, we will listen to Stripe's events and will need to handle them; most often, this involves updating our database. For example, when the transaction succeeds, we will save the subscription in our database; when users cancel their subscription, we will delete it, and so on.
  5. Real-Time client updates: (Optional) The client will listen for changes using a real-time database, for example, Firestore, and will display the data as it arrives.

The image above does not consider the updates made after the user subscribes to a plan using the Stripe Checkout Portal; for example, when the plan is cancelled or updated.

With that said, the flow does not change: we will receive webhooks to our API that we need to handle and update the database accordingly.

1) Creating products and prices from the Stripe Console

If you have already signed up for Stripe, you have access to the Stripe console from which you can add products and prices, which are fundamental for what we're going to do.

What's the difference between a Product and a Price?

  1. Stripe Products can be described as two different plans of your service, for example, a Basic Plan and a Pro Plan. They each have different features, and as, such we create two different products.
  2. Stripe Prices belong to products. A Price defines the amount, billing period, and whether the price is a one-time payment or a subscription.

When choosing how to build your Stripe Products and Prices, you have two think:

  1. How to separate your SaaS features as products
  2. How much money to charge and the details regarding billing. For example, your yearly plan will be cheaper than your monthly plan: that's two separate Prices for the same Product

Creating a Stripe Product with a Price

To start creating our Pricing, visit the Stripe Products Page.

Before continuing, make sure you activated "Test Mode" from the Stripe website:

Then, from the Console, let's create two Products: a Basic Plan and Pro Plan.

To do so, click on the "Add product" button, which will take you to the form to make a Product, as you can see below:

Below, you can also create the prices for your product:

As you can see from the images above:

  • we are creating a Product named "Basic Plan"
  • The Product contains one Price defined as a monthly subscription of $10 USD per month

Assuming we changed "Recurring" to "One time," it would not be a recurring monthly subscription but a single payment. However, for SaaS, this is usually not what you want. Therefore, we stick with "Recurring".

Save and, optionally, repeat the process for creating a Pro Plan.

2) Adding Stripe to a Next.js application

Now that we created our Products and Prices from the Stripe Console, we have finished this article's business part.

It's now time to install Stripe in our application that we assumed you have already created with Next.js.

Install the Stripe packages from NPM

To install the required Stripe packages, run the following command:

npm i --save stripe npm i -D stripe-cli

Add the Stripe keys as Environment Variables

We now have to go back to your Stripe Dashboard to retrieve some keys we have to add to our configuration so that our application can securely communicate with the Stripe servers.

  1. So, the first step is to go back to the Stripe Developers Page, where we find the Stripe keys for our Organization
  2. The second step is to copy the keys and add the following environment variables to your .env file.

Adding Environment Variables to your Next.js application

If you visited the Stripe Developer page linked above, you should see something similar to the below:

To add the Stripe keys as Environment Variables to your Next.js application, we have to create a .env.local file and add the following environment variables based on the keys you see on your Developer Page (see image above):


Of course, you have to replace *** with your actual keys. To make the changes live, remember to restart your Next.js server.

NB: do not push this file into git; it's only local to your environment.

Add Stripe Products to your Local Configuration

To create a Plan Selector in our UI, we have to add some details from the Stripe Dashboard to our project, such as the Stripe Price IDs the user can choose.

Before continuing, go back to your Stripe Products Page to retrieve the Stripe Price IDs to use in our configuration.

Click on a product, and you will find each price from which you can copy the relative IDs:

In Makerkit, we create a global configuration.ts file in which we store all the application's configuration and shared properties.

In this configuration file, we also add the plans we made in Stripe. First, let's add a data structure similar to the below, and paste the correct Price IDs you copied in the previous step in the stripePriceId properties:

{ plans: [ { name: 'Basic', description: 'Basic Plan yo!', price: '$10/month', stripePriceId: 'price_123', }, { name: 'Pro', description: 'Pro Plan yo!', price: '$49/month', stripePriceId: 'price_456', }, ], }

Run the Stripe CLI for executing the Stripe Webhooks locally

To redirect the Stripe webhooks toward our local environment, we have to use the Stripe CLI package we have installed.

Let's add the following script to our package.json to start the Stripe CLI server:

{ "scripts": { "stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook" } }

Now it's possible to run the server with the following command:

npm run stripe:listen

When this command starts running, it will expose a Webhook Secret Key that you will need to verify the messages coming from Stripe.

We will need to add this secret key as another Environment Variable to the .env.local file:


NB: this command needs to be running when using the Stripe webhooks during development.

3) Stripe Domain Model

In this section, we will describe the entities of a SaaS and the domain model we will use for this article.

Who is subscribing: User or Organization?

The entities are highly dependent on your domain and application, but in a SaaS, you would typically use an individual user or a group of users (such as a team, project, or organization).

Because it's more common for a SaaS to use a group of users, we will assign a subscription to an entity named Organization.

In the Makerkit SaaS starter, we use the following interface for an organization:

interface Organization { name: string; timezone?: string; logoURL?: string | null; subscription?: OrganizationSubscription; customerId?: string; }

The customerId property is an ID assigned by Stripe when adding a new customer: why is it optional? It is because when the organization is created, the organization is not subscribed to any plan. So, it's totally possible not to make the customerId property optional, but you will have to add the organization as a customer when it's created.

Additionally, we have a subscription field that represents the organization's Stripe subscription, with the following shape OrganizationSubscription:

enum OrganizationPlanStatus { AwaitingPayment = 'awaitingPayment', Paid = 'paid', } interface OrganizationSubscription { id: string; priceId: string; status: OrganizationPlanStatus; currency: string | null; interval: string | null; intervalCount: number | null; createdAt: UnixTimestamp; periodStartsAt: UnixTimestamp; periodEndsAt: UnixTimestamp; trialStartsAt: UnixTimestamp | null; trialEndsAt: UnixTimestamp | null; }

The OrganizationSubscription interface represents an organization subscription object and contains the bare minimum we need to know about it.

I am aware that the data above looks very minimal, but it's by design. In fact, should you need to access further information about your clients, you can use the Stripe API, which means storing and managing fewer data in your database.

In terms of data structure lifecycle, we have 3 scenarios.

1) The organization never subscribed to a plan

When the organization never subscribed to a plan, the properties customerId and subscription are not defined.

2) The organization subscribed to a plan

When the organization subscribes to a plan, the organization is identified in Stripe via a customerId property and a subscription object is added to the organization.

3) The organization unsubscribes from a plan

When the organization unsubscribes from a plan, the subscription property is set to undefined.

As we have already identified the organization in Stripe with the ID customerId, this is going to remain attached to the organization.

Furthermore, we will use the customerId property to access the Stripe Portal, from which the organization will be able to visualize and download the invoices issued.

Organizations can only access the Stripe Portal after subscribing

Accessing the Stripe Portal is only possible after the organization creates a subscription for the first time because it needs a customerId assigned, and this is created when the organization creates a subscription.

Alternatively, you can choose to create a Stripe Customer as soon as the organization is created, so to always have a customerId assigned to an organization.

4) Creating a Stripe Checkout Session

The configuration part of this tutorial is finished. Now comes the programming part, where we will build the UI and API to connect our application to Stripe.

Our UI will be extremely basic, but here's what we're going to build:

  1. a PlanSelectionForm component that lists the available plans, and allows the user to select a plan with a radio input
  2. a checkout button which will redirect the users to the API handler in charge of creating the Stripe Checkout session and redirecting the user's to Stripe, where the payment will take place

Creating a Component to select a Stripe plan

First, we need to create a Component to choose a Stripe Product: we call this a Plan Selector Component.

We will populate this component using the plans we have added to the configuration file.

const PlanSelectionForm: React.FC<{ onChange: (stripePriceId: string) => void; }> = () => { const plans = configuration.plans; return ( <div className='flex flex-col space-y-2'> { => { return ( <input type='radio' value={plan.stripePriceId} name='plan' onChange={e => onChange(} /> ); }) } </> ); };

Stripe Checkout Button

The checkout button is responsible for creating a form that will call the API handler with the correct selection.

The above form needs to include four properties:

  • The selected organization's ID
  • The URL to redirect the user back
  • The ID of the Price selected
  • The organization's customerID property, if this exists
// this is the path to the API endpoint for creating // a checkout session. For example /api/stripe/checkout const CHECKOUT_SESSION_API_ENDPOINT = configuration.paths.api.checkout; const CheckoutRedirectButton: React.FCC<{ disabled?: boolean; priceId: string; organizationId: string; customerId: string | undefined; }> = ({ children, ...props }) => { return ( <form action={CHECKOUT_SESSION_API_ENDPOINT} method="POST" > <CheckoutFormData customerId={props.customerId} organizationId={props.organizationId} priceId={props.priceId} /> <button type="submit" disabled={props.disabled}> {children} </button> </form> ); };

When the user clicks the button, the form will make a POST request to the API endpoint defined at configuration.paths.api.checkout, for example /api/stripe/checkout.

The POST request will send along as body the data we defined within CheckoutFormData as hidden inputs, such as the below:

function CheckoutFormData( props: React.PropsWithChildren<{ organizationId: string; priceId: string; customerId: string | undefined; }> ) { return ( <> <input type="hidden" name={'organizationId'} defaultValue={props.organizationId} /> <input type="hidden" name={'returnUrl'} defaultValue={getReturnUrl()} /> <input type="hidden" name={'priceId'} defaultValue={props.priceId} /> <input type="hidden" name={'customerId'} defaultValue={props.customerId} /> </> ); } function getReturnUrl() { return isBrowser() ? [ window.location.origin, window.location.pathname ].join('') : undefined; }

Our API endpoint will need to handle the POST request at /api/stripe/checkout with the body defined as:

{ customerId: string | undefined; priceId: string; organizationId: string; returnUrl: string; }

Form Container

Now, it's time to put everything together!

We add the PlanSelectionForm component to a container component PlanSelectionContainer, responsible for displaying either the Checkout Form or the button to redirect to the Billing page in case they're already subscribed.

The below is a very simplified version but gives you an idea of what the subscription page should look like:

const PlanSelectionContainer = () => { const [selectedPrice, setSelectedPrice] = useState(); // inject your selected organization const organization = useCurrentOrganization(); const subscription = organization.subscription; const customerId = organization.customerId; // when the user is already subscribed // show the "Manage Subscription" button // which redirects users to the Stripe Billing Portal if (subscription) { return <BillingPortalRedirectButton customerId={customerId} />; } // otherwise, show the form to pick a plan // and the button to redirect them to the Stripe Checkout return ( <> <PlanSelectionForm onChange={setSelectedPrice} /> <CheckoutRedirectButton priceId={stripePriceId} organizationId={} customerId={customerId} /> { customerId ? <BillingPortalRedirectButton customerId=customerId /> : null } </> ); };

Borrowing an example from the Makerkit's Next.js SaaS Starter, the above will look like the below:

Loading video...

Redirecting to the Stripe Checkout Portal

When the user submits the form defined within CheckoutRedirectButton, as we've said, we make a POST request to the endpoint we are going to define in the next paragraph.

Creating a function to create a Checkout Session

In the snippet below, we will

  • create a Stripe instance using the secret keys we defined in the beginning
  • build a LineItem with the selected price ID
  • define the success and cancel return URLs, which we use to redirect the users in case the user completes the checkout or cancels the session: The UI needs to handle both these situations.
interface CreateCheckoutParams { returnUrl: string; organizationId: string; priceId: string; customerId?: string; } function getStripeInstance() { const Stripe = await import('stripe'); const key = process.env.STRIPE_SECRET_KEY; return new Stripe(key, { apiVersion: `2020-08-27`, // update this! }); } function createStripeCheckout(params: CreateCheckoutParams) { const clientReferenceId = params.organizationId; const customer = params.customerId || undefined; const mode: Stripe.Checkout.SessionCreateParams.Mode = 'subscription'; const stripe = await getStripeInstance(); const lineItem: Stripe.Checkout.SessionCreateParams.LineItem = { quantity: 1, price: params.priceId, }; // NB: trimmed for simplicity but use // smarter methods for appending the query parameters const successUrl = `${returnUrl}?success=true`; const cancelUrl = `${returnUrl}?cancel=true`; return stripe.checkout.sessions.create({ mode, customer, line_items: [lineItem], success_url: successUrl, cancel_url: cancelUrl, client_reference_id: clientReferenceId, }); }

Now, we will create the API endpoint which will use the createCheckoutSession function defined above.

To define an API endpoint, we need to create the following folder structure in our project: /pages/api/stripe/session/checkout.ts.

export default async function checkoutsSessionHandler( req: NextApiRequest, res: NextApiResponse ) { const { headers, firebaseUser } = req; const { organizationId, priceId, customerId, returnUrl } =req.body; // NB: here you may want to check that: // - the user can update billing // - the data sent is correct // - the user belongs to the organization in the body // we omit it for simplicity, but food for thought! try { const { url } = await createStripeCheckout({ returnUrl, organizationId, priceId, customerId, }); // redirect user back based on the response res.redirect(301, url); } catch (e) { console.error(e, `Stripe Checkout error`); // either end request or ideally redirect users to the same URL // but using a query parameter such as error=true return res.status(500).end(); } }

If everything went well, the user went through the checkout process and returned to the page. At this point, Stripe will be sending us some webhooks to communicate the state of the transaction.

Remember, the checkout process was complete, but it doesn't always mean the payment was successful. In many cases, payments are asynchronous, which means we need to wait for updates from Stripe to know if the payment was successful or unsuccessful.

5) Writing Webhooks for handling asynchronous events from Stripe

Creating an API Handler for receiving Webhooks

To receive webhooks from Stripe, we need to create an API handler that Stripe can communicate with. To do so, let's create the following file /pages/api/stripe/webhook.ts.

Before proceeding, let's make sure you did the following steps:

  • run the command npm run stripe:listen
  • copy the STRIPE_WEBHOOKS_SECRET key that the CLI displayed, and add it to the .env.local file
  • Finally, don't forget to restart the Next.js development server

Verifying Stripe Webhooks

After creating the webhook.ts file, the first thing to do is being able to verify the message transmitted by Stripe.

To do so, we have to retrieve the raw body from the message so we can compare it with the signature sent by Stripe. This is essential because we need to ensure we're communicating with Stripe. Using the shared webhooks secret key, we will verify that we received the webhook from Stripe.

To get the raw body from the request, we use the package raw-body, so we install it with the following command:

npm i raw-body --save

First, we need to instruct the Next.js handler not to parse the body of the POST request. We can do so by exporting a variable named config:

export const config = { api: { bodyParser: false, }, };

To verify a Stripe webhook, we can use the Stripe function stripe.webhooks.constructEvent, which will throw an error if the signature could not be verified:

import type { Stripe } from 'stripe'; import getRawBody from 'raw-body'; const STRIPE_SIGNATURE_HEADER = 'stripe-signature'; // NB: we disable body parser to receive the raw body string. The raw body // is fundamental to verify that the request is genuine export const config = { api: { bodyParser: false, }, }; export default async function checkoutsWebhooksHandler( req: NextApiRequest, res: NextApiResponse ) { const signature = req.headers[STRIPE_SIGNATURE_HEADER]; const rawBody = await getRawBody(req); const stripe = await getStripeInstance(); const event = stripe.webhooks.constructEvent( rawBody, signature, webhookSecretKey ); // NB: if stripe.webhooks.constructEvent fails, it would throw an error // here we handle each event based on {event.type} try { switch (event.type) { case StripeWebhooks.Completed: { const session = as Stripe.Checkout.Session; const subscriptionId = session.subscription as string; const subscription = await stripe.subscriptions.retrieve( subscriptionId ); await onCheckoutCompleted(session, subscription); break; } case StripeWebhooks.AsyncPaymentSuccess: { const session = as Stripe.Checkout.Session; const organizationId = session.client_reference_id as string; await activatePendingSubscription(organizationId); break; } case StripeWebhooks.SubscriptionDeleted: { const subscription = as Stripe.Subscription; await deleteOrganizationSubscription(; break; } case StripeWebhooks.SubscriptionUpdated: { const subscription = as Stripe.Subscription; await onSubscriptionUpdated(subscription); break; } case StripeWebhooks.PaymentFailed: { const session = as Stripe.Checkout.Session; // TODO: handle this properly onPaymentFailed(session); break; } } return respondOk(res); } catch (e) { return internalServerErrorException(res); } }

Which Stripe events do we need to handle?

If everything went well, the event object will contain the event sent from Stripe. At this point, we need to use a different strategy based on the event type being sent.

Generally speaking, we will need to handle at least the following five events:

  • payment completed
  • asynchronous payment success
  • subscription deleted
  • subscription updated
  • payment failed

Therefore, we create an enum containing the webhook types sent by Stripe:

export enum StripeWebhooks { AsyncPaymentSuccess = 'checkout.session.async_payment_succeeded', Completed = 'checkout.session.completed', PaymentFailed = 'checkout.session.async_payment_failed', SubscriptionDeleted = 'customer.subscription.deleted', SubscriptionUpdated = 'customer.subscription.updated', }

Creating and storing Subscriptions for your SaaS in a Firestore database

Considering the case when the user subscribes to a plan, we have two scenarios:

  1. the payment succeeds and Stripe returns a webhook with a confirmation
  2. the payment is being processed and needs to wait for a confirmation

In any of the cases above, we create the subscription object and store it:

case StripeWebhooks.Completed: { const session = as Stripe.Checkout.Session; const subscriptionId = session.subscription as string; const subscription = await stripe.subscriptions.retrieve( subscriptionId ); await onCheckoutCompleted(session, subscription); break; }

As you can see, we retrieve the subscription object and pass it to the onCheckoutCompleted handler, defined below:

async function onCheckoutCompleted( session: Stripe.Checkout.Session, subscription: Stripe.Subscription ) { const organizationId = session.client_reference_id as string; const customerId = session.customer as string; // status can either be PAID or AWAITING_PAYMENT (if asynchronous) const status = getOrderStatus(session.payment_status); const subscriptionData = buildOrganizationSubscription(subscription, status); // use your DB methods to // set organization.subscription=subscriptionData return setOrganizationSubscription({ organizationId, customerId, subscription: subscriptionData, }); }

The buildOrganizationSubscription function is a simple factory that adapts the Stripe object to the object in our database. You can adapt it however you like, as long as it respects your database schema or interface.

If you're interested in knowing how we map it, below is the factory function:

function buildOrganizationSubscription( subscription: Stripe.Subscription, status: OrganizationPlanStatus = OrganizationPlanStatus.Paid ): OrganizationSubscription { const lineItem =[0]; const price = lineItem.price; return { id:, priceId: price?.id, status, currency: lineItem.price.currency ?? null, interval: price?.recurring?.interval ?? null, intervalCount: price?.recurring?.interval_count ?? null, createdAt: subscription.created, periodStartsAt: subscription.current_period_start, periodEndsAt: subscription.current_period_end, trialStartsAt: subscription.trial_start ?? null, trialEndsAt: subscription.trial_end ?? null, }; }

Handling payments being processed in Stripe

Considering the case where a payment is being processed, we wait for confirmation from Stripe. Then, Stripe will send us either AsyncPaymentSuccess or PaymentFailed.

1) When the payment is successful

In this case, we simply need to flip the status property of the subscription object from OrganizationPlanStatus.AwaitingPayment to OrganizationPlanStatus.Paid:

case StripeWebhooks.AsyncPaymentSuccess: { const session = as Stripe.Checkout.Session; const organizationId = session.client_reference_id as string; await activatePendingSubscription(organizationId); break; }

2) When the payment fails

In this case, you may want to email your customers:

case StripeWebhooks.PaymentFailed: { const session = as Stripe.Checkout.Session; onPaymentFailed(session); break; }

What to do when the payment is still in progress?

Should you let your customers use your application while the payment is being processed? This is your decision based on what is best for your application, but if you decide not to, use the status property to understand if they're free to use your service or not.

The status property needs to be equal to OrganizationPlanStatus.Paid (or whatever value you choose to use).

6) Managing the Stripe Portal after the user subscribed to a plan

Once users subscribe to a plan, they have two choices: update their subscription or unsubscribe.

Creating a Button to access the Stripe Billing Portal

When a customer is assigned with a customerId in Stripe, they can access their Billing Portal hosted by Stripe, and therefore we need to let them visit it from our application's UI.

If you remember what we wrote above, we created a component named BillingPortalRedirectButton: this button is there to let your users access the Stripe Billing Portal.

Similar to the CheckoutRedirectButton, this button will send a POST request to the API, which will create a Stripe Billing Portal session and redirect the user to Stripe, where the user will be able to edit or cancel their subscription, or download invoices.

const BILLING_PORTAL_REDIRECT_ENDPOINT = configuration.paths.api.billingPortal; const BillingPortalRedirectButton: React.FCC<{ customerId: string; className?: string; }> = ({ children, customerId, className }) => { return ( <form method="POST" action={BILLING_PORTAL_REDIRECT_ENDPOINT}> <input type={'hidden'} name={'customerId'} value={customerId} /> <Button color={'secondary'} className={className}> {children} </Button> </form> ); };

Now, we have to write the API handler that takes care of creating the Stripe session and redirecting the user to Stripe.

To do so, let's create an API function at /pages/api/stripe/portal.ts, and add the following content:

export default function billingPortalRedirectHandler( req: NextApiRequest, res: NextApiResponse ) { const { firebaseUser, headers } = req; const referrerPath = getApiRefererPath(headers); const { customerId } = req.body; // NB: you may want to check that // the user is authorized to access the portal if (!canAccessBillingPortal()) { return redirectToErrorPage(); } try { const returnUrl = req.headers.referer || req.headers.origin || configuration.paths.appHome; const stripe = await getStripeInstance(); const { url } = await stripe.billingPortal.sessions.create({ customer: customerId, return_url: returnUrl, }); res.redirect(301, url); } catch (e) { console.error(e, `Stripe Billing Portal redirect error`); // Here, consider redirecting the user to an error page return res.end(500); } }

Check out the demo below as per the Makerkit SaaS starter:

Loading video...

7) Writing a Permissions System based on the user's Subscription

Now that organizations can subscribe to a plan and pay for your services, you should also make sure that organizations that aren't subscribed cannot access certain pages or perform certain actions.

To do so, you should be able to pull your organization and make sure to check its subscription object.

For example, you can write logic that checks that to invite more members to an organization, the user must have a valid/paid subscription:

function useOrganizationSubscription() { const organization = useCurrentOrganization(); return organization.subscription; } function useIsSubscriptionValid() { const subscription = useOrganizationSubscription(); const isPaid = subscription?.status === OrganizationPlanStatus.Paid; const isPeriodValid = subscription?.periodEndsAt > new Date().getTime(); return isPaid && isPeriodValid; } function useOrganizationCanInviteMembers() { return useIsSubscriptionValid(); }


And that's it! If you followed this guide, you should have a good idea of what it takes to integrate Stripe with your Next.js application and start collecting payments for your SaaS.

The code above is a minimal stripped-down version of the Makerkit's SaaS Boilerplate for Next.js and Firebase, which uses Stripe to handle payments.

If you're interested in having all the code above already written for you, take a look! Otherwise, I hope this guide will help you implement Stripe and get your SaaS up and running. Ciao!

Read more about Tutorials

Cover Image for Building an AI Writer SaaS with Next.js and Supabase

Building an AI Writer SaaS with Next.js and Supabase

57 min read
Learn how to build an AI Writer SaaS with Next.js and Supabase - from writing SEO optimized blog posts to managing subscriptions and billing.
Cover Image for Announcing the Data Loader SDK for Supabase

Announcing the Data Loader SDK for Supabase

8 min read
We're excited to announce the Data Loader SDK for Supabase. It's a declarative, type-safe set of utilities to load data into your Supabase database that you can use in your Next.js or Remix apps.
Cover Image for Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

20 min read
In this tutorial, we will learn how to use add AI capabilities to your SaaS using Supabase Vector, HuggingFace models and Next.js Server Components.
Cover Image for Building an AI-powered Blog with Next.js and WordPress

Building an AI-powered Blog with Next.js and WordPress

17 min read
Learn how to build a blog with Next.js 13 and WordPress and how to leverage AI to generate content.
Cover Image for Using Supabase Vault to store secrets

Using Supabase Vault to store secrets

6 min read
Supabase Vault is a Postgres extension that allows you to store secrets in your database. This is a great way to store API keys, tokens, and other sensitive information. In this tutorial, we'll use Supabase Vault to store our API keys
Cover Image for Introduction to Next.js Server Actions

Introduction to Next.js Server Actions

9 min read
Next.js Server Actions are a new feature introduced in Next.js 13 that allows you to run server code without having to create an API endpoint. In this article, we'll learn how to use them.