mcpField
W
waitlist

waitlist

github.com/tushaarmehtaaMarketing & growth1239 installsUpdated 2w ago
Repository

About waitlist

waitlist is a skill for adding waitlist functionality to an existing project, first checking its technology stack and adapting the implementation. It covers signup forms, storage, confirmation emails, referral tracking, and an admin view.

Key features

  1. 01Detect the existing project stack
  2. 02Create signup routes and forms
  3. 03Adapt database storage
  4. 04Send confirmation emails
  5. 05Support referrals and an admin view

Use cases

  1. 01Collect early signups for a product
  2. 02Use the project's existing database
  3. 03Send confirmation emails to registrants
  4. 04Review signups and referral activity

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/tushaarmehtaa/tushar-skills --skill waitlist

After installing, type/waitlistin Claude Code to confirm it loaded.

Details

From the project README

Scaffold a complete waitlist — email capture, storage, confirmation email, and an admin view. Reads the project first, wires into existing stack.

Phase 1: Detect the Project

cat package.json | grep -E "next|supabase|prisma|drizzle|resend"

Check for:

  • Database: Supabase / Prisma / Drizzle / raw Postgres?
  • Email: Resend already installed?
  • Auth: Any auth provider? (Admin view needs it)
  • Existing waitlist table? Search for waitlist in schema files

Phase 2: Ask the User

I'll scaffold a waitlist for [product name].

Quick decisions:

1. What info do you want to collect?
   a) Email only (default — highest conversion)
   b) Email + name
   c) Email + name + "what are you building?" (optional field)

2. Referral tracking?
   a) No — simple list
   b) Yes — each signup gets a unique referral link, referred count tracked

3. Confirmation email?
   a) Yes — send a "you're on the list" email via Resend (default)
   b) No

4. Do you have Resend and a verified sending domain? (yes / need to set up)

Phase 3: Database Schema

Adapt to the detected database. Create a waitlist table:

Supabase / raw SQL:

create table public.waitlist (
  id uuid primary key default gen_random_uuid(),
  email text not null unique,
  name text,
  note text,                              -- optional "what are you building?" field
  referral_code text unique,              -- their shareable code (if referral enabled)
  referred_by text references public.waitlist(referral_code),  -- who sent them
  referral_count integer default 0,       -- how many they've referred
  position integer,                       -- queue position (set by trigger)
  confirmed_at timestamptz,               -- null = unconfirmed
  created_at timestamptz default now()
);

-- Auto-increment position on insert
create or replace function set_waitlist_position()
returns trigger as $$
begin
  new.position = (select coalesce(max(position), 0) + 1 from public.waitlist);
  return new;
end;
$$ language plpgsql;

create trigger waitlist_position_trigger
  before insert on public.waitlist
  for each row execute function set_waitlist_position();

If no referral tracking needed, drop referral_code, referred_by, referral_count.

Prisma:

model Waitlist {
  id           String    @id @default(cuid())
  email        String    @unique
  name         String?
  note         String?
  referralCode String?   @unique
  referredBy   String?
  referralCount Int      @default(0)
  position     Int
  confirmedAt  DateTime?
  createdAt    DateTime  @default(now())
}

Phase 4: The Signup API Route

// app/api/waitlist/route.ts
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const { email, name, note, referredBy } = await req.json();

  // Validate email
  if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    return NextResponse.json({ error: 'Valid email required' }, { status: 400 });
  }

  // Normalize
  const normalizedEmail = email.toLowerCase().trim();

  // Check for duplicate
  const existing = await db.query.waitlist.findFirst({
    where: eq(waitlist.email, normalizedEmail),
  });

  if (existing) {
    // Return success — don't tell spammers which emails are registered
    return NextResponse.json({
      success: true,
      position: existing.position,
      alreadyRegistered: true,
    });
  }

  // Generate referral code if enabled
  const referralCode = generateReferralCode(normalizedEmail);  // e.g., first 8 chars of email hash

  // Insert
  const entry = await db.insert(waitlistTable).values({
    email: normalizedEmail,
    name: name?.trim() || null,
    note: note?.trim() || null,
    referralCode,
    referredBy: referredBy || null,
  }).returning();

  // Increment referrer's count
  if (referredBy) {
    await db.update(waitlistTable)
      .set({ referralCount: sql`referral_count + 1` })
      .where(eq(waitlistTable.referralCode, referredBy));
  }

  // Send confirmation email (if Resend is set up)
  if (process.env.RESEND_API_KEY) {
    await sendWaitlistConfirmation({
      email: normalizedEmail,
      name: name || 'there',
      position: entry[0].position,
      referralCode,
    });
  }

  return NextResponse.json({
    success: true,
    position: entry[0].position,
    referralCode,
  });
}

function generateReferralCode(email: string): string {
  const { createHash } = require('crypto');
  return createHash('sha256').update(email).digest('hex').slice(0, 8);
}

Phase 5: Confirmation Email

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

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

export async function sendWaitlistConfirmation({
  email, name, position, referralCode,
}: { email: string; name: string; position: number; referralCode?: string }) {
  const referralUrl = referralCode
    ? `${process.env.NEXT_PUBLIC_APP_URL}/?ref=${referralCode}`
    : null;

  await resend.emails.send({
    from: `[Product Name] <[email protected]>`,
    to: email,
    subject: `you're #${position} on the list`,
    html: `
      <div style="font-family: sans-serif; max-width: 520px; margin: 0 auto; padding: 40px 20px; color: #111;">
        <p>Hey ${name},</p>
        <p>You're <strong>#${position}</strong> on the waitlist. We'll reach out when you're up.</p>
        ${referralUrl ? `
        <p style="margin-top: 24px;">
          Move up the list — share your link:<br>
          <a href="${referralUrl}" style="color: #000; font-weight: bold;">${referralUrl}</a>
        </p>
        ` : ''}
        <p style="color: #666; font-size: 13px; margin-top: 40px;">— [Your Name]</p>
      </div>
    `,
  });
}

Phase 6: The Signup Form Component

// components/waitlist-form.tsx
'use client';
import { useState } from 'react';

export function WaitlistForm({ referralCode }: { referralCode?: string }) {
  const [email, setEmail] = useState('');
  const [state, setState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [position, setPosition] = useState<number | null>(null);
  const [userReferralCode, setUserReferralCode] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setState('loading');

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

    const data = await res.json();

    if (data.success) {
      setState('success');
      setPosition(data.position);
      setUserReferralCode(data.referralCode);
    } else {
      setState('error');
    }
  }

  if (state === 'success') {
    return (
      <div>
        <p>you're #{position} on the list.</p>
        {userReferralCode && (
          <p>
            share your link to move up:{' '}
            <code>{`${window.location.origin}/?ref=${userReferralCode}`}</code>
          </p>
        )}
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="[email protected]"
        required
        disabled={state === 'loading'}
      />
      <button type="submit" disabled={state === 'loading'}>
        {state === 'loading' ? 'joining...' : 'join waitlist'}
      </button>
      {state === 'error' && <p>something went wrong. try again.</p>}
    </form>
  );
}

Read the referral code from URL params in the page:

// app/page.tsx
export default function Page({ searchParams }: { searchParams: { ref?: string } }) {
  return <WaitlistForm referralCode={searchParams.ref} />;
}

Phase 7: Admin View

Simple route that requires a secret header — no full auth needed for a waitlist admin:

// app/api/admin/waitlist/route.ts
export async function GET(req: Request) {
  if (req.headers.get('x-admin-secret') !== process.env.ADMIN_SECRET) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const signups = await db.select().from(waitlistTable).orderBy(asc(waitlistTable.position));

  return Response.json({
    total: signups.length,
    signups,
  });
}

Fetch with: curl -H "x-admin-secret: $ADMIN_SECRET" https://yoursite.com/api/admin/waitlist

Verify

[ ] Duplicate email returns success with alreadyRegistered: true (no error, no leak)
[ ] Email normalized to lowercase before storage
[ ] Position increments correctly — first signup is #1
[ ] Confirmation email arrives in inbox, not spam
[ ] Referral code in URL (?ref=) is captured and stored on signup
[ ] Referrer's count increments when referred signup completes
[ ] Admin route requires x-admin-secret header
[ ] ADMIN_SECRET and RESEND_API_KEY in .env.example
[ ] Form disables input during submission
[ ] Success state shows position and referral link

Comments

Markdown supported