The simplest genuinely useful thing you can build. One input, one table, one email — and four ways it silently doesn’t work.
The Build
Next.js App Router, TypeScript, Tailwind, Supabase. Build a waitlist landing page.
Hero with headline, one-sentence description, and an email capture form. Below: three benefit points and an FAQ.
Create a
waitlisttable: id uuid, email citext unique not null, referrer text, utm_source text, created_at timestamptz, confirmed boolean default false.The form must submit to a server action that validates the email server-side, inserts it, and returns a real success or error state. Don’t stub the handler — I want a working database write.
That last sentence is there because the default behaviour is a stub. You’ll get a form with a spinner, a success message, and nothing in your database.
Test it immediately. Submit your own address, then check the table. Don’t move on until you see the row.
The Four Things It Gets Wrong
1. Duplicate submissions
Someone signs up twice, or double-clicks. Without handling, you get either a database error shown as a scary red message, or two rows.
Handle duplicates gracefully. On a unique violation, return the same success message as a new signup — don’t reveal whether the address was already on the list. Disable the submit button while the request is in flight.
Not revealing existing addresses matters more than it seems: a waitlist that says “already registered” is an email enumeration tool.
2. No spam protection
An open form on a public page will get bot submissions within days.
Add a honeypot field — a hidden input bots fill and humans don’t. Silently discard any submission where it’s non-empty, returning success so bots don’t learn. Add rate limiting of 3 submissions per IP per hour.
Honeypots cost nothing and catch most low-effort bots. Add Turnstile only if you actually see abuse — it adds friction for real users.
3. Email that goes to spam
Sending from your app server directly means poor deliverability from day one.
Send a confirmation email via Resend when someone joins. Include their position on the list. Send from a subdomain like updates@yourdomain.com, not the root domain. Give me the SPF, DKIM and DMARC records I need to add.
Set up the DNS records before launch. Deliverability reputation builds over time, and starting cold on launch day is how a waitlist announcement lands in spam.
4. No attribution
If you don’t capture where signups came from, you can’t tell which channel worked.
Capture the referrer and any UTM parameters from the URL and store them with each signup. Add an admin page at /admin/waitlist showing total signups, signups per day, and a breakdown by source. Protect it — it must not be publicly accessible.
That last clause matters. An unprotected admin route lists everyone’s email address.
Position Numbers and Referrals
Showing someone they’re “#347” measurably improves sharing. Referral mechanics amplify it further.
Add a referral system. Each signup gets a unique code. Signing up with someone’s code moves the referrer up the list. Show the user their position and a shareable link with pre-filled share text.
One caution: don’t compute position by counting rows on every page load. It’s a full table scan on your most-hit endpoint.
Store position as a column assigned at insert. Don’t compute it with a count query on read.
Before Launch
- Real submission tested end to end — row exists in the database
- Confirmation email received, checked in Gmail and Outlook
- SPF, DKIM, DMARC configured and verified
- Duplicate submission returns success, not an error
- Honeypot present and working
- Rate limiting active
- Admin page requires authentication
- UTM parameters captured
- Mobile tested on a real device
- Privacy policy linked (you’re collecting personal data)
That last one is a legal requirement in most jurisdictions once you store email addresses, and it’s the sort of thing AI never adds unasked.
Export Before You Need It
Write the export now, while you remember the schema:
Add an admin-only CSV export of the waitlist with email, signup date, source, and referral code.
Launch day is a bad time to discover you can’t get your list out.
Next
A waitlist is the front of a product that doesn’t exist yet. When it does, vibe code a landing page covers the real marketing page, and vibe code a SaaS MVP covers what those emails eventually get invited to.
Related reading: