Skip to main content

Add Waitlist + Viral Referral to Any SaaS Boilerplate

·StarterPick Team
waitlistreferrallaunch-strategysaas-boilerplateindie-hacker

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:

  1. FOMO mechanics — "4,382 people are waiting" creates urgency
  2. Viral sharing — people share to move up the queue
  3. 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:

  1. Wave 1 (100 users): top referrers — most engaged, best word-of-mouth
  2. Wave 2 (500 users): next in queue — keep momentum
  3. 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.

Comments