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 pad your numbers with accounts that never convert. @venelinkochev put it well: block these at signup, add Cloudflare Turnstile for the bots, and signup quality jumps for very little work.

The trick is almost embarrassingly simple. There's a community-maintained list of disposable domains, and you 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 demo above runs in your browser 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, so stack a few cheap ones. Turnstile (or hCaptcha) filters out bots before they submit. The disposable-domain block filters out throwaway humans. And a verification email still requires a real, owned inbox at the end. Each is minutes of work, and together they cut a surprising amount of junk.

What I'd flag: use a maintained list, not a hand-rolled regex of domains you remember. The regex goes stale the day after you write it; the list doesn't.

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 well 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