SaaS Waitlist Backend
Picks one of four adapters and wires it into the generated landing page.
Adapter selection logic
In priority order:
- User explicitly chose one in chat → honor it.
SUPABASE_URL+SUPABASE_ANON_KEYin env → use Supabase (tablewaitlist_signups).RESEND_API_KEYin env → use Resend Audiences.LOOPS_API_KEYin env → use Loops.- No env hints → default to local JSON (
sites/<slug>/.data/waitlist.json) — zero external dependencies, perfect for early validation.
After picking, ask the user once for confirmation in chat, then proceed. Don't pause again.
What you write per adapter
For all adapters, write three pieces:
sites/<slug>/lib/waitlist.ts— adapter module exportingsubmit(email: string, meta?: Record<string, unknown>).sites/<slug>/app/api/waitlist/route.ts— Next.js Route Handler (POST) that callssubmit().sites/<slug>/.env.local.example— required env vars stubbed with comments.
See adapters.md for the actual code stubs.
Validation rules (all adapters)
- Email regex on server, not just client.
- Reject obvious throwaways (
@example.com,@test.com) with a friendly error. - Return 200 with
{ ok: true }on success, 400 with{ ok: false, error }on validation failure, 500 on backend error. - Rate-limit: simple in-memory token bucket keyed by IP, 5/min, in the route. (For local-JSON this is the only rate-limit; for Supabase, recommend adding an RLS policy in a follow-up.)
Supabase specifics
- Default table:
waitlist_signupswith columnsid (uuid, default uuid_generate_v4()),email (text, unique),brand_slug (text),meta (jsonb),created_at (timestamptz, default now()). - If the table doesn't exist, generate a migration SQL file at
sites/<slug>/supabase/migrations/0001_waitlist.sqland tell the user to run it (or apply viamcp__claude_ai_Supabase__apply_migrationif they want). - Insert with the
anonkey + RLS policy "anyone can insert" — keep reads service-role only.
Resend specifics
- Use Resend Audiences API to add the email to a named audience.
- Audience name =
<brand-slug>-waitlist. Create it if missing via the API on the server. - Returns 200 even on duplicate (idempotent).
Loops specifics
- POST to
https://app.loops.so/api/v1/contacts/createwithemailand a custom propertysource: "<brand-slug>-landing". - Handle duplicate as success.
Local JSON specifics
- Write to
sites/<slug>/.data/waitlist.json(gitignored). - File shape:
{ "signups": [{ "email": "...", "createdAt": "...", "meta": {} }] }. - On every write, create parent dir if needed; use atomic write (
writeFileto tmp then rename). - Dedupe on email.
- Tell the user: "Local JSON — fine for validation. Swap to Supabase or Resend before launch."
Hard rules
- Always validate server-side. Never trust the client.
- Never log full emails to stdout. Hash or partial-mask in logs.
- Don't leak the chosen adapter in the response. Just
{ ok: true }. - The Waitlist component never short-circuits on success without showing user feedback. Animated checkmark + "You're on the list."
Related
- adapters.md — concrete code per adapter
saas-landing-builder/section-recipes.md— Waitlist component spec