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.
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
| Component | Timeline | Cost (USD) |
|---|---|---|
| Signup API + referral tracking | 1โ2 days | $800โ$1,600 |
| Position calculation | 0.5 day | $300โ$500 |
| Drip email sequences | 1โ2 days | $800โ$1,600 |
| Landing page form component | 0.5โ1 day | $400โ$800 |
| Admin invite batch | 0.5 day | $300โ$500 |
| Full waitlist system | 1โ2 weeks | $5,000โ$10,000 |
See Also
- SaaS Email Sequences โ Post-signup drip email automation
- SaaS Referral System โ Full referral program after launch
- SaaS Onboarding Checklist โ Converting waitlist โ active user
- Next.js Middleware Authentication โ Protecting early access routes
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.
About the Author
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.
Building a SaaS Product?
We've helped launch 50+ SaaS platforms. Let's build yours โ fast.
Free consultation โข No commitment โข Response within 24 hours
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.