A waitlist page creates anticipation before launch and builds your email list. Here is how to build a compelling one.
Step 1: Database Schema
// db/schema.ts
import { pgTable, text, timestamp, integer, uuid } from "drizzle-orm/pg-core";
export const waitlist = pgTable("waitlist", {
id: uuid("id").defaultRandom().primaryKey(),
email: text("email").notNull().unique(),
name: text("name"),
referralCode: text("referral_code").notNull().unique(),
referredBy: text("referred_by"),
position: integer("position").notNull(),
referralCount: integer("referral_count").default(0),
createdAt: timestamp("created_at").defaultNow(),
});
Step 2: API Route
// app/api/waitlist/route.ts
import { NextResponse } from "next/server";
import { db } from "@/db";
import { waitlist } from "@/db/schema";
import { eq, count } from "drizzle-orm";
import { nanoid } from "nanoid";
export async function POST(req: Request) {
const { email, name, ref } = await req.json();
if (!email || !email.includes("@")) {
return NextResponse.json(
{ error: "Valid email is required" },
{ status: 400 }
);
}
// Check if already registered
const existing = await db
.select()
.from(waitlist)
.where(eq(waitlist.email, email))
.limit(1);
if (existing.length > 0) {
return NextResponse.json({
position: existing[0].position,
referralCode: existing[0].referralCode,
alreadyRegistered: true,
});
}
// Get next position
const [{ total }] = await db
.select({ total: count() })
.from(waitlist);
const position = total + 1;
// Generate referral code
const referralCode = nanoid(8);
// Insert
await db.insert(waitlist).values({
email,
name: name || null,
referralCode,
referredBy: ref || null,
position,
});
// Increment referrer's count
if (ref) {
await db
.update(waitlist)
.set({
referralCount: db.raw`referral_count + 1`,
})
.where(eq(waitlist.referralCode, ref));
}
return NextResponse.json({
position,
referralCode,
alreadyRegistered: false,
});
}
Step 3: Waitlist Page
// app/page.tsx
import { WaitlistForm } from "@/components/WaitlistForm";
import { CountdownTimer } from "@/components/CountdownTimer";
export default function WaitlistPage() {
return (
<main className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-950 via-blue-950 to-gray-950 text-white">
<div className="mx-auto max-w-lg px-4 text-center">
{/* Badge */}
<div className="mb-6 inline-flex items-center rounded-full border border-blue-500/30 bg-blue-500/10 px-4 py-1.5 text-sm text-blue-300">
Coming Soon
</div>
{/* Headline */}
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
Something great is
<span className="bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
{" "}
coming soon
</span>
</h1>
<p className="mt-4 text-lg text-gray-400">
Be the first to know when we launch. Join the waitlist and get early
access.
</p>
{/* Countdown */}
<div className="mt-8">
<CountdownTimer targetDate="2026-07-01T00:00:00Z" />
</div>
{/* Form */}
<div className="mt-8">
<WaitlistForm />
</div>
{/* Social proof */}
<p className="mt-6 text-sm text-gray-500">
Join 2,400+ people already on the waitlist
</p>
</div>
</main>
);
}
Step 4: Waitlist Form Component
"use client";
import { useState, useTransition } from "react";
import { useSearchParams } from "next/navigation";
export function WaitlistForm() {
const [email, setEmail] = useState("");
const [result, setResult] = useState<{
position: number;
referralCode: string;
} | null>(null);
const [isPending, startTransition] = useTransition();
const searchParams = useSearchParams();
const ref = searchParams.get("ref");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
startTransition(async () => {
const res = await fetch("/api/waitlist", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, ref }),
});
const data = await res.json();
if (res.ok) setResult(data);
});
}
if (result) {
const referralLink = `${window.location.origin}?ref=${result.referralCode}`;
return (
<div className="rounded-2xl border border-green-500/30 bg-green-500/10 p-6">
<p className="text-lg font-semibold text-green-300">
You are #{result.position} on the waitlist!
</p>
<p className="mt-2 text-sm text-gray-400">
Share your referral link to move up:
</p>
<div className="mt-3 flex gap-2">
<input
readOnly
value={referralLink}
className="flex-1 rounded-lg bg-gray-800 px-3 py-2 text-sm text-gray-300"
/>
<button
onClick={() => navigator.clipboard.writeText(referralLink)}
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium hover:bg-blue-700"
>
Copy
</button>
</div>
<div className="mt-4 flex justify-center gap-3">
<a
href={`https://twitter.com/intent/tweet?text=I just joined the waitlist!&url=${encodeURIComponent(referralLink)}`}
target="_blank"
rel="noopener noreferrer"
className="rounded-lg bg-gray-800 px-3 py-1.5 text-sm hover:bg-gray-700"
>
Share on X
</a>
</div>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
className="flex-1 rounded-xl border border-gray-700 bg-gray-900 px-4 py-3 text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none"
/>
<button
type="submit"
disabled={isPending}
className="rounded-xl bg-blue-600 px-6 py-3 font-medium hover:bg-blue-700 disabled:opacity-50"
>
{isPending ? "Joining..." : "Join"}
</button>
</form>
);
}
Step 5: Countdown Timer
"use client";
import { useState, useEffect } from "react";
export function CountdownTimer({ targetDate }: { targetDate: string }) {
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft());
function calculateTimeLeft() {
const diff = new Date(targetDate).getTime() - Date.now();
if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0 };
return {
days: Math.floor(diff / 86400000),
hours: Math.floor((diff % 86400000) / 3600000),
minutes: Math.floor((diff % 3600000) / 60000),
seconds: Math.floor((diff % 60000) / 1000),
};
}
useEffect(() => {
const timer = setInterval(() => setTimeLeft(calculateTimeLeft()), 1000);
return () => clearInterval(timer);
}, []);
return (
<div className="flex justify-center gap-4">
{Object.entries(timeLeft).map(([unit, value]) => (
<div key={unit} className="text-center">
<div className="rounded-xl bg-gray-800/50 px-4 py-3 text-3xl font-bold tabular-nums">
{String(value).padStart(2, "0")}
</div>
<p className="mt-1 text-xs uppercase tracking-wider text-gray-500">
{unit}
</p>
</div>
))}
</div>
);
}
Need a Pre-Launch Landing Page?
We design and build stunning launch pages that capture leads and build anticipation. Contact us to get started.