Skip to content
← writing

Block disposable emails at signup

security
signups
spam
frontend

If you let anyone sign up with any email, a chunk of your "users" will be throwaway inboxes — mailinator.com, 10minutemail.com, and hundreds more. They're how people farm free trials, dodge bans, and inflate your numbers with accounts that will never convert. @venelinkochev put it well: blocking these at signup, plus Cloudflare Turnstile for the bots, is a big lift in signup quality for very little work.

The trick is almost embarrassingly simple: there's a community-maintained list of disposable domains, and you just check the signup email's domain against it.

How it works

The disposable/disposable-email-domains repo publishes a domains.json — tens of thousands of known throwaway domains, updated continuously. Fetch it (cache it for a day), drop it in a Set, and membership-test the domain after the @:

const LIST =
  "https://rawcdn.githack.com/disposable/disposable-email-domains/master/domains.json";
 
let cache: Set<string> | null = null;
let fetchedAt = 0;
 
async function disposableDomains() {
  if (cache && Date.now() - fetchedAt < 86_400_000) return cache; // 1 day
  const res = await fetch(LIST);
  cache = new Set<string>(await res.json());
  fetchedAt = Date.now();
  return cache;
}
 
export async function isDisposableEmail(email: string) {
  const domain = email.split("@")[1]?.toLowerCase().trim();
  if (!domain) return false;
  return (await disposableDomains()).has(domain);
}

Then guard the signup handler:

if (await isDisposableEmail(email)) {
  return new Response("Please use a permanent email address.", { status: 422 });
}

A Set lookup is O(1), and the daily-cached fetch means you're not hammering the CDN. On an edge runtime like Workers, keep the cache in module scope (or KV) so it survives between requests.

Do it server-side

The check belongs on the server, at the signup endpoint — never trust a client-only check, since anyone can skip it. The interactive demo above runs in your browser only because it's a demo; in production this is a few lines in your API route.

Layer it

No single signal is enough on its own. Stack cheap ones:

  • Turnstile (or hCaptcha) to filter out bots before they submit.
  • Disposable-domain block to filter out throwaway humans.
  • Verification email so a real, owned inbox is still required.

Each is minutes of work; together they cut a surprising amount of junk.

Takeaways

  • Disposable inboxes are a top source of fake/abuse signups — block the domain at signup against a maintained list, not a hand-rolled regex.
  • Fetch domains.json once a day into a Set; the check is O(1) and runs server-side.
  • Combine with Turnstile and email verification for layered, low-effort defense.

I put a live checker and the copy-paste guard on the Freebies page.

Ask your agent to implement this

Read the full writeup at https://seangeng.com/writing/block-disposable-emails.md and implement it in my project.

It covers: Block disposable emails at signup — Throwaway inboxes are how spam, trial-abuse, and fake accounts get in. Checking the email domain against a maintained disposable-domains list at signup takes minutes and pairs perfectly with Cloudflare Turnstile.

Requirements:
- Follow the technique/approach exactly as described in the writeup.
- Adapt names, colors, and styling to my project's existing conventions.
- If it's a component, make it reusable with sensible props and TypeScript types.
- Keep it accessible: semantic HTML, keyboard support, and respect prefers-reduced-motion.
- When done, tell me which files you created or changed and how to use it.

Paste into Claude Code, Codex, Cursor, or any agent. view raw .md