How to Add a Waitlist Page to an AI-Built App

A concrete walkthrough of the schema, prompt sequence, and validation rules for adding a waitlist page to an AI-built app, including position calculation and a referral unlock mechanic.

Steve Jefferson
Steve Jefferson
Developer Advocate
29 August 20261 min read

A waitlist page needs four things working together: a signup form, a database table that survives duplicate submissions, a way to calculate each person's position in line, and optionally a referral mechanic that rewards people for sharing. Most tutorials stop at the form. The schema and the duplicate-handling logic are where things actually break, especially when you're prompting an AI app builder to generate the backend and it quietly lets the same email sign up three times with three different queue positions. This walks through the exact schema, the prompt sequence, and the validation rules worth spelling out explicitly, because the AI builder will not guess them correctly on its own.

What a waitlist page is actually made of

Strip away the marketing copy and a waitlist is a single table plus three pieces of logic:

  • A unique, case-insensitive email address

  • A position number that reflects signup order, and referral boosts if you're using them

  • A referral code unique to each signup, plus an optional pointer to whoever referred them

  • A timestamp for tiebreaking and analytics

Everything else, the confirmation email, the "you're number 412" message, the share button, is built on top of those four fields. Get the schema right first and the rest is mostly UI work.

The schema

sql
create table waitlist_signups (
  id uuid primary key default gen_random_uuid(),
  email text not null,
  email_normalized text generated always as (lower(trim(email))) stored,
  position integer,
  referral_code text not null unique,
  referred_by text references waitlist_signups(referral_code),
  referral_count integer not null default 0,
  created_at timestamptz not null default now()
);

create unique index waitlist_signups_email_unique
  on waitlist_signups (email_normalized);

Two details worth calling out. The email_normalized generated column plus a unique index on it is what actually stops duplicate signups, not a unique constraint on email directly, because "Jordan@example.com" and "jordan@example.com" are the same person to everyone except a database. And position is nullable and stored as a plain integer rather than computed on the fly every time, because recalculating a live ranking on every page load gets slow once you have a few thousand signups.

The prompt sequence

This is the order that actually works when you're describing this to an AI app builder. Skipping steps or bundling them into one giant prompt is the fastest way to end up with a table that has no unique constraint and a position field that never updates.

Step 1: Describe the schema explicitly

Don't just say "add a waitlist table." Hand the builder the actual constraints:

"Create a waitlist_signups table with email (unique, case-insensitive), a referral_code that's unique and auto-generated for each row, an optional referred_by field that points to another signup's referral_code, a referral_count integer defaulting to 0, an integer position field, and a created_at timestamp. Add a unique index on the lowercased, trimmed email so duplicate signups with different casing are rejected at the database level, not just in the app."

Naming the case-insensitivity requirement matters. Left unstated, most AI builders will add a plain unique constraint on email and call it done, which passes every test you're likely to run manually and then fails the first time someone re-types their email with a capital letter.

Step 2: Position-in-queue logic

Ask for the ranking query separately from the schema, since it's the piece most likely to need a second pass:

sql
select
  id,
  email,
  row_number() over (
    order by referral_count desc, created_at asc
  ) as computed_position
from waitlist_signups;

This ranks by referral count first, then by signup time, so referrals move people up without letting someone game the system by referring after the fact and jumping past people who signed up the same day. Tell the builder to run this as a scheduled job or a database trigger that updates the stored position column, rather than computing it inline on every page request. A live join across the whole table on every visit is the kind of thing that works fine in a demo and falls over in the first real spike.

Step 3: Email capture and duplicate handling

This is the step where vague instructions cause the most damage. Be specific about what should happen when someone submits an email that's already in the table:

"When a signup request comes in, check for an existing row with that normalized email first. If one exists, don't insert a new row or throw an error, just return that person's existing position and referral code as if they'd just signed up again. If no row exists, insert a new one, generate a referral code, and set position to the current count plus one."

That single instruction, don't throw an error, return the existing state, is the difference between a form that feels broken (an error message for someone checking their spot in line) and one that feels intentional. Most default implementations get this backwards: they treat a duplicate email as a failure case instead of the far more common case of someone returning to check their status.

Step 4: Referral unlock mechanic

If you want signups to move up the queue by referring others, this needs to be prompted as its own step, not an afterthought bolted onto the insert:

"When someone signs up with a referral_code in the URL, look up the referrer by that code. If found, increment their referral_count by 1 and set the new signup's referred_by field to the referrer's code. Recalculate both people's positions after the update. If the referral code doesn't match any row, still complete the signup, just without crediting a referrer."

That last sentence matters more than it looks. Without it, a mistyped or expired referral link sitting in someone's browser history can silently block a signup entirely, which is a strange failure mode to debug when it's happening to one user at a time and quietly.

What to tell the AI builder about validation, explicitly

Left to its own defaults, an AI app builder will usually add basic email format validation and stop there. That's not enough for a page whose entire job is collecting emails accurately. Spell out these four things in your prompt rather than assuming they'll be inferred:

  • Reject empty strings and whitespace-only input before it reaches the database, not after

  • Normalize email casing and trim whitespace before checking for duplicates, in the application layer and again at the database level

  • Return the same success state for a genuinely new signup and a duplicate resubmission, so the frontend never has to special-case an already-on-the-list response

  • Rate-limit the signup endpoint, since a public form with no rate limit is an open invitation to script a few thousand fake rows into your referral leaderboard

None of these are exotic. They just tend to get skipped because a waitlist form looks simple enough that nobody thinks to ask for them.

Common mistakes AI builders make on this specific feature

A few patterns show up often enough that they're worth checking for by name once the AI builder hands back a first version:

  • The unique constraint sits on email instead of the normalized version, so casing variants slip through

  • Position is calculated inline on every request instead of stored and recalculated, which is fine at ten signups and slow at ten thousand

  • A duplicate signup returns a 500 error instead of the existing position, which breaks any UI that tries to show someone their spot again

  • The referral code is generated from a sequential integer, which makes it trivial to guess other people's codes by incrementing a number in the URL

  • There's no index on referral_code itself, so the referrer lookup on every new signup does a full table scan

None of these show up in a quick manual test with two or three signups. They show up once real traffic hits the page, which is exactly when they're most annoying to fix.

Testing before you launch it

Before pointing traffic at the page, submit the same email twice in a row, once in lowercase and once with the first letter capitalized, and confirm you get the same position both times, not two separate rows. Then submit a signup with a referral code that doesn't exist and confirm it still completes. Then check what the referral code actually looks like, a short random string is harder to guess than an incrementing number, and pull up the position query's execution plan if your table already has more than a few hundred rows, since a missing index on referral_code or email_normalized is easy to miss until it's slow. Those checks catch most of what breaks in a real waitlist that never showed up in the demo.

What comes after the waitlist page

Once the waitlist page is live and collecting signups, a status page is often the next thing worth adding, particularly if the waitlist is fronting a product that's still being built and you want somewhere to point people when something breaks. A changelog page is a natural follow-up too, since a waitlist audience is exactly the group most likely to care about what shipped since they signed up. If you're prompting an AI app builder like Swarmz for both the schema and the queue logic in one project, it also helps to have already worked out how to prompt AI to design a database schema more generally, and how to add email sending for the confirmation message every signup expects. For the broader picture of building an app this way from the ground up, see how to build an app with AI.

FAQ

How do you calculate position in a waitlist?

Rank signups by referral count first, then by signup timestamp, using a window function like row_number(). Store the result in a column on the table and recalculate it on a schedule or trigger rather than computing it live on every page load.

How do you prevent duplicate signups on a waitlist?

Normalize the email (lowercase, trimmed) in a generated column and put a unique index on that column, not on the raw email field. Then handle the duplicate case in application logic by returning the existing signup's position instead of an error.

Should a waitlist page require email verification?

It depends on how much the position matters to people. If referrals move people up the queue, unverified emails make it trivial to create fake accounts to game the ranking. A lightweight confirmation link is usually enough without adding real friction to signup.

How does a referral unlock mechanic work on a waitlist?

Each signup gets a unique referral code. When a new person signs up through that code, the referrer's referral_count increments and their position is recalculated. The mechanic only works if the ranking query actually weights referral_count, not just signup order.

Can you build a waitlist page without a database?

Not really, not one that tracks positions or referrals correctly. A pure form-to-email setup can capture addresses, but it can't tell you who's already signed up, calculate a position, or credit a referral, all of which need a table to check against.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.

How to Add a Waitlist Page to an AI-Built App | swarmz.net