How to create a simple waitlist form in Next.js using Supabase to collect responses


Prerequisites
Initialize a Next.js project (Next.js 15 recommended) with Tailwind CSS.
(Optional) This guide uses Shadcn UI components. Install it from the official docs website: ui.shadcn.com
Setup Supabase credentials in
.env.local
Setup supabase clients and middleware (optional)
Note: Replace the
<Input />
,<Button>
, and<Toast />
components with your own components or default tags if you don’t want to install Shadcn UI
Here’s the notes on how to create a simple waitlist form in Next.js, collect responses from it, and store it on Supabase.
Create a table called “waitlist“ in the Supabase SQL Editor:
-- 1. Create the table for the waitlist CREATE TABLE public.waitlist ( id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, email text NOT NULL UNIQUE, created_at timestamptz DEFAULT now() ); -- 2. Enable Row Level Security (RLS) on the table ALTER TABLE public.waitlist ENABLE ROW LEVEL SECURITY; -- 3. Create a policy that allows public insertion into the table CREATE POLICY "Allow public insert" ON public.waitlist FOR INSERT WITH CHECK (true);
Create the API for handling the form submission,
/api/waitlist
:import { createClient } from '@/utils/supabase/server'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { const { email } = await request.json(); if (!email) { return NextResponse.json({ error: 'Email is required' }, { status: 400 }); } const supabase = await createClient(); const { error } = await supabase.from('waitlist').insert([{ email: email.trim().toLowerCase() }]); if (error) { if (error.code === '23505') { // unique_violation return NextResponse.json({ message: 'You are already on the waitlist.' }, { status: 200 }); } return NextResponse.json({ error: error.message || 'Something went wrong' }, { status: 500 }); } return NextResponse.json({ message: 'You have been added to the waitlist!', email: email.trim().toLowerCase() }); }
Add this waitlist form code to your landing page or wherever you want it:
"use client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { useState } from "react"; import { toast } from "sonner"; import { Loader2, Check } from "lucide-react"; export default function Page() { const [email, setEmail] = useState(''); const [loading, setLoading] = useState(false); const [isSuccess, setIsSuccess] = useState(false); // Function to handle the submission const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!email) { toast.error("Please enter your email."); return; } setLoading(true); try { const response = await fetch('/api/waitlist', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: email.trim().toLowerCase() }), }); const data = await response.json(); if (response.ok) { toast.success(data.message); setEmail(''); setIsSuccess(true); } else { toast.error(data.error || "Something went wrong."); } } catch { toast.error("An unexpected error occurred."); } finally { setLoading(false); } }; return ( <> {/* Your JSX Code */} {/* Waitlist form Code: */} {isSuccess ? ( <Button disabled className="cursor-default bg-black text-white border disabled:opacity-100"> <Check className="h-4 w-4 text-green-500" /> you're on the list! </Button> ) : ( <form onSubmit={handleSubmit} className="flex gap-2"> <Input type="email" placeholder="e.g., naruto@gmail.com" value={email} onChange={(e) => setEmail(e.target.value)} disabled={loading} /> <Button type="submit" className="cursor-pointer" disabled={loading}> {loading ? <><Loader2 className="animate-spin mr-2" /> joining...</> : 'join waitlist'} </Button> </form> )} </> ); }
Note: There is some
toast
based logic in the UI component. Replace the toast logic withconsole.log()
statements if you’re not using or haven’t installed thesonner
package.Test, Deploy, and Launch!
Result
Here is an example site, called nvyt.xyz that is using the above code:
Here’s the result upon submitting the form:
Note:
If you’re using Shadcn UI components, you should see a similar result in the UI.
I’ve used the
<Toast />
component in my root layout is how I got that toast, in the bottom right corner.
Here’s how to use this with AI tool(s) like Cursor
Write a prompt saying:
“Build a waitlist page using the instructions and the code, as is, in this blog post:“
And, paste this post’s link in an code editor like Cursor and let it handle the coding for you!
Bonus
You can refer to my other article about rate-limiting your Next.js APIs like the one we have here, to prevent brute-force attacks bombing your public waitlist form with random email address. It has the AI prompt too, if you just want to skip coding.
Here’s the link: How to rate limit your Next.js APIs using Upstash?
P.S.
And… that’s it! Hope this helps!
Do you need a website or an app for your business?
You can reach out to me at @CharanMNX on X/Twitter or email me at charan@devsforfun.com
Here are my other socials if you wanna talk:
Instagram: iam.charan.dev
X/Twitter: @CharanMNX
LinkedIn: Charan Manikanta Nalla
GitHub: CharanMN7
YouTube: Charan
Website: charan.dev
Happy Coding or Vibe Coding!
Subscribe to my newsletter
Read articles from Charan Manikanta Nalla directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Charan Manikanta Nalla
Charan Manikanta Nalla
I'm a software engineer and freelance full-stack developer. Need a website/app for your business? Hit me up on one of my socials, and let's talk!