Waitlist System

Build an effective waitlist with email capture, referral mechanics, and viral loop optimization

The Waitlist workflow helps you build anticipation before launch by capturing interested users, implementing referral mechanics, and maintaining engagement through strategic communication.

Overview#

PropertyValue
Components5 (Signup, Referral, Dashboard, Emails, Analytics)
TierFree
Typical Duration3-5 days to build
Best ForPre-launch signups, beta access, product launches

Outcomes#

A successful waitlist system delivers:

  • Email capture form with validation
  • Unique referral links for viral growth
  • Position tracking to incentivize sharing
  • Automated email sequences
  • Analytics dashboard to track growth

System Architecture#

┌─────────────────┐ │ Landing Page │ └────────┬────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ Waitlist API │ ├─────────────────────────────────────────────────┤ │ POST /api/waitlist │ │ - Validate email │ │ - Generate referral code │ │ - Calculate position │ │ - Send welcome email │ │ - Track referral attribution │ └─────────────────────────────────────────────────┘ │ ┌──────────────┴──────────────┐ │ │ ▼ ▼ ┌────────────┐ ┌─────────────┐ │ Database │ │ Email │ │ (Waitlist │ │ Service │ │ entries) │ │ (Resend) │ └────────────┘ └─────────────┘

Database Schema#

1// prisma/schema.prisma 2 3model WaitlistEntry { 4 id String @id @default(cuid()) 5 email String @unique 6 referralCode String @unique 7 referredBy String? // referralCode of referrer 8 position Int 9 referralCount Int @default(0) 10 status WaitlistStatus @default(PENDING) 11 12 createdAt DateTime @default(now()) 13 updatedAt DateTime @updatedAt 14 15 @@index([referralCode]) 16 @@index([referredBy]) 17} 18 19enum WaitlistStatus { 20 PENDING 21 INVITED 22 CONVERTED 23 UNSUBSCRIBED 24}

API Implementation#

Signup Endpoint#

1// app/api/waitlist/route.ts 2import { NextRequest, NextResponse } from 'next/server'; 3import { prisma } from '@/lib/prisma'; 4import { nanoid } from 'nanoid'; 5import { sendWelcomeEmail } from '@/lib/email'; 6import { z } from 'zod'; 7 8const signupSchema = z.object({ 9 email: z.string().email(), 10 referralCode: z.string().optional(), 11}); 12 13export async function POST(request: NextRequest) { 14 try { 15 const body = await request.json(); 16 const { email, referralCode } = signupSchema.parse(body); 17 18 // Check if email already exists 19 const existing = await prisma.waitlistEntry.findUnique({ 20 where: { email }, 21 }); 22 23 if (existing) { 24 return NextResponse.json({ 25 success: true, 26 position: existing.position, 27 referralCode: existing.referralCode, 28 message: "You're already on the list!", 29 }); 30 } 31 32 // Get current position 33 const count = await prisma.waitlistEntry.count(); 34 const position = count + 1; 35 36 // Generate unique referral code 37 const newReferralCode = nanoid(8); 38 39 // Create waitlist entry 40 const entry = await prisma.waitlistEntry.create({ 41 data: { 42 email, 43 referralCode: newReferralCode, 44 referredBy: referralCode || null, 45 position, 46 }, 47 }); 48 49 // Update referrer's count if applicable 50 if (referralCode) { 51 await prisma.waitlistEntry.updateMany({ 52 where: { referralCode }, 53 data: { referralCount: { increment: 1 } }, 54 }); 55 } 56 57 // Send welcome email 58 await sendWelcomeEmail({ 59 email, 60 position, 61 referralCode: newReferralCode, 62 }); 63 64 return NextResponse.json({ 65 success: true, 66 position, 67 referralCode: newReferralCode, 68 message: 'Welcome to the waitlist!', 69 }); 70 } catch (error) { 71 if (error instanceof z.ZodError) { 72 return NextResponse.json( 73 { error: 'Invalid email address' }, 74 { status: 400 } 75 ); 76 } 77 console.error('Waitlist signup error:', error); 78 return NextResponse.json( 79 { error: 'Failed to join waitlist' }, 80 { status: 500 } 81 ); 82 } 83}

Position Check Endpoint#

1// app/api/waitlist/position/route.ts 2export async function GET(request: NextRequest) { 3 const { searchParams } = new URL(request.url); 4 const email = searchParams.get('email'); 5 6 if (!email) { 7 return NextResponse.json( 8 { error: 'Email required' }, 9 { status: 400 } 10 ); 11 } 12 13 const entry = await prisma.waitlistEntry.findUnique({ 14 where: { email }, 15 }); 16 17 if (!entry) { 18 return NextResponse.json( 19 { error: 'Not found' }, 20 { status: 404 } 21 ); 22 } 23 24 // Calculate effective position (original - referrals) 25 const effectivePosition = Math.max( 26 1, 27 entry.position - (entry.referralCount * 10) // Each referral moves up 10 spots 28 ); 29 30 return NextResponse.json({ 31 position: effectivePosition, 32 originalPosition: entry.position, 33 referralCount: entry.referralCount, 34 referralCode: entry.referralCode, 35 }); 36}

Frontend Components#

Waitlist Form#

1// components/marketing/WaitlistForm.tsx 2'use client'; 3 4import { useState } from 'react'; 5import { Button } from '@/components/ui/button'; 6import { Input } from '@/components/ui/input'; 7import { useSearchParams } from 'next/navigation'; 8 9interface WaitlistResult { 10 position: number; 11 referralCode: string; 12} 13 14export function WaitlistForm() { 15 const searchParams = useSearchParams(); 16 const referralCode = searchParams.get('ref'); 17 18 const [email, setEmail] = useState(''); 19 const [loading, setLoading] = useState(false); 20 const [result, setResult] = useState<WaitlistResult | null>(null); 21 const [error, setError] = useState(''); 22 23 async function handleSubmit(e: React.FormEvent) { 24 e.preventDefault(); 25 setLoading(true); 26 setError(''); 27 28 try { 29 const res = await fetch('/api/waitlist', { 30 method: 'POST', 31 headers: { 'Content-Type': 'application/json' }, 32 body: JSON.stringify({ email, referralCode }), 33 }); 34 35 const data = await res.json(); 36 37 if (!res.ok) { 38 throw new Error(data.error || 'Failed to join'); 39 } 40 41 setResult({ 42 position: data.position, 43 referralCode: data.referralCode, 44 }); 45 } catch (err) { 46 setError(err instanceof Error ? err.message : 'Something went wrong'); 47 } finally { 48 setLoading(false); 49 } 50 } 51 52 if (result) { 53 return <WaitlistSuccess {...result} />; 54 } 55 56 return ( 57 <form onSubmit={handleSubmit} className="space-y-4"> 58 <div className="flex gap-2"> 59 <Input 60 type="email" 61 placeholder="Enter your email" 62 value={email} 63 onChange={(e) => setEmail(e.target.value)} 64 required 65 className="flex-1" 66 /> 67 <Button type="submit" disabled={loading}> 68 {loading ? 'Joining...' : 'Join Waitlist'} 69 </Button> 70 </div> 71 {error && ( 72 <p className="text-sm text-destructive">{error}</p> 73 )} 74 <p className="text-sm text-muted-foreground"> 75 Join 2,500+ others waiting for early access 76 </p> 77 </form> 78 ); 79}

Success State with Referral#

1// components/marketing/WaitlistSuccess.tsx 2'use client'; 3 4import { useState } from 'react'; 5import { Button } from '@/components/ui/button'; 6import { CheckCircle, Copy, Twitter, Linkedin } from 'lucide-react'; 7 8interface WaitlistSuccessProps { 9 position: number; 10 referralCode: string; 11} 12 13export function WaitlistSuccess({ position, referralCode }: WaitlistSuccessProps) { 14 const [copied, setCopied] = useState(false); 15 const referralUrl = `${window.location.origin}?ref=${referralCode}`; 16 17 async function copyLink() { 18 await navigator.clipboard.writeText(referralUrl); 19 setCopied(true); 20 setTimeout(() => setCopied(false), 2000); 21 } 22 23 const twitterText = encodeURIComponent( 24 "I just joined the waitlist for [Product Name]! Join me and get early access:" 25 ); 26 const twitterUrl = `https://twitter.com/intent/tweet?text=${twitterText}&url=${encodeURIComponent(referralUrl)}`; 27 28 const linkedinUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(referralUrl)}`; 29 30 return ( 31 <div className="text-center space-y-6 p-8 rounded-lg border bg-card"> 32 <div className="flex justify-center"> 33 <CheckCircle className="w-16 h-16 text-green-500" /> 34 </div> 35 36 <div> 37 <h3 className="text-2xl font-bold mb-2">You're on the list!</h3> 38 <p className="text-4xl font-bold text-primary">#{position}</p> 39 <p className="text-muted-foreground">Your position in line</p> 40 </div> 41 42 <div className="space-y-3"> 43 <p className="font-medium">Move up by referring friends</p> 44 <p className="text-sm text-muted-foreground"> 45 Each referral moves you up 10 spots 46 </p> 47 48 <div className="flex gap-2"> 49 <Input 50 value={referralUrl} 51 readOnly 52 className="text-sm" 53 /> 54 <Button variant="outline" onClick={copyLink}> 55 {copied ? <CheckCircle className="w-4 h-4" /> : <Copy className="w-4 h-4" />} 56 </Button> 57 </div> 58 59 <div className="flex justify-center gap-3 pt-4"> 60 <Button variant="outline" size="sm" asChild> 61 <a href={twitterUrl} target="_blank" rel="noopener noreferrer"> 62 <Twitter className="w-4 h-4 mr-2" /> 63 Share on Twitter 64 </a> 65 </Button> 66 <Button variant="outline" size="sm" asChild> 67 <a href={linkedinUrl} target="_blank" rel="noopener noreferrer"> 68 <Linkedin className="w-4 h-4 mr-2" /> 69 Share on LinkedIn 70 </a> 71 </Button> 72 </div> 73 </div> 74 </div> 75 ); 76}

Email Sequences#

Welcome Email#

1// lib/email/templates/waitlist-welcome.ts 2import { Resend } from 'resend'; 3 4const resend = new Resend(process.env.RESEND_API_KEY); 5 6interface WelcomeEmailProps { 7 email: string; 8 position: number; 9 referralCode: string; 10} 11 12export async function sendWelcomeEmail({ 13 email, 14 position, 15 referralCode, 16}: WelcomeEmailProps) { 17 const referralUrl = `https://yoursite.com?ref=${referralCode}`; 18 19 await resend.emails.send({ 20 from: 'Your Product <hello@yoursite.com>', 21 to: email, 22 subject: "You're on the waitlist! Here's how to skip the line", 23 html: ` 24 <h1>Welcome to the Waitlist!</h1> 25 <p>You're #${position} in line for early access.</p> 26 27 <h2>Skip the line</h2> 28 <p>For every friend who joins using your link, you'll move up 10 spots:</p> 29 <p><a href="${referralUrl}">${referralUrl}</a></p> 30 31 <h2>What's next?</h2> 32 <ul> 33 <li>We'll notify you when it's your turn</li> 34 <li>Early access members get 50% off the first year</li> 35 <li>Follow us on Twitter for updates</li> 36 </ul> 37 `, 38 }); 39}

Email Sequence Strategy#

DayEmailPurpose
0WelcomeConfirm signup, share referral link
3Behind the scenesShare your story, build connection
7Social proofShare testimonials or beta feedback
14Feature previewTease upcoming features
21Urgency"Launch is coming soon"
LaunchAccess emailInvite to sign up

Analytics Dashboard#

Key Metrics to Track#

1// lib/analytics/waitlist.ts 2export async function getWaitlistMetrics() { 3 const [total, today, referrals, conversionRate] = await Promise.all([ 4 // Total signups 5 prisma.waitlistEntry.count(), 6 7 // Signups today 8 prisma.waitlistEntry.count({ 9 where: { 10 createdAt: { 11 gte: new Date(new Date().setHours(0, 0, 0, 0)), 12 }, 13 }, 14 }), 15 16 // Referral stats 17 prisma.waitlistEntry.aggregate({ 18 _sum: { referralCount: true }, 19 _avg: { referralCount: true }, 20 }), 21 22 // Conversion rate 23 prisma.waitlistEntry.groupBy({ 24 by: ['status'], 25 _count: true, 26 }), 27 ]); 28 29 const converted = conversionRate.find( 30 (r) => r.status === 'CONVERTED' 31 )?._count || 0; 32 33 return { 34 total, 35 today, 36 referralTotal: referrals._sum.referralCount || 0, 37 referralAverage: referrals._avg.referralCount || 0, 38 conversionRate: total > 0 ? (converted / total) * 100 : 0, 39 }; 40}

Admin Dashboard Component#

1// app/admin/waitlist/page.tsx 2import { getWaitlistMetrics } from '@/lib/analytics/waitlist'; 3 4export default async function WaitlistDashboard() { 5 const metrics = await getWaitlistMetrics(); 6 7 return ( 8 <div className="grid grid-cols-4 gap-4"> 9 <MetricCard 10 title="Total Signups" 11 value={metrics.total.toLocaleString()} 12 /> 13 <MetricCard 14 title="Today" 15 value={metrics.today.toLocaleString()} 16 /> 17 <MetricCard 18 title="Referrals" 19 value={metrics.referralTotal.toLocaleString()} 20 /> 21 <MetricCard 22 title="Viral Coefficient" 23 value={(metrics.referralAverage).toFixed(2)} 24 /> 25 </div> 26 ); 27}
PhaseAgentPurpose
Designui-ux-expertForm and success state design
Backendbackend-expertAPI and database implementation
Emailcopywriting-expertEmail sequence copy
Analyticsanalytics-expertDashboard and tracking

Viral Loop Optimization#

Incentive Ideas#

IncentiveMechanism
Position jumpMove up X spots per referral
Exclusive accessTop referrers get beta access
RewardsSwag, discounts, or free months
Milestone badgesUnlock achievements for sharing

Best Practices#

  1. Make sharing easy - One-click share buttons
  2. Show progress - Display position changes in real-time
  3. Create urgency - Limited spots, launch countdown
  4. Reward top referrers - Leaderboard or special perks
  5. Send reminders - Periodic emails about position

Deliverables#

DeliverableDescription
Database schemaWaitlistEntry model with referral tracking
API endpointsSignup, position check, referral tracking
Frontend componentsForm, success state, share buttons
Email templatesWelcome, nurture, launch sequence
Admin dashboardMetrics and waitlist management