16th January 2024 • 5 min read
How to build a waitlist with Supabase and Next.js

Mohammed Elhadi Baci
Let's build a simple app where:
1. Users sign up and join a waitlist.
2. Upon admin approval, users get a notification email and can use the app.
We'll use Next.js, Supabase, and Resend.
The code for this tutorial can be accessed here.
Let's first setup a project from the Supabase dashboard and create a waitlist table from the Supabase SQL editor :
1create table waitlist (2 user_id uuid not null references auth.users on delete cascade,3 email text unique not null,4 approved boolean not null default false,5 primary key (user_id)6);78alter table waitlist enable row level security;910CREATE POLICY "Users can read their own waitlist row" ON "public"."waitlist"11AS PERMISSIVE FOR SELECT12TO authenticated13USING ((auth.uid() = user_id))14
What the query does:
Creates a waitlist table with
user_id,emailandapprovedcolumns.Each waitlist row corresponds to a unique
user_idcolumn from the built-inauth.userstable that holds all signed-up users. Therefore,user_idis both a foreign and primary key.When a user from the
auth.userstable is deleted, its corresponding waitlist row is deleted as well.approvedis a boolean value that determines whether the user has access to the app or not.Lastly, it applies row-level security to give read access to each user to its waitlist row.

Now let's create a database function, handle_new_user , that populates a waitlist row whenever a new user signs up:
1create function public.handle_new_user () returns trigger language plpgsql security definer2set3 search_path = public as $$4begin5 insert into public.waitlist (user_id, email)6 values (new.id, new.email);7 return new;8end;9$$;101112create trigger on_auth_user_created13after insert on auth.users for each row14execute procedure public.handle_new_user ();
Let's use create-next-app to create a Next.js project with authentication already backed-in:
1npx create-next-app -e with-supabaseOnce Supabase environment variables are filled in, we can run the project locally.
Notice that signup already works and creates a waitlist entry.

A new row inserted in the waitlist table for the signed-up user
Let's create the following 2 pages:
A dashboard exclusively for accessible use by approved waitlist members.
A waitlist landing page for all logged-in individuals not yet approved
1// app/dashboard/page.tsx23export default function Dashboard() {4 return (5 <div className="text-center min-h-screen flex flex-col justify-center gap-5">6 <h1 className="text-6xl font-medium">You're in!</h1>7 <p>Welcome to the dashboard! We're excited to have you on board.</p>8 </div>9 );10}
1// app/waitlist/page.tsx23export default function Waitlist() {4 return (5 <div className="text-center min-h-screen flex flex-col justify-center gap-5">6 <h1 className="text-6xl font-medium">Hold on!</h1>7 <p>8 We're not quite ready for you yet. We'll let you know when we're ready9 to onboard you.10 </p>11 </div>12 );13}
To make the access logic work, let's some redirects it in the middleware.ts file, which is executed before a user accesses any route:
1import { NextResponse, type NextRequest } from "next/server";2import { createClient } from "@/utils/supabase/middleware";34export async function middleware(req: NextRequest) {5 const { supabase, response } = createClient(req);67 const { data } = await supabase.auth.getSession();89 // If the user is accessing a page other than dashboard, do nothing10 if (req.nextUrl.pathname !== "/dashboard") return response;1112 const userLoggedIn = !!data?.session?.user;1314 // If the user is not logged in, redirect to login page15 if (!userLoggedIn) {16 return NextResponse.redirect(req.nextUrl.origin + "/login");17 }1819 // If user is logged in.2021 // Fetch the user's waitlist entry22 const { data: waitlistEntry } = await supabase23 .from("waitlist")24 .select("approved")25 .eq("user_id", data.session?.user.id)26 .single();2728 // The user is approved, allow access to dashboard29 if (waitlistEntry?.approved) return response;3031 // The user is not approved, redirect to waitlist page32 return NextResponse.redirect(req.nextUrl.origin + "/waitlist");33}
Logged-in users are now redirected to the waitlist if they try to access the dashboard before getting approved on the waitlist:

Setting approved to true on the waitlist table makes it possible for users to access the dahsboard:

Let's use Resend to notify users by email whenever they have granted access to the app.
1. Setup Resend
Once a Resend account is created, we can generate an API key and store it on Supabase secrets with the alias RESEND_API_KEY.

Supabase edge functions secrets
2. Create a Supabase edge function to send emails
We'll use Supabase CLI to create and deploy an edge function that sends emails.
First, we login with the Supabase CLI:
1npx supabase loginThen we initialize a local Supabase project:
1npx supabase initA new supabase folder will be generated at the root of the application.
Now let's link it to the remote Supabase project:
1npx supabase linkWith the CLI linked, let's run the following command to create a Supabase edge function named send-approval-email and instruct it to send an email using Resend:
1import { serve } from "https://deno.land/std@0.168.0/http/server.ts";2import { createClient } from "https://esm.sh/@supabase/supabase-js";34// The Resend API key is stored in an environment variable from the Supabase dashboard5const RESEND_API_KEY = Deno.env.get("RESEND_API_KEY");67const handler = async (_request: Request): Promise<Response> => {8 const json = await _request.json();910 const record = json.record;1112 // If the new value of approved is not true, do nothing13 if (!record?.approved)14 return new Response(15 JSON.stringify({16 message: "User not approved",17 }),18 {19 status: 200,20 headers: {21 "Content-Type": "application/json",22 },23 }24 );2526 try {27 const supabaseClient = createClient(28 // Supabase API URL - env var exported by default.29 Deno.env.get("SUPABASE_URL") ?? "",30 // Supabase API ANON KEY - env var exported by default.31 Deno.env.get("SUPABASE_ANON_KEY") ?? "",32 // Create client with Auth context of the user that called the function.33 // This way your row-level-security (RLS) policies are applied.34 {35 global: {36 headers: { Authorization: _request.headers.get("Authorization")! },37 },38 }39 );4041 // Fetch the user's data from the auth.users table42 const { data: userData } = await supabaseClient.auth.admin.getUserById(43 record.user_id44 );4546 const email = userData?.user?.email;4748 // If the user doesn't have an email, do nothing49 if (!email)50 return new Response(51 JSON.stringify({52 message: "User not found",53 }),54 {55 status: 200,56 headers: {57 "Content-Type": "application/json",58 },59 }60 );6162 // The user has an email, and is approved, send the email63 const res = await fetch("https://api.resend.com/emails", {64 method: "POST",65 headers: {66 "Content-Type": "application/json",67 Authorization: `Bearer ${RESEND_API_KEY}`,68 },69 body: JSON.stringify({70 from: 'onboarding@resend.dev',71 to: email,72 subject: "You've been accepted 🎉",73 html: "<strong>You're in!</strong>",74 }),75 });7677 const data = await res.json();7879 return new Response(JSON.stringify(data), {80 status: 200,81 headers: {82 "Content-Type": "application/json",83 },84 });85 } catch (error) {86 console.error(error);8788 return new Response(JSON.stringify(error), {89 status: 500,90 headers: {91 "Content-Type": "application/json",92 },93 });94 }95};9697serve(handler);
This function receives a waitlist row and, if it's approved, retrieves the corresponding email address from auth.users and sends an email using Resend API and the api-key created earlier.
To deploy the function to Supabase, we run the following command:
1npx supabase functions deploy send-approval-emailThe function is now deployed and can be seen on the database functions screen on Supabase's dashboard.

Supabase edge functions dashboard
3. Trigger email notifications
All that is left is for us to create a database webhook that triggers the send-approval-email function whenever the waitlist table is changed.
To create a webhook, let's open the project's webhooks dashboard, and enable the feature.
Once we have the webhook table visible click on the Create new hook at the top right and fill in the hook info:
The hook name: any value is accepted as long as it doesn't contain spaces.
The table: the
waitlisttable that we want to listen to its changes.Events: Choose the
updateevent since that's what we want to listen for.Type: should be Supabase edge function.
Edge function: the
send-approval-emailfunction should be selected by default, if not select it.Authentication header: since all edge functions deployed with authorization enabled we need to add an
Authorizationheader to the webhook request.

Now that the webhook is set, the entire flow can be tested.



