Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

Pre-launch waitlist in Next.js App Router -- collecting early signups and tracking referrals with Drizzle ORM

June 28, 2026
nextjsdrizzle-ormsaasresend

Every founder needs a waitlist page before launch. The faster you can put up a signup form, collect emails, and start measuring interest, the less time you waste building something nobody wants.

This post shows you how to build one -- a Drizzle ORM schema that tracks signups and referral chains, a position counter, a unique share link for every signup, and a confirmation email via Resend -- wired into a Next.js App Router SaaS.

The schema

Two columns do the heavy lifting: referralCode (each user's unique share link) and referredBy (the code that brought them in).

// modules/waitlist/waitlist.schema.ts
import { pgTable, text, timestamp, integer } from 'drizzle-orm/pg-core'
import { createId } from '@paralleldrive/cuid2'
 
export const waitlistTable = pgTable('waitlist', {
  id: text('id').primaryKey().$defaultFn(() => createId()),
  email: text('email').notNull().unique(),
  referralCode: text('referral_code').notNull().unique().$defaultFn(() => createId()),
  referredBy: text('referred_by'),
  position: integer('position').notNull(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
})

No separate referrals table. Self-referencing on referralCode keeps queries simple and migrations minimal.

The service

The service assigns position (count + 1), inserts the entry, and sends the confirmation email with the share link baked in.

// modules/waitlist/waitlist.service.ts
async function join(email: string, referredBy?: string): Promise<WaitlistEntry> {
  const count = await waitlistRepo.count()
  const entry = await waitlistRepo.create({ email, referredBy, position: count + 1 })
  await emailService.sendEmail({
    to: email,
    subject: 'You are on the list',
    react: React.createElement(WaitlistEmail, {
      appName: 'YourApp',
      position: entry.position,
      referralLink: `${process.env.NEXT_PUBLIC_BASE_URL}/waitlist?ref=${entry.referralCode}`,
    }),
  })
  return entry
}

Position races are unlikely at early launch scale, but if you need strict ordering, wrap the count and insert in a Drizzle transaction.

The API route

// app/api/waitlist/route.ts
export async function POST(req: Request) {
  const body = await req.json()
  const result = joinSchema.safeParse(body)
  if (!result.success) return handleError(new HttpError(400, 'Invalid input'))
  try {
    const entry = await waitlistService.join(result.data.email, result.data.referredBy)
    return Response.json(
      { position: entry.position, referralCode: entry.referralCode },
      { status: 201 }
    )
  } catch (error: unknown) {
    return handleError(error)
  }
}

Thin route, all logic in the service. The referral code comes back in the response so the UI can display the share link immediately after signup.

The waitlist page

A server component reads the ref search param and passes it to the client form.

// app/(main)/waitlist/page.tsx
export default function WaitlistPage({
  searchParams,
}: {
  searchParams: { ref?: string }
}) {
  return <WaitlistForm referredBy={searchParams.ref} />
}

The form stores referredBy in a hidden field. After submission, swap the form for a success state showing the user's position and their personal share link. That link is the flywheel -- every person who signs up through it bumps the original referrer up the list.

Optional: referral leaderboard

If you want to create urgency, show who is sending the most referrals.

// modules/waitlist/waitlist.repo.ts
async function getTopReferrers(limit = 10) {
  return db.execute(sql`
    SELECT referred_by, COUNT(*) AS referral_count
    FROM waitlist
    WHERE referred_by IS NOT NULL
    GROUP BY referred_by
    ORDER BY referral_count DESC
    LIMIT ${limit}
  `)
}

No extra tables, no extra migrations -- just a query on the data you already have.

What you get

  • A unique share link for every person on the waitlist
  • Position tracking from the very first signup
  • A confirmation email with the share link already in it
  • An optional leaderboard to drive word-of-mouth before you launch

This is the kind of feature that takes a full day to build from scratch -- schema design, email setup, referral tracking, the whole loop. With Claude Code Boilerplate, the email infrastructure, database client, and API conventions are already in place. You add the schema, write the service, and ship.

Clone the repo, add the waitlist module, and go live before your next competitor does -- start here.