How to Build a High-Converting SaaS Waitlist with Next.js and BCMS

How to Build a High-Converting SaaS Waitlist
By Winnie
Read time 6 min
Posted on 7 Mar 2025

Customers are central to your SaaS. While your service is still under development, you can keep potentially interested customers engaged and excited about its launch through a waitlist.

What is a SaaS waitlist?

A SaaS waitlist is a lightweight customer relationship management, lead generation, and marketing tool that collects customer contact information in the pre-launch phase of your service. It outlines key updates and news about your service leading up to its launch, details upcoming features, links to social media for external engagement, and can even spur fundraising by demonstrating demand for your product.

Why a SaaS waitlist is essential for your pre-launch strategy

These are some ways a waitlist can be useful for your SaaS:

  • Waitlists keep your customers interested through product updates as they anticipate its launch.

  • With a waitlist, you get a pool of users willing to test your service and give you actionable feedback.

  • Through early adopters, you can gauge your product’s reception, see what features users like, and assess pricing strategies to which they may be amenable to.

  • A waitlist is a community-building tool that gathers enthusiastic customers who will more likely evangelize it, knowledge-share with, and encourage newer users to try it and be a valuable source of feedback post-launch.

  • A waitlist is essential in marketing and lead generation to promote future features, communicate new offerings of your existing service, or sell separate products you may develop later.

  • The tangible interest in your service, backed up by a brimming waitlist, shows potential investors that there is indeed demand in the market for it, that you have a ready user base, and that you have secured possible committed revenue. It gives the impression that your service is a sound investment.

For all these reasons, a product waitlist can help your business. But for a product launch, it is not enough to have a waitlist page, it is important to know how to build a high-converting SaaS waitlist.

How to Build a SaaS Waitlist Using Next.js and BCMS

In this guide, I will build a waitlist for a fictional language-learning app called Lingua Mate. It will collect potential customer names and emails and will provide pre-launch updates on the service. Its front end is built with Next.js and BCMS acts as its back end.

Prerequisites

To follow along, you will need:

Node.js installed.

Set up BCMS and create SaaS waitlist site news entries

Here, you will find all the necessary steps. Let's go.

Create a new project

On the BCMS dashboard, click the “Create new project +” button or visit the “Create new project” page. A project is a collection of templates, entries, widgets, and media, among other things, used to manage your content. Put in the project name as “Lingua Mate Waitlist”.

Create new project page.png

Create customer and news templates

On BCMS, a template describes the structure of your content. In this case, it defines customer and news content structures used to make entries of each.

On the dashboard, under “Administration”, navigate to the “Templates” page. Create two templates, “Customer” and “News”, by clicking the “Create new template +” button.

The Customer template defines a customer. The News template defines the news and updates about Lingua Mate.

The Customer template has two extra properties aside from the default title and slug:

  • Name: String, Required

  • Email: String, Required

Customer template page

The News template has one extra property:

  • Body: Rich Text, Required

News template page

Add "News page" entries

A BCMS Entry is an individual record modeled after a template. Once you’ve created the News template, go to its equivalent “Entries” page, click the “Create new entry” button, and add about three update entries.

New News entry page

Get an API key

API keys enable you to post, fetch, and manage content from your BCMS project on your app with varying permissions for each template you create. Under “Project Settings”, select the “API keys” tab and then click the “Add a new key +” button. Provide a name and description and add the key.

Add new keyform.png

Under “Template permissions”, select “Can create” for the Customer template and “Can get” for the News template. Leave this page open as you will copy these API key values to the Next.js project.

Template permission section on the Waitlist Site key form.png

Set up the Next.js app

The Next.js app is the frontend of the waitlist. On it, customers enter their emails and names through a form, which is then posted to BCMS. You will list news and updates of the SaaS on it as well.

Generate the Next.js app

Create the app on your terminal using the command below.

npx create-next-app@latest

Use these values to answer the prompts from create-next-app:

prompt values
Prompt replies

Add dependencies

After the project setup is completed, install these dependencies:

npm i --save @thebcms/cli @thebcms/client @thebcms/components-react @headlessui/react yup formik

This is what each of the dependencies does:

dependencies

Amend scripts

Update your package.json scripts object to this so that the BCMS CLI tool can pull types for your project.

"scripts": {
   "dev": "bcms --pull types --lng ts && next dev",
   "build": "bcms --pull types --lng ts && next build",
   "start": "bcms --pull types --lng ts && next start",
   "lint": "bcms --pull types --lng ts && next lint"
}

The types are saved in /bcms/types/.

Add environment variables

Create a .env file and place API key values from before in it.

BCMS_ORG_ID="Your BCMS_ORG_ID here"
BCMS_INSTANCE_ID="Your BCMS_INSTANCE_ID here"
BCMS_API_KEY_ID="Your BCMS_API_KEY_ID here"
BCMS_API_KEY_SECRET="Your BCMS_API_KEY_SECRET here"

Create a bcms.config.cjs file to hold configuration for the BCMS packages. Place the API key values here as well.

module.exports = {
   client: {
      orgId: "your orgId here",
      instanceId: "your instanceId here",
      apiKey: {
         id: "your api key id here",
         secret: "your api key secret here",
      },
   },
}

Update the TypeScript configuration in the tsconfig.json file. Change the moduleResolution to node.

Create a BCMS client

Create a BCMS client that you will use to post customer information and fetch news updates. Make a file called app/lib/bcms.ts and in it place the code below:

import { Client } from "@thebcms/client"

const getBCMSClient = () => {
   const bcmsOrgId = process.env.BCMS_ORG_ID
   const bcmsInstanceId = process.env.BCMS_INSTANCE_ID
   const bcmsApiKeyId = process.env.BCMS_API_KEY_ID
   const bcmsApiKeySecret = process.env.BCMS_API_KEY_SECRET

   if (bcmsOrgId && bcmsInstanceId && bcmsApiKeyId && bcmsApiKeySecret) {
      return new Client(
         bcmsOrgId,
         bcmsInstanceId,
         {
            id: bcmsApiKeyId,
            secret: bcmsApiKeySecret,
         },
         {
            injectSvg: true,
         }
      )
   }

   throw new Error("BCMS organization ID, instance ID, API key ID or secret is missing")
}

export const bcms = getBCMSClient()

Add form schemas

Create another file, app/lib/schemas.ts, to hold schemas to validate the customer input in the customer information capture form. Place this in it.

import * as Yup from "yup"

export const CustomerSchema = Yup.object().shape({
   name: Yup.string()
     .min(2, 'Too Short!')
     .max(70, 'Too Long!')
     .required('Required'),
   email: Yup.string()
     .email('Invalid email')
     .required('Required'),
})

Update styling

In the app/globals.css file, add this styling. It adds a background to the body of the app.

body {
   color: var(--foreground);
   background: var(--background);
   font-family: Arial, Helvetica, sans-serif;
   background: radial-gradient(farthest-side at -33.33% 50%, #0000 52%, rgba(15, 15, 15, 0.55) 54% 57%, #0000 59%) 0 calc(60px/2), radial-gradient(farthest-side at 50% 133.33%, #0000 52%, rgba(15, 15, 15, 0.55) 54% 57%, #0000 59%) calc(60px/2) 0, radial-gradient(farthest-side at 133.33% 50%, #0000 52%, rgba(15, 15, 15, 0.55) 54% 57%, #0000 59%), radial-gradient(farthest-side at 50% -33.33%, #0000 52%, rgba(15, 15, 15, 0.55) 54% 57%, #0000 59%), #000000;
   background-size: calc(60px/4.667) 60px, 60px calc(60px/4.667);
}

Update metadata

In app/layout.tsx, change the metadata to this, to reflect the name and description of the SaaS and for SEO purposes.

export const metadata: Metadata = {
   title: "Lingua Mate: Language Learning App",
   description: "Your personal AI tutor is ready 24/7 to help you practice, perfect, and    progress",
}

Include assets

Create a public/language.svg file to use as the app logo. Add this to the file.

<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">  <path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" /></svg>

Fetch news form and post customer information to BCMS

Since the BCMS client requires a sensitive API key and secret to post customer information to BCMS and fetch updates, you’ll use server actions to perform these operations. Create a app/actions.ts file and add this to it.

"use server"

import { bcms } from "@/app/lib/bcms"
import { NewsEntry } from "@/bcms/types/ts"

// fetches news from BCMS
const fetchNews = async () => {
   return (await bcms.entry.getAll("news")) as NewsEntry[]
}

// creates a customer given their email and name
const createCustomer = (customer: { email: string, name: string }) => {
   return bcms.entry.create("customer", {
      meta: [{ lng: 'en', data: { ...customer, title: customer.name, slug: '' } }],
      statuses: [],
      content: []
   })
}

export { fetchNews, createCustomer }

The two actions here fetch news updates from BCMS and post customer information from the waitlist form.

Add a form to capture customer emails

Create the form component at app/components/WaitlistForm.tsx and add this to the file

"use client"

import { Field, Form, Formik } from "formik"
import clsx from "clsx"
import { Button } from "@headlessui/react"
import { createCustomer } from "@/app/actions"
import { CustomerSchema } from "@/app/lib/schemas"
import { useState, ReactNode } from "react"

const inputStyles = clsx(
   "block w-full rounded-lg border-none bg-white/10 py-1.5 px-3 text-sm/6 text-white",
   "focus:outline-none data-[focus]:outline-2 data-[focus]:-outline-offset-2 data-   [focus]:outline-white/25"
   )

const CustomError = ({ children }: { children: ReactNode }) => {
   return <div className="text-xs text-red-600 text-start h-[16px] my-1">{children}</div>
}

const WaitlistForm = () => {
   const [submitError, setSubmitError] = useState("")
   const [success, setSuccess] = useState(false)

   return <Formik
         initialValues={{
            name: "",
            email: "",
         }}
         validationSchema={CustomerSchema}
         onSubmit={async (values, formikBag) => {
            const errors = await formikBag.validateForm()

            if (!errors.name && !errors.email) {
               createCustomer(values).then(() => {
                  setSuccess(true)
                  formikBag.resetForm()
               }).catch(() => {
                  setSubmitError("There was a problem adding you to the waitlist.")
               })} else {
                  setSubmitError("There was a problem with the name or email you entered. Please try again")
               }
         }}>
   {({ errors, touched, isValid }) => (
      <Form className="flex flex-col my-3 justify-stretch max-sm:w-[300px] sm:w-[400px]">
         <label>
            <Field
               id="name"
               name="name"
               placeholder="Your name"
               className={inputStyles}
            />
         </label>
         <CustomError>
            {errors.name && touched.name ? errors.name : null}
         </CustomError>
         <label>
            <Field
               id="email"
               name="email"
               placeholder="Your email"
               type="email"
               className={inputStyles}
            />
         </label>
         <CustomError>
            {errors.email && touched.email ? errors.email : null}
         </CustomError>
         <Button
            type="submit"
            className="inline-flex items-center gap-2 rounded-md bg-zinc-800 py-1.5 px-3 text-sm/6 font-semibold text-white shadow-inner shadow-white/10 focus:outline-none data-[hover]:bg-zinc-700 data-[open]:bg-zinc-800 data-[focus]:outline-1 data-[focus]:outline-white text-center mt-2 flex justify-center"
            disabled={!isValid}
         >
            Join the waitlist!
         </Button>
         <div className="text-gray-500 max-w-[300px] my-3 mx-auto">
            For any queries, reach out to us on social media!
         </div>
         <div className={`font-light bg-red-500/10 bg-red-700/25 rounded-lg w-100 px-3 py-1 m-1 justify-between flex ${submitError ? "flex" : "hidden"}`}>
            <div>{submitError}</div>
            <Button className="ms-3" onClick={() => setSubmitError("")}>𝗫</Button>
         </div>
         <div className={`font-light bg-green-500/10 bg-green-700/25 rounded-lg w-100 py-1 px-3 m-1 justify-between ${success ? "flex" : "hidden"}`}>
             <div>You&apos;ve successfully signed up for the waitlist.</div>
             <Button className="ms-3" onClick={() => setSuccess(false)}>𝗫</Button>
          </div>
       </Form>)}
    </Formik>
}

export default WaitlistForm

This component contains a form with two fields, one for the customer's email and another for their name. When the input is submitted, it is validated against the schema created earlier and then posted to BCMS. Any input validation or submission errors are reported.

Update the home page

Update the home page, app/page.tsx, with the code below to include the customer information form and the news updates list.

import Image from "next/image"
import WaitlistForm from "@/app/components/WaitlistForm"
import { fetchNews } from "@/app/actions"
import { BCMSContentManager } from "@thebcms/components-react"

export default async function Home() {
   const news = await fetchNews();

   return (
   <div
      className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-      screen p-8 pb-20 gap-16 sm:p-20"
      style={{ fontFamily: "var(--font-geist-sans)" }}
   >
      <main className="flex flex-col gap-2 row-start-2 items-center text-center">
      <div className="bg-zinc-700/30 rounded-xl px-6 py-1 text-orange-500">Coming soon!      </div>
      <Image
         priority
         src="/language.svg"
         alt="Lingua Mate logo"
         width={70}
         height={70}
         className="rounded-full p-4 bg-orange-500"
      />
      <h1 className="text-orange-500 text-4xl mb-4">Lingua Mate</h1>
      <h2 className="text-3xl mb-4 max-w-[600px]">Your personal AI tutor is ready 24/7 to help you practice, perfect, and progress.</h2>
      <h6 className="text-gray-500 max-w-[400px]">Join the waitlist to get early access to the product and receive updates on the progress!</h6>
      <WaitlistForm />
      <div className="flex flex-col max-w-[600px] items-center gap-4">
         <h2 className="text-3xl mt-4 max-w-[600px]">Updates</h2>
         <h6 className="text-gray-500 max-w-[400px]">Join the waitlist to get early access to the product and receive updates on the progress!</h6>
          {news.map((item, index) => {
             return <div key={`update-${index}`} className="bg-white/10 border border-white/15 p-4 rounded-2xl mb-2 text-start">
             <span className="flex w-100 justify-between">
                <p className="text-orange-500">{item.meta.en?.title}</p>
                <p className="text-orange-500 text-xs">{(new Date(item.updatedAt)).toLocaleDateString()}</p>
             </span>
             <span className="text-gray-300">
                {item.meta.en?.body.nodes && <BCMSContentManager items={item.meta.en?.body.nodes} />}
              </span>
           </div>
        })}
     </div>
   </main>
</div>)
}

The news and updates are fetched from BCMS, and each is listed as a card. For every news item, there is a title and the date it was updated to accompany them.

Here is a screenshot of the finished homepage now.

SaaS waitlist example:

waitlist example

UI/UX best practices for maximizing conversions on your SaaS waitlist

Eye-catching, consistent branding as seen in logos, color palettes, typography, and graphics easily captures customers' attention and draws them to the waitlist to see more of your product. A solid description tells potential customers what your service will deliver and the benefits they may get, to excite them to try it. In the sign-up form, include a strong call to action on the sign-up button to persuade them to be part of your exclusive waitlist. Links to social media added to the waitlist let customers further engage with and learn more about your brand on external platforms, increasing their trust and excitement for the upcoming launch.

Although not included in the project, adding feedback from early users who have tested your app, as well as links to third-party media like news organizations that gave your service positive coverage, can go a long way in building credibility. Including a launch date tracker that indicates major events that lead up to the launch date can create anticipation in your customers, exciting them to sign up. If already decided, clearly display the launch date for the service so that customers can know when to follow up and try your service.

Update the waitlist news to show SaaS progress

Since the news entries are pulled from BCMS whenever the Next.js site is loaded, you can directly add new news entries on the dashboard under “Entries” and then “News”. They will automatically reflect on the site to new visitors.

Add admin controls in BCMS to manage the waitlist

If you have a larger team and people dedicated to customer relations, you can put them in charge of managing the waitlist emails. These can include team members like marketers, customer support or success specialists, or even salespeople. On the dashboard, under “Project Settings” in the “General Settings” and then “Manage Members”, you can grant them permission to access and manage the waitlist.

Manage members

Deployment

Now that your waitlist is complete, you can deploy it on platforms like Vercel, Netlify, or Render that support full-fledged Next.js apps. All you need to do is upload the source code and set the aforementioned environment variables and you are good to go. Follow these guides to deploy your app: Vercel, Netlify, or Render. 

Conclusion

You don’t have to stop just here. Include other features on your waitlist, such as an FAQ, newsletter signup, perks for early customers like discounts, and feature lists for your service. You can even connect BCMS to an email marketing platform like Mailchimp or a customer relationship management system like Hubspot and pass the emails to them.


BCMS is a headless CMS with flexible content modeling. It offers composable content, media management, team collaboration, integrations, and extension tools, among other features. Learn more about BCMS and how to use it on its docs site.   

It takes a minute to start using BCMS

Gradient

Join our Newsletter

Get all the latest BCMS updates, news and events.

You’re in!

The first mail will be in your inbox next Monday!
Until then, let’s connect on Discord as well:

Join BCMS community on Discord

By submitting this form you consent to us emailing you occasionally about our products and services. You can unsubscribe from emails at any time, and we will never pass your email to third parties.

Gradient