| name | saas-waitlist-backend |
| description | Pick and wire the waitlist backend adapter when generating the landing page. Auto-activate when `saas-landing-builder` is scaffolding the waitlist component. Chooses between local-JSON (default), Supabase, Resend, or Loops based on env vars and user preference, then writes the matching `app/api/waitlist/route.ts` and `lib/waitlist.ts` into `sites/<slug>/`. |
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_KEY in env → use Supabase (table waitlist_signups).
RESEND_API_KEY in env → use Resend Audiences.
LOOPS_API_KEY in 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 exporting submit(email: string, meta?: Record<string, unknown>).
sites/<slug>/app/api/waitlist/route.ts — Next.js Route Handler (POST) that calls submit().
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_signups with columns id (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.sql and tell the user to run it (or apply via mcp__claude_ai_Supabase__apply_migration if they want).
- Insert with the
anon key + 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/create with email and a custom property source: "<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 (
writeFile to 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