Skip to main content

Add a Waitlist and Launch Page to Your Boilerplate 2026

·StarterPick Team
waitlistlaunchmarketingnextjsguide2026

TL;DR

A waitlist page buys you time to build while growing demand. The minimum viable waitlist is: email form → confirmation email → simple admin view. Add referral mechanics later if you want viral growth. Total setup: 1 day. This guide uses Resend for emails and your existing database — no external tools required.


Database Schema

// prisma/schema.prisma
model WaitlistEntry {
  id             String   @id @default(cuid())
  email          String   @unique
  name           String?
  referredBy     String?  // WaitlistEntry.id of referrer
  referralCode   String   @unique @default(cuid())
  referralCount  Int      @default(0)
  position       Int      // Calculated based on referrals
  source         String?  // 'twitter', 'ph', 'direct'
  confirmed      Boolean  @default(false)
  invitedAt      DateTime?
  createdAt      DateTime @default(now())
}

Waitlist Signup API

// app/api/waitlist/route.ts
import { prisma } from '@/lib/prisma';
import { sendWaitlistConfirmation } from '@/lib/email';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  name: z.string().optional(),
  referralCode: z.string().optional(),
  source: z.string().optional(),
});

export async function POST(req: Request) {
  const body = await req.json();
  const { email, name, referralCode, source } = schema.parse(body);

  // Check existing
  const existing = await prisma.waitlistEntry.findUnique({ where: { email } });
  if (existing) {
    return Response.json({
      success: true,
      position: existing.position,
      referralCode: existing.referralCode,
      message: "You're already on the waitlist!",
    });
  }

  // Find referrer
  let referrer = null;
  if (referralCode) {
    referrer = await prisma.waitlistEntry.findUnique({
      where: { referralCode },
    });
  }

  // Get current count for position
  const count = await prisma.waitlistEntry.count();

  // Create entry
  const entry = await prisma.waitlistEntry.create({
    data: {
      email,
      name,
      referredBy: referrer?.id,
      position: count + 1,
      source,
    },
  });

  // Bump referrer's count and position
  if (referrer) {
    await prisma.waitlistEntry.update({
      where: { id: referrer.id },
      data: {
        referralCount: { increment: 1 },
        position: { decrement: 5 }, // Move up 5 spots per referral
      },
    });
  }

  // Send confirmation email
  await sendWaitlistConfirmation(entry);

  return Response.json({
    success: true,
    position: entry.position,
    referralCode: entry.referralCode,
  });
}

Waitlist Landing Page

// app/page.tsx — pre-launch landing page
'use client';
import { useState } from 'react';

export default function WaitlistPage() {
  const [email, setEmail] = useState('');
  const [submitted, setSubmitted] = useState(false);
  const [position, setPosition] = useState<number | null>(null);
  const [referralCode, setReferralCode] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);

    // Get referral code from URL if present
    const urlCode = new URLSearchParams(window.location.search).get('ref');

    const res = await fetch('/api/waitlist', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, referralCode: urlCode }),
    });
    const data = await res.json();

    setPosition(data.position);
    setReferralCode(data.referralCode);
    setSubmitted(true);
    setLoading(false);
  }

  if (submitted && position && referralCode) {
    const referralUrl = `${window.location.origin}?ref=${referralCode}`;
    return (
      <div className="max-w-md mx-auto text-center py-20">
        <h2 className="text-2xl font-bold mb-2">You're #Waitlist{position}! 🎉</h2>
        <p className="text-gray-600 mb-6">
          Share your link to move up the waitlist — each referral bumps you 5 spots.
        </p>
        <div className="bg-gray-50 rounded-lg p-4 mb-4">
          <p className="text-sm text-gray-500 mb-2">Your referral link</p>
          <p className="font-mono text-sm break-all">{referralUrl}</p>
        </div>
        <button
          onClick={() => navigator.clipboard.writeText(referralUrl)}
          className="bg-indigo-600 text-white px-6 py-2 rounded-lg"
        >
          Copy Link
        </button>
      </div>
    );
  }

  return (
    <div className="max-w-2xl mx-auto text-center py-20 px-4">
      <div className="inline-block bg-indigo-100 text-indigo-700 text-sm px-3 py-1 rounded-full mb-4">
        Coming soon
      </div>
      <h1 className="text-5xl font-bold tracking-tight mb-4">
        [Your SaaS Tagline Here]
      </h1>
      <p className="text-xl text-gray-500 mb-8">
        [One sentence describing your product's core value.]
      </p>
      <form onSubmit={handleSubmit} className="flex gap-2 max-w-sm mx-auto">
        <input
          type="email"
          value={email}
          onChange={e => setEmail(e.target.value)}
          placeholder="you@example.com"
          required
          className="flex-1 border border-gray-300 rounded-lg px-4 py-2"
        />
        <button
          type="submit"
          disabled={loading}
          className="bg-indigo-600 text-white px-6 py-2 rounded-lg"
        >
          {loading ? '...' : 'Join Waitlist'}
        </button>
      </form>
      <p className="text-gray-400 text-sm mt-3">
        Join 2,400+ people waiting for early access.
      </p>
    </div>
  );
}

Confirmation Email

// emails/WaitlistConfirmationEmail.tsx
export function WaitlistConfirmationEmail({
  name,
  position,
  referralUrl,
}: {
  name?: string;
  position: number;
  referralUrl: string;
}) {
  return (
    <EmailLayout preview={`You're #${position} on the waitlist!`}>
      <Heading>You're on the list! 🎉</Heading>
      <Text>Hi {name ?? 'there'},</Text>
      <Text>
        You're <strong>#{position}</strong> on the waitlist. We'll send you early access
        when we're ready to launch.
      </Text>
      <Text>
        Want to move up? Share your referral link — each signup bumps you 5 spots:
      </Text>
      <Button href={referralUrl}>Share Your Link →</Button>
    </EmailLayout>
  );
}

Admin: Invite Users from Waitlist

// app/admin/waitlist/actions.ts
export async function inviteFromWaitlist(entryId: string) {
  const entry = await prisma.waitlistEntry.findUnique({
    where: { id: entryId },
  });
  if (!entry) throw new Error('Entry not found');

  // Create user account
  const user = await prisma.user.create({
    data: {
      email: entry.email,
      name: entry.name,
    },
  });

  // Send magic link or temporary password
  await sendInvitationEmail(user.email, entry.name);

  // Mark as invited
  await prisma.waitlistEntry.update({
    where: { id: entryId },
    data: { invitedAt: new Date() },
  });
}

Option 2: Managed Waitlist Tools

If you don't want to build it:

ToolCostFeatures
Waitlist.emailFree → $29/moReferral system, embeddable
Tally.soFree → $29/moForm builder with waitlist mode
MailchimpFree → $13/moBasic email collection
Loops.soFree → $49/moEmail + waitlist + sequences

For most solo founders, Tally + Loops is fastest and free to start.


Converting Waitlist → Paid

When you're ready to launch:

  1. Segment by referrals — top referrers become your initial power users and advocates
  2. Create urgency — "First 100 users get 50% off forever"
  3. Announce with specifics — exact launch date, not "soon"
  4. Give early access in batches — 50 users per wave prevents support overload
  5. Offer Lifetime Deal — waitlist-only pricing builds goodwill

Find boilerplates with waitlist features built-in on StarterPick.

Check out this boilerplate

View ShipFast on StarterPick →

Comments