Back to Blog

SaaS Waitlist in 2026: Invite Codes, Referral Tracking, Drip Emails, and Launch Conversion

Build a SaaS pre-launch waitlist: invite code generation, referral position tracking, automated drip email sequences, viral share mechanics, and conversion to paid subscription.

Viprasol Tech Team
February 16, 2027
13 min read

SaaS Waitlist in 2026: Invite Codes, Referral Tracking, Drip Emails, and Launch Conversion

A well-built waitlist does four things: captures demand before you launch, creates social proof through referral mechanics, warms leads with automated email sequences, and converts signups to paid customers at launch. A poorly built one is just a spreadsheet with an email field.

This post builds the complete waitlist system: signup with referral tracking, invite code generation, position calculation with referral bumps, drip email sequences via Resend, and the conversion flow when you open early access.


Database Schema

CREATE TABLE waitlist_entries (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email           TEXT NOT NULL UNIQUE,
  name            TEXT,
  referral_code   TEXT NOT NULL UNIQUE,   -- Their shareable code
  referred_by     UUID REFERENCES waitlist_entries(id),
  referral_count  INTEGER NOT NULL DEFAULT 0,
  position        INTEGER,                -- Calculated, not stored raw
  status          TEXT NOT NULL DEFAULT 'waiting'
                  CHECK (status IN ('waiting', 'invited', 'converted', 'unsubscribed')),
  invited_at      TIMESTAMPTZ,
  converted_at    TIMESTAMPTZ,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  ip_address      INET,
  source          TEXT,                   -- 'twitter', 'hn', 'direct', etc.
  utm_source      TEXT,
  utm_campaign    TEXT
);

CREATE INDEX idx_waitlist_referral_code ON waitlist_entries(referral_code);
CREATE INDEX idx_waitlist_referred_by   ON waitlist_entries(referred_by);
CREATE INDEX idx_waitlist_status_created ON waitlist_entries(status, created_at);

-- Drip email tracking
CREATE TABLE waitlist_emails (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  entry_id    UUID NOT NULL REFERENCES waitlist_entries(id),
  sequence    INTEGER NOT NULL,           -- 1, 2, 3... (which email in drip)
  template    TEXT NOT NULL,
  sent_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
  opened_at   TIMESTAMPTZ,
  clicked_at  TIMESTAMPTZ,
  resend_id   TEXT                        -- Resend message ID
);

CREATE UNIQUE INDEX idx_waitlist_emails_entry_seq
  ON waitlist_emails(entry_id, sequence);

Waitlist Signup API

// app/api/waitlist/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { db } from "@/lib/db";
import { generateReferralCode } from "@/lib/waitlist/codes";
import { sendWelcomeEmail } from "@/lib/waitlist/emails";
import { rateLimit } from "@/lib/rate-limit";

const SignupSchema = z.object({
  email: z.string().email().toLowerCase().trim(),
  name: z.string().max(100).optional(),
  referralCode: z.string().optional(),    // ?ref=ABC123 from URL
  source: z.string().max(50).optional(),
  utmSource: z.string().max(100).optional(),
  utmCampaign: z.string().max(100).optional(),
});

export async function POST(req: NextRequest) {
  // Rate limit: 3 signups per IP per hour
  const ip = req.headers.get("x-forwarded-for")?.split(",")[0] ?? "unknown";
  const limited = await rateLimit(`waitlist:${ip}`, 3, 3600);
  if (limited) {
    return NextResponse.json({ error: "Too many requests" }, { status: 429 });
  }

  const body = await req.json().catch(() => ({}));
  const parsed = SignupSchema.safeParse(body);
  if (!parsed.success) {
    return NextResponse.json({ error: "Invalid input" }, { status: 400 });
  }

  const { email, name, referralCode, source, utmSource, utmCampaign } = parsed.data;

  // Find referrer
  let referredBy: string | null = null;
  if (referralCode) {
    const referrer = await db.waitlistEntry.findUnique({
      where: { referralCode },
      select: { id: true },
    });
    referredBy = referrer?.id ?? null;
  }

  // Check if already signed up
  const existing = await db.waitlistEntry.findUnique({ where: { email } });
  if (existing) {
    const position = await getPosition(existing.id);
    return NextResponse.json({
      alreadyRegistered: true,
      position,
      referralCode: existing.referralCode,
      referralCount: existing.referralCount,
    });
  }

  // Create entry
  const entry = await db.waitlistEntry.create({
    data: {
      email,
      name,
      referralCode: await generateReferralCode(),
      referredById: referredBy,
      source,
      utmSource,
      utmCampaign,
      ipAddress: ip,
    },
  });

  // Credit referrer
  if (referredBy) {
    await db.waitlistEntry.update({
      where: { id: referredBy },
      data: { referralCount: { increment: 1 } },
    });
  }

  // Send welcome email (non-blocking)
  sendWelcomeEmail(entry).catch(console.error);

  const position = await getPosition(entry.id);

  return NextResponse.json({
    success: true,
    position,
    referralCode: entry.referralCode,
    referralCount: 0,
  }, { status: 201 });
}

// Position = signups before you - (your referral count ร— REFERRAL_BUMP) + 1
// Referrals move you up the list
const REFERRAL_BUMP = 5;

async function getPosition(entryId: string): Promise<number> {
  const result = await db.$queryRaw<[{ position: bigint }]>`
    WITH ranked AS (
      SELECT
        id,
        ROW_NUMBER() OVER (
          ORDER BY
            (referral_count * ${REFERRAL_BUMP}) DESC,
            created_at ASC
        ) AS position
      FROM waitlist_entries
      WHERE status = 'waiting'
    )
    SELECT position FROM ranked WHERE id = ${entryId}::uuid
  `;
  return Number(result[0]?.position ?? 0);
}

๐Ÿš€ SaaS MVP in 8 Weeks โ€” Seriously

We have launched 50+ SaaS platforms. Multi-tenant architecture, Stripe billing, auth, role-based access, and cloud deployment โ€” all handled by one senior team.

  • Week 1โ€“2: Architecture design + wireframes
  • Week 3โ€“6: Core features built + tested
  • Week 7โ€“8: Launch-ready on AWS/Vercel with CI/CD
  • Post-launch: Maintenance plans from month 3

Referral Code Generation

// lib/waitlist/codes.ts
import { customAlphabet } from "nanoid";
import { db } from "@/lib/db";

// 8 character alphanumeric, no confusable characters (0/O, 1/I/l)
const alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
const nanoid = customAlphabet(alphabet, 8);

export async function generateReferralCode(): Promise<string> {
  let code: string;
  let attempts = 0;

  // Retry on collision (extremely rare with 32^8 = 1T possibilities)
  do {
    code = nanoid();
    const exists = await db.waitlistEntry.findUnique({
      where: { referralCode: code },
      select: { id: true },
    });
    if (!exists) return code;
    attempts++;
  } while (attempts < 5);

  throw new Error("Failed to generate unique referral code");
}

Drip Email Sequences with Resend

// lib/waitlist/emails.ts
import { Resend } from "resend";
import { db } from "@/lib/db";
import { WelcomeEmail } from "@/emails/waitlist/WelcomeEmail";
import { NurtureEmail } from "@/emails/waitlist/NurtureEmail";

const resend = new Resend(process.env.RESEND_API_KEY!);
const FROM = "Viprasol <hello@viprasol.com>";

// Sequence definition
const DRIP_SEQUENCE = [
  { sequence: 1, template: "welcome",   delayDays: 0 },
  { sequence: 2, template: "nurture-1", delayDays: 3 },
  { sequence: 3, template: "nurture-2", delayDays: 7 },
  { sequence: 4, template: "nurture-3", delayDays: 14 },
  { sequence: 5, template: "reminder",  delayDays: 30 },
] as const;

export async function sendWelcomeEmail(entry: {
  id: string;
  email: string;
  name: string | null;
  referralCode: string;
}) {
  const position = await getPosition(entry.id);

  const { data, error } = await resend.emails.send({
    from: FROM,
    to: entry.email,
    subject: `You're #${position} on the Viprasol waitlist`,
    react: WelcomeEmail({
      name: entry.name ?? "there",
      position,
      referralCode: entry.referralCode,
      referralLink: `https://viprasol.com/?ref=${entry.referralCode}`,
      referralBump: 5,
    }),
  });

  if (error) throw new Error(`Resend error: ${error.message}`);

  await db.waitlistEmail.create({
    data: {
      entryId: entry.id,
      sequence: 1,
      template: "welcome",
      resendId: data!.id,
    },
  });
}

// Cron job: send scheduled drip emails
export async function sendScheduledDripEmails() {
  for (const step of DRIP_SEQUENCE.slice(1)) { // Skip welcome (sent immediately)
    const eligibleEntries = await db.$queryRaw<{
      id: string;
      email: string;
      name: string | null;
      referral_code: string;
      referral_count: number;
    }[]>`
      SELECT e.id, e.email, e.name, e.referral_code, e.referral_count
      FROM waitlist_entries e
      WHERE e.status = 'waiting'
        AND e.created_at <= now() - INTERVAL '${step.delayDays} days'
        AND NOT EXISTS (
          SELECT 1 FROM waitlist_emails em
          WHERE em.entry_id = e.id AND em.sequence = ${step.sequence}
        )
      LIMIT 100  -- Batch size to avoid rate limits
    `;

    for (const entry of eligibleEntries) {
      await sendNurtureEmail(entry, step).catch((err) =>
        console.error(`Failed to send drip ${step.sequence} to ${entry.email}:`, err)
      );
      // Small delay to respect Resend rate limits
      await new Promise((r) => setTimeout(r, 100));
    }
  }
}

async function sendNurtureEmail(
  entry: { id: string; email: string; name: string | null; referral_code: string; referral_count: number },
  step: { sequence: number; template: string }
) {
  const position = await getPosition(entry.id);

  const { data, error } = await resend.emails.send({
    from: FROM,
    to: entry.email,
    subject: getNurtureSubject(step.template, position),
    react: NurtureEmail({
      name: entry.name ?? "there",
      position,
      referralCode: entry.referral_code,
      referralCount: entry.referral_count,
      template: step.template,
    }),
  });

  if (error) throw new Error(error.message);

  await db.waitlistEmail.create({
    data: {
      entryId: entry.id,
      sequence: step.sequence,
      template: step.template,
      resendId: data!.id,
    },
  });
}

function getNurtureSubject(template: string, position: number): string {
  const subjects: Record<string, string> = {
    "nurture-1": `You're still #${position} โ€” here's what's coming`,
    "nurture-2": "3 features our beta users love most",
    "nurture-3": `Move up the list โ€” you have ${5} spots to gain`,
    "reminder":  "We haven't forgotten you",
  };
  return subjects[template] ?? "Update from Viprasol";
}

async function getPosition(entryId: string): Promise<number> {
  const result = await db.$queryRaw<[{ position: bigint }]>`
    WITH ranked AS (
      SELECT id, ROW_NUMBER() OVER (ORDER BY (referral_count * 5) DESC, created_at ASC) AS position
      FROM waitlist_entries WHERE status = 'waiting'
    )
    SELECT position FROM ranked WHERE id = ${entryId}::uuid
  `;
  return Number(result[0]?.position ?? 1);
}

๐Ÿ’ก The Difference Between a SaaS Demo and a SaaS Business

Anyone can build a demo. We build SaaS products that handle real load, real users, and real payments โ€” with architecture that does not need to be rewritten at 1,000 users.

  • Multi-tenant PostgreSQL with row-level security
  • Stripe subscriptions, usage billing, annual plans
  • SOC2-ready infrastructure from day one
  • We own zero equity โ€” you own everything

Waitlist Landing Page Component

// components/Waitlist/WaitlistSignupForm.tsx
"use client";

import { useState } from "react";
import { useSearchParams } from "next/navigation";

export function WaitlistSignupForm() {
  const searchParams = useSearchParams();
  const refCode = searchParams.get("ref") ?? undefined;

  const [email, setEmail] = useState("");
  const [name, setName] = useState("");
  const [state, setState] = useState<
    | { status: "idle" }
    | { status: "loading" }
    | { status: "success"; position: number; referralCode: string; referralCount: number; alreadyRegistered?: boolean }
    | { status: "error"; message: string }
  >({ status: "idle" });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setState({ status: "loading" });

    try {
      const res = await fetch("/api/waitlist", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          email,
          name,
          referralCode: refCode,
          source: document.referrer ? "referral" : "direct",
        }),
      });
      const data = await res.json();

      if (!res.ok) throw new Error(data.error ?? "Something went wrong");

      setState({
        status: "success",
        position: data.position,
        referralCode: data.referralCode,
        referralCount: data.referralCount,
        alreadyRegistered: data.alreadyRegistered,
      });
    } catch (err) {
      setState({ status: "error", message: err instanceof Error ? err.message : "Error" });
    }
  };

  if (state.status === "success") {
    const shareUrl = `https://viprasol.com/?ref=${state.referralCode}`;
    return (
      <div className="text-center space-y-4">
        <div className="inline-flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mx-auto">
          <span className="text-3xl">๐ŸŽ‰</span>
        </div>
        <h3 className="text-xl font-bold">
          {state.alreadyRegistered ? "You're already on the list!" : "You're on the list!"}
        </h3>
        <p className="text-gray-600">
          You're currently <strong>#{state.position}</strong> in line.
          {state.referralCount > 0 && ` Your ${state.referralCount} referral(s) moved you up!`}
        </p>

        <div className="bg-gray-50 rounded-lg p-4">
          <p className="text-sm text-gray-600 mb-2">
            Share your link to move up โ€” each referral moves you up 5 spots:
          </p>
          <div className="flex gap-2">
            <input
              readOnly
              value={shareUrl}
              className="flex-1 text-sm border border-gray-200 rounded px-3 py-2 bg-white font-mono text-xs"
            />
            <button
              onClick={() => navigator.clipboard.writeText(shareUrl)}
              className="px-3 py-2 bg-blue-600 text-white rounded text-sm"
            >
              Copy
            </button>
          </div>
        </div>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-3">
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Your name (optional)"
        className="w-full border border-gray-200 rounded-lg px-4 py-2.5 text-sm"
      />
      <input
        type="email"
        required
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="your@email.com"
        className="w-full border border-gray-200 rounded-lg px-4 py-2.5 text-sm"
      />
      <button
        type="submit"
        disabled={state.status === "loading"}
        className="w-full py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50"
      >
        {state.status === "loading" ? "Joining..." : "Join the waitlist"}
      </button>
      {state.status === "error" && (
        <p className="text-red-500 text-sm text-center">{state.message}</p>
      )}
      {refCode && (
        <p className="text-xs text-gray-400 text-center">
          Referred by a friend โ€” you'll start higher in the queue.
        </p>
      )}
    </form>
  );
}

Admin: Invite Batch

// app/admin/waitlist/actions.ts
"use server";

import { db } from "@/lib/db";
import { Resend } from "resend";
import { EarlyAccessEmail } from "@/emails/waitlist/EarlyAccessEmail";

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

export async function inviteBatch(count: number = 100) {
  const toInvite = await db.waitlistEntry.findMany({
    where: { status: "waiting" },
    orderBy: [
      { referralCount: "desc" },
      { createdAt: "asc" },
    ],
    take: count,
  });

  for (const entry of toInvite) {
    // Generate one-time access token
    const token = crypto.randomUUID();

    await db.waitlistEntry.update({
      where: { id: entry.id },
      data: {
        status: "invited",
        invitedAt: new Date(),
      },
    });

    await resend.emails.send({
      from: "Viprasol <hello@viprasol.com>",
      to: entry.email,
      subject: "Your early access is ready ๐ŸŽ‰",
      react: EarlyAccessEmail({
        name: entry.name ?? "there",
        accessLink: `https://viprasol.com/signup?token=${token}&email=${encodeURIComponent(entry.email)}`,
        expiresIn: "7 days",
      }),
    });

    await new Promise((r) => setTimeout(r, 100)); // Rate limit
  }

  return { invited: toInvite.length };
}

Cost and Timeline

ComponentTimelineCost (USD)
Signup API + referral tracking1โ€“2 days$800โ€“$1,600
Position calculation0.5 day$300โ€“$500
Drip email sequences1โ€“2 days$800โ€“$1,600
Landing page form component0.5โ€“1 day$400โ€“$800
Admin invite batch0.5 day$300โ€“$500
Full waitlist system1โ€“2 weeks$5,000โ€“$10,000

See Also


Working With Viprasol

We build pre-launch waitlist systems that convert interest into paying customers. Our team has shipped waitlists with viral referral mechanics, automated drip sequences, and admin invite workflows for SaaS products across B2B and consumer markets.

What we deliver:

  • Signup API with referral code tracking and rate limiting
  • Referral-boosted position algorithm
  • Resend drip email sequence (welcome โ†’ nurture โ†’ reminder)
  • Share mechanics with copy-to-clipboard referral links
  • Admin batch invite with access token generation

Explore our SaaS development services or contact us to launch your waitlist.

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Building a SaaS Product?

We've helped launch 50+ SaaS platforms. Let's build yours โ€” fast.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท AI Agent Systems

Add AI automation to your SaaS product?

Viprasol builds custom AI agent crews that plug into any SaaS workflow โ€” automating repetitive tasks, qualifying leads, and responding across every channel your customers use.