Skip to main content

Add a Waitlist and Launch Page to Your Boilerplate 2026

·StarterPick Team
Share:

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.


Why Waitlists Work for SaaS Products

A waitlist serves two distinct purposes that most founders conflate: demand validation and demand generation. They're different problems with different success metrics.

Demand validation: If nobody signs up for your waitlist after you share it in relevant communities, that's valuable signal. You learn early — before building — that the market may not exist or the messaging isn't resonating. 50-200 organic signups from a well-targeted community post suggests genuine interest. Zero signups after repeated attempts is a warning sign worth heeding before investing 6 months of development.

Demand generation: For products targeting a specific niche, a waitlist creates exclusivity. "Apply for early access" sounds more deliberate than "sign up for free." The artificial scarcity of a waitlist can actually increase the perceived value of the product — people want what others want. Referral mechanics amplify this: a user with 10 referrals has strong social proof that their network validated your product.

The practical outcome: a well-executed waitlist converts 5-15% of signups to paying customers within 30 days of launch. Compare this to typical cold traffic conversion rates of 0.5-3%. The waitlist list self-selects for the most motivated potential customers.


Pre-Launch Page Design

The waitlist page is your first product impression. The elements that matter:

Above the fold: A single, specific headline explaining what the product does and who it's for. Avoid generic SaaS copy ("The all-in-one platform for everything"). Be specific: "The fastest way for freelance designers to collect client feedback on Figma files." Specific beats generic every time for waitlist conversion.

Social proof signals: The counter ("Join 2,400+ people waiting") works because of social proof, not the specific number. Even 50 signups is worth showing as "Join 50 early adopters." The number signals that real people find this interesting.

Expected timeline: "Early access opens April 2026" gives signups a reason to remember you exist. Vague "coming soon" pages get forgotten. A specific date (even if it slips) creates a mental anchor.

What they're getting: One paragraph describing exactly what early access means. Will they get all features free? A discount? Priority support? Being specific about the early adopter benefit increases conversion.

Form simplicity: Email field only, submit button. Name is optional. Every additional field reduces conversion. You can collect more information after they sign up.


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 #{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() },
  });
}

Referral Mechanics Psychology

The reason referral mechanics work on waitlists is that they convert a passive action (joining a list) into an active, socially visible one (sharing with your network). The referrer has an incentive (better position) aligned with the product's interest (more signups).

The "move up X spots per referral" mechanic works better than "skip the line entirely" because it creates visible progress. Position dropping from 842 to 837 feels concrete. "You're now in the top 10%" feels abstract. Show the number.

For B2B products targeting professionals, LinkedIn and Slack community sharing outperforms Twitter for waitlist growth. A single post in a relevant Slack workspace with 5,000 members can drive 100+ signups in 24 hours if the product is well-targeted. The referral link lets you attribute these community-driven signups correctly.

Track sources (source field in the schema above). After 500 signups, you'll see which channels drove the best-converting users. If 40% of your signups came from a specific Twitter thread but only 5% of those convert, vs 10% from a Slack community that converts at 30%, that data shapes your post-launch marketing.


Email Sequence After Signup

The confirmation email is one touchpoint. A waitlist email sequence keeps your product top-of-mind during the weeks or months before launch — and significantly improves conversion when you finally send invites.

A three-email sequence that works well for SaaS waitlists:

Email 1 (immediate — confirmation): "You're #[position] on the list." Include the referral link, a one-sentence re-statement of what the product does, and an expected timeline. Keep it under 150 words. The referral link and position number are the important elements.

Email 2 (1-2 weeks after signup — behind the scenes): "Here's what we're building." Share a screenshot, a short video walkthrough, or a specific feature description. This is not marketing copy — it's a product update. The goal is to remind them why they signed up and generate excitement. Keep it honest: if the product is 60% done, say so. Founders who share honest progress updates build more goodwill than those who always sound fully polished.

Email 3 (launch week — early access announcement): "Your early access is ready." This email should arrive 2-3 days before you open signups broadly. Give waitlist members a head start: early access link, any special pricing or lifetime deal, and a clear deadline ("offer expires Friday"). Urgency combined with exclusivity drives activation.

Implementation with Resend and Next.js:

// lib/waitlist-email.ts
import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function sendBehindTheScenesUpdate(entries: WaitlistEntry[]) {
  // Batch send to all unconfirmed + confirmed entries
  await resend.emails.send({
    from: 'founder@yoursaas.com',
    to: entries.map(e => e.email),
    subject: 'Building [Product]: what we've been working on',
    react: BehindTheScenesEmail({ entries }),
  });
}

// Trigger from an admin endpoint or cron job
export async function triggerWaitlistUpdate() {
  const entries = await prisma.waitlistEntry.findMany({
    where: { invitedAt: null }, // Only send to uninvited entries
  });
  await sendBehindTheScenesUpdate(entries);
}

The sequence converts because it maintains a relationship between signup intent and actual use. Waitlist signups are warm leads who have already expressed interest — treat them like early customers, not just email addresses.


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 to 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

The batch release approach works because it creates a second wave of social proof. Each batch of invites gets announced publicly: "Wave 2 now open — join the 200 people already using [Product]." Early adopters who are having a good experience share organically. By wave 3-4, you have testimonials to quote.

Personally email your top 20 referrers before the general launch. These are your most engaged potential customers. A personal email explaining that you're giving them early access as a thank-you for their referrals costs 30 minutes and converts at dramatically higher rates than the mass announcement.


Waitlist Position Leaderboard

For consumer-facing products with viral growth goals, a public leaderboard of top referrers creates additional social incentive. Displaying the top 10 referrers by name (with their permission) signals that real people are engaged and competing for position. This works particularly well for products targeting developer communities, where public recognition has social currency.

The leaderboard query:

// Get top referrers for public display
export async function getTopReferrers(limit = 10) {
  return prisma.waitlistEntry.findMany({
    where: { referralCount: { gt: 0 } },
    orderBy: { referralCount: 'desc' },
    take: limit,
    select: {
      name: true,
      referralCount: true,
      position: true,
      // Never expose email in public display
    },
  });
}

Only include this if your waitlist audience is the type to engage with public competition. For B2B enterprise-focused products, the leaderboard can feel out of place — a quiet referral link is more appropriate than a public contest.


Analytics and Source Tracking

When your waitlist reaches several hundred signups, knowing where they came from becomes strategically important. A single viral tweet can drive more signups than three months of blog posts. Knowing that changes where you spend time.

The source field in the schema handles UTM-based attribution. Capture it from URL parameters on landing:

// Capture source from URL params
const source =
  new URLSearchParams(window.location.search).get('utm_source') ??
  new URLSearchParams(window.location.search).get('source') ??
  document.referrer ? new URL(document.referrer).hostname : 'direct';

With source tracking in place, your admin dashboard can show a simple breakdown: signups from Twitter, Product Hunt, Hacker News, direct traffic, and referral links. After 500 signups, look at conversion-to-paid rates by source — not just total signup counts. A community that drives 50 signups with 40% conversion beats a viral tweet that drove 500 signups with 2% conversion.

The referral source field also lets you reward top-performing channels: a community where 200 members signed up from a single thread deserves a thank-you post or an early access code for that community's audience.


For a full blog and SEO setup to go alongside your waitlist landing page, how to add a blog for SEO to your SaaS boilerplate covers the complete content marketing stack. For converting waitlist signups to paying customers with a Stripe checkout, best boilerplates for micro-SaaS covers the full one-click payment flow. For analytics to track where your waitlist signups originate, SEO in SaaS boilerplates covers UTM tracking and source attribution.


Methodology

Implementation patterns based on standard Next.js API routes and Resend email documentation. Referral mechanics based on Viral Loops and similar services' public playbooks. Conversion benchmarks sourced from Indie Hackers community discussion on waitlist-to-paid conversion rates.

Find boilerplates with waitlist features built-in on StarterPick.

Check out this boilerplate

View ShipFaston StarterPick →

The SaaS Boilerplate Matrix (Free PDF)

20+ SaaS starters compared: pricing, tech stack, auth, payments, and what you actually ship with. Updated monthly. Used by 150+ founders.

Join 150+ SaaS founders. Unsubscribe in one click.