Add Waitlist + Viral Referral to Any SaaS Boilerplate
TL;DR
The fastest path to 1,000 waitlist signups is referral mechanics — reward users for bringing friends. A custom waitlist with position-based referrals can be added to any SaaS boilerplate in under a day. Three approaches: (1) embed Waitlisted.co or Loops for zero-code, (2) use the Prisma schema + Next.js API in this guide for a custom solution, (3) add ReferralHero for a full self-serve referral program. Each option takes different time and gives different control.
Key Takeaways
- Fastest (< 1 hour): embed Waitlisted.co or Loops — pre-built, no code
- Custom (2-4 hours): Prisma schema + API + email — full control, your branding
- Viral mechanic: share referral link → move up waitlist → reward early access
- Email capture is the goal: every waitlist subscriber is an owned email, not a rented social follower
- Referral boost math: position-based referrals typically 2-4x organic signups
- Convert waitlist → users: segment by referral count when opening early access
Why Waitlists Still Work in 2026
Waitlists are more effective than "coming soon" pages for three reasons:
- FOMO mechanics — "4,382 people are waiting" creates urgency
- Viral sharing — people share to move up the queue
- Email capture — you build your list before you build the product
The referral boost effect: early Loom and Superhuman both used position-based waitlists. The "share to jump the queue" mechanic reliably produces 3-5x the signups of a plain email capture form.
Option 1: Zero-Code — Waitlisted.co or Loops
Time: < 30 minutes Cost: Free tier available
The fastest path — embed a third-party widget:
<!-- Waitlisted.co embed (in your landing page): -->
<div id="waitlisted-widget" data-api-key="your_api_key"></div>
<script src="https://waitlisted.co/widget.js"></script>
<!-- Or Loops.so waitlist embed: -->
<script src="https://r.loops.so/embed.min.js" data-form-id="your-form-id"></script>
Waitlisted.co features (free):
- Referral tracking with custom share URL
- Position on waitlist shown to user
- Email notifications when position improves
- Dashboard with referral analytics
Loops.so approach: Loops is primarily an email tool but has waitlist forms — better if you plan to use Loops for all transactional emails anyway.
When to use third-party:
- Pre-launch (no codebase yet)
- Validating an idea (no time to build)
- < 5,000 expected signups (free tiers cover this)
Option 2: Custom Waitlist in Any Boilerplate
Time: 2-4 hours Cost: $0 (+ email service costs) Best for: when you want full control, custom branding, and data in your own DB
Database Schema
// prisma/schema.prisma — add to any boilerplate:
model WaitlistEntry {
id String @id @default(cuid())
email String @unique
name String?
position Int @unique // Position in queue
referralCode String @unique // Their unique referral link code
referredById String? // Who referred them
referredBy WaitlistEntry? @relation("Referrals", fields: [referredById], references: [id])
referrals WaitlistEntry[] @relation("Referrals")
referralCount Int @default(0) // Cached count
earlyAccess Boolean @default(false)
accessGrantedAt DateTime?
createdAt DateTime @default(now())
@@index([referralCode])
@@index([referredById])
}
API Route — Signup
// app/api/waitlist/route.ts
import { db } from '@/lib/db';
import { sendWelcomeEmail } from '@/lib/email';
import { nanoid } from 'nanoid';
export async function POST(req: Request) {
const { email, name, referralCode } = await req.json();
// Validate email:
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return Response.json({ error: 'Invalid email' }, { status: 400 });
}
// Check if already on waitlist:
const existing = await db.waitlistEntry.findUnique({ where: { email } });
if (existing) {
return Response.json({
message: 'Already on the waitlist!',
position: existing.position,
referralCode: existing.referralCode,
});
}
// Find referrer if code provided:
let referredById: string | undefined;
if (referralCode) {
const referrer = await db.waitlistEntry.findUnique({
where: { referralCode },
});
if (referrer) {
referredById = referrer.id;
}
}
// Get next position:
const count = await db.waitlistEntry.count();
// Create entry:
const entry = await db.waitlistEntry.create({
data: {
email,
name,
position: count + 1,
referralCode: nanoid(8), // 8-char unique code: "aB3xK9mZ"
referredById,
},
});
// Bump referrer's position if applicable:
if (referredById) {
await promoteReferrer(referredById);
}
// Send welcome email with referral link:
await sendWelcomeEmail({
email,
name,
position: entry.position,
referralCode: entry.referralCode,
referralLink: `${process.env.NEXT_PUBLIC_APP_URL}?ref=${entry.referralCode}`,
});
return Response.json({
success: true,
position: entry.position,
referralCode: entry.referralCode,
});
}
// Promote referrer when they get a new referral:
async function promoteReferrer(referrerId: string) {
const referrer = await db.waitlistEntry.findUnique({
where: { id: referrerId },
include: { _count: { select: { referrals: true } } },
});
if (!referrer) return;
const referralCount = referrer._count.referrals + 1;
// Move up 5 positions per referral:
const positionsToGain = 5;
const newPosition = Math.max(1, referrer.position - positionsToGain);
// Update referrer:
await db.waitlistEntry.update({
where: { id: referrerId },
data: {
position: newPosition,
referralCount,
},
});
}
Email Templates
// lib/email/waitlist.tsx — using React Email:
import { Html, Head, Body, Container, Text, Link, Button } from '@react-email/components';
interface WelcomeEmailProps {
name?: string;
position: number;
referralLink: string;
referralCode: string;
}
export function WelcomeEmail({ name, position, referralLink, referralCode }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'sans-serif', backgroundColor: '#f9fafb' }}>
<Container style={{ maxWidth: '600px', margin: '40px auto', padding: '24px', backgroundColor: '#fff' }}>
<Text style={{ fontSize: '24px', fontWeight: 'bold' }}>
You're on the list! 🎉
</Text>
<Text>
{name ? `Hey ${name}!` : 'Hey!'} You're <strong>#{position}</strong> on the waitlist.
</Text>
<Text>
Move up faster by sharing your referral link. Each friend who signs up moves you up 5 spots.
</Text>
<Button
href={referralLink}
style={{
backgroundColor: '#000',
color: '#fff',
padding: '12px 24px',
borderRadius: '8px',
display: 'inline-block',
}}
>
Share Your Link
</Button>
<Text style={{ color: '#6b7280', fontSize: '14px' }}>
Your referral code: <code>{referralCode}</code>
</Text>
<Text style={{ color: '#6b7280', fontSize: '14px' }}>
{referralLink}
</Text>
</Container>
</Body>
</Html>
);
}
// Send via Resend:
// lib/email/index.ts
import { Resend } from 'resend';
import { WelcomeEmail } from './waitlist';
import { render } from '@react-email/render';
const resend = new Resend(process.env.RESEND_API_KEY!);
export async function sendWelcomeEmail({
email, name, position, referralCode, referralLink,
}: {
email: string;
name?: string;
position: number;
referralCode: string;
referralLink: string;
}) {
await resend.emails.send({
from: 'Your App <hello@yourdomain.com>',
to: email,
subject: `You're #${position} on the waitlist`,
html: render(<WelcomeEmail name={name} position={position} referralCode={referralCode} referralLink={referralLink} />),
});
}
The Waitlist Page Component
// app/waitlist/page.tsx — landing page form:
'use client';
import { useState } from 'react';
export default function WaitlistPage() {
const [email, setEmail] = useState('');
const [name, setName] = useState('');
const [result, setResult] = useState<{
position?: number;
referralCode?: string;
error?: string;
} | null>(null);
const [loading, setLoading] = useState(false);
// Check for referral code in URL:
const ref = typeof window !== 'undefined'
? new URLSearchParams(window.location.search).get('ref')
: null;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
const res = await fetch('/api/waitlist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, name, referralCode: ref }),
});
const data = await res.json();
setResult(data);
setLoading(false);
};
if (result?.position) {
const referralLink = `${window.location.origin}?ref=${result.referralCode}`;
return (
<div className="text-center p-8">
<h2 className="text-2xl font-bold">You're #{result.position}! 🎉</h2>
<p className="mt-2 text-gray-600">Share to move up the list:</p>
<div className="mt-4 flex gap-2">
<input
value={referralLink}
readOnly
className="flex-1 rounded border p-2 text-sm"
/>
<button
onClick={() => navigator.clipboard.writeText(referralLink)}
className="rounded bg-black text-white px-4 py-2"
>
Copy
</button>
</div>
<p className="mt-4 text-sm text-gray-500">
Each friend who signs up moves you up 5 spots.
</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-8 space-y-4">
<h1 className="text-3xl font-bold">Join the waitlist</h1>
<p className="text-gray-600">Be first when we launch. Share to move up the queue.</p>
{ref && (
<p className="text-sm text-green-600">✓ Referred by a friend — you'll both benefit!</p>
)}
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your name (optional)"
className="w-full rounded border p-3"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
required
className="w-full rounded border p-3"
/>
<button
type="submit"
disabled={loading}
className="w-full rounded bg-black text-white p-3 font-semibold"
>
{loading ? 'Joining...' : 'Join the waitlist →'}
</button>
</form>
);
}
Option 3: ReferralHero (Managed Service)
Time: ~1 hour Cost: $79/month (Pro) Best for: when you want advanced analytics, A/B testing, and fraud detection
ReferralHero handles the referral mechanics and you embed a widget:
<!-- Embed on landing page: -->
<script src="https://app.referralhero.com/sdk.js" data-id="your-campaign-id"></script>
<div id="rhwidget-ot" class="ReferralHero"></div>
// Webhook when user completes a referral action:
// app/api/webhooks/referralhero/route.ts
export async function POST(req: Request) {
const { event, referrer_email, new_user_email } = await req.json();
if (event === 'referral_converted') {
// Grant reward to referrer:
await grantReferralReward(referrer_email);
}
return new Response('OK');
}
ReferralHero vs custom:
- Custom: free, full control, your data, 2-4 hours to build
- ReferralHero: $79/month, fraud detection, A/B test rewards, analytics dashboard
For early-stage (<1,000 signups), custom is fine. For a serious launch or marketing campaign, ReferralHero's fraud detection is worth paying for — fake accounts inflating referral counts is a real problem.
Converting Waitlist → Users
When you're ready to open access:
// Admin endpoint — grant access in waves:
// app/api/admin/waitlist/release/route.ts
export async function POST(req: Request) {
const { count, prioritizeReferrers } = await req.json();
const entries = await db.waitlistEntry.findMany({
where: { earlyAccess: false },
orderBy: prioritizeReferrers
? [{ referralCount: 'desc' }, { position: 'asc' }] // Referrers first
: { position: 'asc' }, // Pure queue order
take: count,
});
// Grant access:
await db.waitlistEntry.updateMany({
where: { id: { in: entries.map((e) => e.id) } },
data: { earlyAccess: true, accessGrantedAt: new Date() },
});
// Send access emails:
for (const entry of entries) {
await sendEarlyAccessEmail(entry.email, entry.name);
}
return Response.json({ granted: entries.length });
}
Wave strategy:
- Wave 1 (100 users): top referrers — most engaged, best word-of-mouth
- Wave 2 (500 users): next in queue — keep momentum
- Wave 3 (full open): announce publicly with social proof from waves 1-2
Quick Add: Boilerplate-Specific Integration
ShipFast:
- Add WaitlistEntry model to schema (MongoDB or Prisma)
- Create /api/waitlist route
- Replace ShipFast landing page with waitlist form
- Use Resend (pre-configured in ShipFast)
Makerkit:
- Add WaitlistEntry model to Prisma schema
- Create Supabase-auth-free endpoint (public, no session required)
- Add /waitlist page outside the auth middleware
- Makerkit uses Resend — wire it up to sendWelcomeEmail
Supastarter:
- Add WaitlistEntry to Supabase schema (raw SQL migration)
- Create public API route (outside Supabase auth check)
- Use Resend (pre-configured)
T3 Stack:
- Add Prisma schema additions above
- Create /api/waitlist (no auth requirement on this route)
- Use Resend or any email package
Find SaaS boilerplates with launch tools built in at StarterPick.