Skip to content
Back to field notes
AuthJune 6, 20266 min read

Waitlist and signup form abuse in vibe-coded SaaS apps

An AI-generated signup or waitlist endpoint with no server-side limit can be flooded with bot and disposable-email entries the moment the URL spreads; this note shows what to check before that happens.

AuthRate limitingAnti-abuse

An open signup endpoint gets flooded the moment the URL spreads

This is the concrete failure: your waitlist or signup form posts to an endpoint that accepts every request it receives. There is no per-IP limit, no check on the email domain, and no challenge that separates a person from a script. The form works perfectly in your own browser, which is exactly why the gap is easy to miss. The risk is what happens once the link reaches a Show HN post, a Reddit thread, or a scraper that crawls Product Hunt: a single script can submit thousands of entries in minutes.

Be precise about what is observable versus what is only possible. What is observable from the outside: the endpoint exists, it accepts a POST, and it returns the same success response on the tenth rapid request as on the first. What is only possible until it happens: your list fills with disposable-email signups, your "5,000 on the waitlist" number becomes fiction, and if each signup triggers a confirmation email you start paying to mail addresses that will never open them. Signup form abuse prevention is about closing the endpoint before any of that runs, not cleaning up after.

This is an availability and integrity problem, not a data-exposure one. Nothing private leaks. What breaks is your ability to trust your own numbers and your sending reputation, which both matter on launch day.

Why AI-built apps ship this endpoint wide open

A prompt like "add a waitlist form that saves the email" produces exactly that and nothing more. The generated handler validates that the field looks like an email, inserts a row, and returns success. It functions on the first try, the demo looks finished, and the part that never gets generated is the abuse layer, because you did not ask for it and the happy path does not need it.

Three patterns make this routine in vibe-coded apps:

None of this is a framework bug. It is the default surface area of a form that was generated to work, not to withstand traffic it did not invite. The same shape shows up on login and OTP endpoints; the broader pattern is covered in the note on rate limiting login endpoints in AI-built apps.

  • The generated endpoint has no rate limit. Each request is handled independently, so ten requests a second and one request a minute are treated the same way.
  • Email validation stops at format. A string that matches the shape of an email passes, so a throwaway address from a disposable-email provider is accepted exactly like a real one.
  • Fast deploy means the public URL exists before anyone reviews the form's failure modes. The endpoint is live and indexable before the question "what stops a bot here?" is ever asked.

What to check on your own form before launch

You can confirm all of this against your own app, with no attack and no third-party target. Open the form in your browser, then open DevTools and the Network tab so you can see the actual request the form sends. Note the endpoint path and method, for example POST /api/waitlist or a direct call to your backend.

Rate limiting. Submit the form a handful of times in quick succession with throwaway values. Watch the responses in the Network tab.

Disposable and throwaway emails. Try signing up with an address at a known disposable-email domain.

Automated submission. With DevTools open, you can see whether the request carries any token your form issued, or whether the raw POST succeeds on its own.

  • Suspicious result: every request returns the same 200 success, with no slowdown and no 429 Too Many Requests after a burst. The endpoint has no per-IP or per-session limit.
  • Safer result: after a few rapid submissions you start getting a 429, or a Retry-After response header appears. Something is counting requests and pushing back.
  • Suspicious result: it is accepted exactly like a real address, with no different handling.
  • Safer result: the signup is rejected or flagged, which tells you a domain check exists. Note that domain blocking is a coarse filter, not proof of a real human.
  • Suspicious result: replaying the bare POST from the Network tab, or hitting the endpoint directly, succeeds with no challenge token and no check.
  • Safer result: the request requires a CAPTCHA token or a server-issued nonce that a naive script would not have. Confirm the server actually verifies that token rather than just accepting its presence.

How to add the controls, and what they do not cover

The fix is a thin server-side layer on the endpoint. The order below is also the order of effort versus payoff.

Be honest about the limits of each control. A rate limit keyed only on IP is weakened by botnets and rotating proxies that spread requests across many addresses; pair it with a per-account or per-fingerprint signal where you can. Domain blocking is an arms race, since new disposable-email domains appear constantly and a determined attacker can use a real-looking inbox. A CAPTCHA reduces casual automation but does not stop a human paid to click, and it can be solved by services. These controls together raise the cost of abuse; none of them makes the endpoint unabusable. And none of this is an authentication or authorization control. You are not deciding who is allowed in, only slowing down how fast anonymous requests can arrive.

  • Add a server-side rate limit keyed on IP, and ideally on a second signal too. A small limit per IP per window stops the cheapest floods. In a Redis-backed app this is a counter with an expiry; many platforms also expose an edge or middleware rate limiter. The limit must live on the server, because anything enforced only in the browser is trivially skipped by posting straight to the endpoint.
  • Validate the email beyond format. Reject obvious disposable-email domains and require a confirmation click before a signup counts. Double opt-in is the single biggest improvement to list integrity, because an unconfirmed address never inflates your numbers and never gets repeatedly mailed.
  • Add a challenge only where you need it. A CAPTCHA or a privacy-preserving alternative raises the cost of scripted submissions. Trigger it on suspicious behavior rather than on every visitor so you do not tax real users. Critically, verify the challenge token on the server; a token the client sends but the server never checks adds friction for humans and nothing for bots.

Where VibeCodeGuard fits, and what it does not prove

VibeCodeGuard's Launch Check probes your signup endpoint from the public surface and reports on rate-limit signals: whether a burst of requests produces a 429 or a Retry-After response header, or whether the endpoint answers identically every time. A missing rate-limit signal is a concrete, observable launch risk, and the scan flags it with severity and fix direction. It is checking the thing a flooding script would hit first.

What the scan cannot do is confirm your other controls actually work. Whether your CAPTCHA token is truly verified server-side, and whether your email validation correctly rejects disposable domains and enforces double opt-in, are correctness questions that need manual testing on your own app. The scan sees the endpoint's response behavior from outside; it does not read your validation logic or replay your challenge flow. Launch Check is a public-surface scan, not a penetration test, a code review, or an exploit-verification tool.

Next step: open your own waitlist form, submit it a few times in a row from the Network tab, and confirm you see a 429 after a burst. Then run a focused Launch Check against the public URL before you share it, so a wide-open endpoint shows up in your scan and not in your signup numbers.

A clean rate-limit result is one signal, not a guarantee. It does not prove your email validation is sound, that your CAPTCHA is enforced, or that your list is bot-free. It tells you the cheapest flood will meet some resistance.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.