Case Study · Real Project · Unfiltered
"Waitlisted" —
A Real
Build Log
Five sessions · Seven days
From idea to deployed · Warts and all
Every mistake documented
This is a complete, honest account of building "Waitlisted" — a simple tool that lets small businesses collect and manage email waitlists — using vibe coding from a blank page to a live URL. Including every mistake, every wrong turn, and every moment where the AI went sideways.
Project
Waitlisted — email waitlist tool
Total time
~11 hours across 7 days
Stack
Next.js, Supabase, Vercel
Tools used
Cursor + Claude
Sessions
5 sessions documented
Outcome
Live at waitlisted.example.com
Every conscious builder hits the same walls. This is an honest account of what those walls look like — and how to get through them.
THE COMPLETE SERIES Included in The Complete Series ($29).

What happened, when

Day 1
Intent Worksheet + architecture
✓ Clear
Day 2
Core CRUD + Supabase setup
✓ Working
Day 3
Auth — went wrong twice
✗ Scrapped, restarted
Day 4
Auth (take 3) + email confirmation
✓ Finally working
Day 5
Admin dashboard + export
✓ Done
Day 6
Safety audit + fixes
✓ 2 issues fixed
Day 7
Deploy to Vercel + custom domain
✓ Live
The idea, the worksheet, and the first architectural mistake

The idea came from a personal frustration: every time I launch anything — a course, a tool, a newsletter — I cobble together a Typeform and a spreadsheet to manage the waitlist. It's fine but it's friction. I wanted a purpose-built thing I could set up in 5 minutes.

Before opening Cursor, I did the Intent Worksheet. I want to share the actual answers because the specificity matters — and mine weren't specific enough on the first attempt.

First Attempt — Too Vague
One-liner: "Waitlisted helps people collect email signups for upcoming products."
This describes a category (lead gen tool), not a specific product. Who? For what workflow? What's the before state?
Whenever your one-liner could describe an existing product (Mailchimp, ConvertKit), it's not specific enough.
Second Attempt — Much Better
"Waitlisted helps indie makers to create a branded waitlist page and see who signed up — without setting up a database, writing code, or paying for a CRM — so they can validate interest before building."

This one names the user (indie makers), the before state (current cobbled solutions), and the specific outcome (validation before building).
The Architecture Conversation

I asked for the architecture before any code. The AI came back with two options: a full Next.js app with Supabase, or a simple HTML page + serverless functions. I almost chose option 2 — it seemed simpler. But when I asked "which would be harder to add authentication to later?" it became clear that the Next.js option was the right foundation even though it was more upfront complexity.

Principle 5 in Action
This is the exact moment where architectural ownership matters. The "simpler" option would have worked for the first version but created a migration nightmare later. Asking about future pain before choosing is the discipline that separates good vibe coding from technical debt accumulation.
Building the core — and the first real success

Session 2 was the most satisfying session of the whole project. I started with the prompt below and ran the code after each iteration. Three focused prompts, each building on the last, each tested before the next one was written.

Prompt 2.1
Database schema + Supabase setup
45 min
Actual prompt used
# Context I'm building Waitlisted: a tool for indie makers to create a simple waitlist page without coding. Stack: Next.js 14, Supabase, TypeScript Architecture: agreed in previous session (app router, server actions) # What I need now Create the Supabase schema for: - waitlists table: id, name, description, slug (unique), owner_id, created_at - subscribers table: id, waitlist_id, email, name (optional), created_at Write the SQL to create these tables with correct types, foreign keys, and RLS policies so: - Anyone can INSERT into subscribers (public signup) - Only the waitlist owner can SELECT/UPDATE/DELETE their waitlists and subscribers Also write the TypeScript types for both tables.

The schema came back correct on the first attempt. The RLS policies took one correction — the AI used `auth.uid()` directly but forgot to handle the case where a user is not authenticated (public signup). Fixed with one follow-up prompt.

Prompt 2.2
Public signup page
40 min
Build the public signup page at /[slug] that: 1. Fetches the waitlist data from Supabase by slug (server component) 2. Shows the waitlist name and description 3. Has an email + optional name form 4. Submits via a server action to INSERT into subscribers 5. Shows a success message after signup (no redirect) 6. Prevents duplicate email signups — show a friendly message if already signed up Handle: slug not found → 404 page Handle: Supabase error → show generic "something went wrong" message Do NOT show internal error details to users.

This worked on the second run. The first run had a TypeScript error — the AI had typed the Supabase response incorrectly. I pasted the error back, it fixed it immediately. This is the correct workflow: don't debug yourself, just paste the error.

Mistake 2.1 — Accepting Code Before Running It
Halfway through session 2 I got comfortable and accepted three prompts' worth of code without running the app between them. When I finally ran it, I got a cascade of related errors that took 40 minutes to untangle instead of the 5 minutes each would have taken in isolation.
Run after every single prompt. The 30 seconds it takes pays for itself every time. Non-negotiable.
Authentication: the session that almost broke the project

I'm documenting this session in detail because it's the most instructive — and the most common failure mode in intermediate vibe coding projects. Auth is where things go wrong. Not because the AI is bad at auth, but because auth has many moving parts and AI often implements them inconsistently across files.

Mistake 3.1 — Asking for "Add auth" as a single prompt
My first auth prompt: "Add Supabase Auth so users can sign up and log in. Protect the /dashboard route." This produced a working-looking implementation that had a critical flaw: the middleware was checking for a session but not refreshing it, meaning users would get logged out after an hour with no explanation. I didn't catch this because I didn't know to test for it.
Never prompt auth as a single request. Break it into: (1) sign-up flow, (2) sign-in flow, (3) session management / middleware, (4) protected routes — each as a separate prompt with testing between each.
Mistake 3.2 — Trying to fix instead of restart
After noticing the session issue, I spent 90 minutes trying to patch the existing auth implementation. Each fix created a new problem. I was in a classic "patchwork spiral." Eventually I scrapped the entire auth implementation and started fresh with a much more structured approach.
When you've made three fixes and the problem count hasn't gone down, stop. Start the section over with a cleaner, more specific prompt. Time spent patching bad foundations is never recovered.
The Auth Prompt That Finally Worked
I started Session 4 with a completely different framing — instead of "add auth," I asked the AI to explain Supabase Auth's session management model first, then implement it step by step with a specific test after each step. The implementation that came from this approach was cleaner, I understood it better, and it hasn't had a single auth bug since.
Auth done, dashboard built, export working

With auth properly implemented, Sessions 4 and 5 were fast and relatively clean. The admin dashboard — showing all subscribers, their signup dates, and a CSV export — took one focused 90-minute session.

The key difference in these sessions: I started each one with a context block summarising what existed, what we were building, and explicit constraints. The AI never had to guess what the project was. Outputs improved dramatically compared to the early sessions.

Session Context Block
Template I used at the start of Sessions 4 and 5
Copy this
# Project: Waitlisted A tool for indie makers to create email waitlists. Stack: Next.js 14 (app router), Supabase, TypeScript, Vercel # What exists - Public page /[slug]: shows waitlist, email signup form, success state - Auth: Supabase Auth with email/password, session refresh middleware working - DB: waitlists and subscribers tables with RLS # Naming conventions - Server components by default, client components only when needed - Server actions for mutations (not API routes) - Error handling: log to console, show generic message to user # Today's goal [FILL IN] # Constraints - Don't change the auth implementation - Don't add new npm packages without asking - All new pages follow the existing layout pattern
The two issues I would have shipped without the audit

Before deploying, I ran the AI security audit prompt from the Safety Guide against every file that handles user data. It found two issues I had completely missed:

Issue 1 — No Rate Limiting on Signup
The public email signup endpoint had no rate limiting. An automated script could submit thousands of fake email addresses to any waitlist. Beyond the annoyance, this would inflate subscriber counts and potentially exhaust the Supabase free tier.
Fix: Added Vercel's rate limiting middleware to the signup server action — 5 submissions per IP per hour. 15 minutes to implement once the issue was identified.
Issue 2 — IDOR on CSV Export
The CSV export endpoint took a waitlist ID as a parameter and returned all subscribers. The RLS in Supabase should have caught this — but the export used the service role key, bypassing RLS entirely. Any authenticated user could export any waitlist's subscribers by changing the ID in the URL.
Fix: Added an explicit ownership check before the export query. Never use the service role key for user-facing operations — or if you must, always add manual ownership checks.
The value of the audit
Both issues would have been in production. The IDOR in particular was a genuine data exposure: one authenticated user could access another user's subscriber list. The 30 minutes the audit took was not optional — it was the most important 30 minutes of the project.

What worked. What didn't.

Worked well
The Intent Worksheet — every hour spent on it saved two in rework
Architecture conversation before any code — the right foundation from the start
Context block at the start of every session — dramatically better outputs
Run after every prompt — caught issues before they compounded
Breaking auth into 4 sub-prompts (after learning the hard way)
Using Supabase Auth instead of custom auth — zero auth bugs in production
The security audit — caught two real issues before launch
Struggled with
Large "do everything" prompts — always produced messy output
Debugging AI auth code without understanding it first
Accepting multiple prompts' code without running between them
Trying to patch bad implementations instead of restarting cleanly
Long sessions without re-introducing context — outputs degraded
TypeScript types — AI was inconsistent; needed multiple corrections
The Single Most Valuable Thing
If I had to name one thing that made the difference between a project that shipped and one that collapsed in a pile of spaghetti code: the context document. Starting every session with a clear, specific description of what existed, what I was building, and what not to touch was the single highest-leverage practice in the entire build. Create one. Use it religiously.