Disposable email detection in Node.js: a 2026 tutorial with code — SignupDoggy Blog

A copy-paste Node.js tutorial for detecting disposable, temporary, and throwaway email addresses at signup. Covers 4 approaches: local blocklist, DNS-based, third-party API, and SignupDoggy. Includes a working 30-line Express middleware you can drop into any SaaS.

Disposable email detection in Node.js: a 2026 tutorial with code

If your signup form is open to the public internet, between 5% and 15% of new accounts are disposable email addresses. Mailinator. Guerrilla Mail. Temp-mail. The same six providers, over and over. A signup from `tempmail.com` is not a future customer — it is a future support ticket.

This post is a copy-paste Node.js tutorial for catching disposable emails at signup. Four approaches, ordered from cheap-and-janky to robust. By the end you will have a working 30-line Express middleware you can drop into any SaaS.

Short answer

The cleanest approach in 2026 is a single API call to a maintained disposable-email blocklist (SignupDoggy is the one we built; there are others). One HTTP request, one boolean response, ~50ms latency, $0.01 per call. For 99% of SaaS signup forms this is the right choice.

The DIY approach (maintain your own blocklist from a public GitHub repo) catches ~85% of disposable emails for $0/month. The remaining 15% — the long tail of providers that rotate domains every 48 hours — is what you give up.

The naïve regex approach (just block `tempmail.com`) catches maybe 30% and makes the rest of your signup flow look unprofessional to real users with similar-sounding email addresses.

What a disposable email actually is

A disposable email is a temporary inbox that the user can read for 10 minutes to 24 hours, then it disappears. The user creates one in two clicks at `tempmail.com` or `guerrillamail.com`, uses it to claim your free trial, and never sees your verification email. From your perspective, the user signed up and then went silent — a real conversion event never happens.

The list of disposable email providers is large, growing, and churns quickly. As of June 2026, the maintained public lists contain between 125,000 and 200,000 domains. The top 50 domains account for ~60% of all disposable-email signups. The long tail of 199,950 domains accounts for the other 40%.

This is why a static blocklist with the top 50 is insufficient. A maintained blocklist with the full 200,000 is sufficient but expensive to keep current.

Approach 1: the regex shortcut (do not do this)

// DON'T DO THIS
const BLOCKED = ['tempmail.com', 'guerrillamail.com', 'mailinator.com', '10minutemail.com'];
function isDisposable(email) {
  const domain = email.split('@')[1];
  return BLOCKED.includes(domain);
}

This catches ~30% of disposable emails. It also breaks for users with `tempmail.com.bank.com` (a legitimate email at a bank whose domain contains the string). It does not catch the long tail. It does not update when a new disposable provider launches.

If you ship this, you ship a known-bad version of disposable email detection. Real users get blocked because their email is `tempmail.com.something.else` and your regex is too eager. Real disposable emails get through because they're from `discard.email` which is not in your top-50 list.

Don't ship this.

Approach 2: the public blocklist (catches ~85%)

The best public blocklist is the GitHub repo `disposable-email-domains/disposable-email-domains`. It has 125,000+ domains, MIT-licensed, updated weekly.

A working implementation:

// blocklist.js
import fs from 'node:fs';
import path from 'node:path';

let cache = null;
let cacheTime = 0;
const TTL = 24  60  60  1000; // 24 hours

export async function loadBlocklist() {
  if (cache && Date.now() - cacheTime < TTL) return cache;
  // Either fetch from GitHub or use a vendored copy
  const url = 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposableemailblocklist.conf';
  const text = await (await fetch(url)).text();
  cache = new Set(text.split('
').map(l => l.trim()).filter(Boolean));
  cacheTime = Date.now();
  return cache;
}

export function isDisposable(blocklist, email) {
  const domain = email.split('@')[1]?.toLowerCase();
  return domain && blocklist.has(domain);
}

The 24-hour cache is important. The list is ~5MB, and re-fetching it on every signup will exhaust your API rate limits and add 200ms to the signup.

This catches ~85% of disposable email signups. The 15% it misses is the long tail: per-provider domain crawl that catches addy.io aliases, custom domains set up by `simpleLogin.io` and `33mail.com`, and the dozens of smaller providers that don't make it into the public list.

Approach 3: SignupDoggy (catches ~99.5%)

SignupDoggy maintains a blocklist of 200,000+ domains by syncing from the public GitHub list, the bulk disposable-email API, and 175 individual disposable-email provider crawls. The result is a single API call:

// signup-check.js
import express from 'express';
const app = express();

app.post('/signup', async (req, res) => {
  const { email, ip } = req.body;
  const result = await fetch('https://signupdoggy-api.jeffrinjames99.workers.dev/v1/check', {
    method: 'POST',
    headers: {
      'X-API-KEY': process.env.SIGNUPDOGGYKEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email, ip }),
  }).then(r => r.json());

  if (result.recommendation === 'block') {
    return res.status(400).json({ error: 'Invalid email' });
  }
  // proceed with signup
  res.json({ ok: true });
});

The API call returns a `recommendation: 'allow' | 'review' | 'block'` plus per-signal scores (email risk, IP risk, phone risk if you sent one). $0.01 per call. Sub-50ms p95 latency. No minimum purchase beyond $5.

This is the approach I would ship in 2026 if I were starting a SaaS today.

Approach 4: combine them (the belt-and-suspenders option)

If you are a regulated business (finance, healthcare, marketplaces) you may want both: a local blocklist for the cheap-and-fast check, plus an API call for the second opinion.

// belt-and-suspenders.js
import { loadBlocklist, isDisposable } from './blocklist.js';

app.post('/signup', async (req, res) => {
  const { email, ip } = req.body;
  const blocklist = await loadBlocklist();

  // Cheap local check first
  if (isDisposable(blocklist, email)) {
    return res.status(400).json({ error: 'Please use a permanent email' });
  }

  // Then API check for the long tail
  const result = await fetch('https://signupdoggy-api.jeffrinjames99.workers.dev/v1/check', {
    method: 'POST',
    headers: { 'X-API-KEY': process.env.SIGNUPDOGGYKEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, ip }),
  }).then(r => r.json());

  if (result.recommendation === 'block') {
    return res.status(400).json({ error: 'Invalid signup' });
  }
  res.json({ ok: true });
});

The local check handles 85% in 1ms. The API call handles the remaining 15% in 50ms. Total signup latency overhead: 51ms. Acceptable.

How to integrate this into your existing auth flow

The above Express snippets assume a custom auth handler. If you are using a managed auth provider (Supabase Auth, Auth0, Clerk, NextAuth), you want to validate BEFORE the auth provider creates the user.

For Supabase Auth, the cleanest pattern is a database trigger on `auth.users`:

create or replace function public.checksignupquality()
returns trigger as $
declare
  result jsonb;
begin
  select body into result from httppost(
    'https://signupdoggy-api.jeffrinjames99.workers.dev/v1/check',
    jsonbbuildobject('email', new.email, 'ip', new.rawusermetadata->>'ip'),
    'application/json',
    jsonbbuildobject('X-API-KEY', currentsetting('app.signupdoggykey'))
  );

  if result->>'recommendation' = 'block' then
    raise exception 'Signup blocked: high risk signal';
  end if;

  return new;
end;
$ language plpgsql;

create trigger checksignupqualitytrigger
  before insert on auth.users
  for each row execute function public.checksignupquality();

The trigger blocks the user creation if the recommendation is `block`. Your profiles table never gets the row. Your support inbox never gets the 'I never got my verification email' ticket from `tempmail.com`.

For Auth0, the equivalent is a Post-Login Action or a Pre-User-Creation Action. For Clerk, use a `beforeUserCreate` webhook.

When to skip disposable email detection

If your signup form is on a free tool with zero monetization and no storage cost, the disposable-email problem is a quality issue but not a security one. A `tempmail.com` signup is annoying but not dangerous. You can ship without it.

If your signup form is on a paid product, a marketplace, or anything that stores user-generated content, disposable email detection is mandatory. A throwaway email address is a single-use weapon for spam, harassment, and abuse.

FAQ

Q: Won't blocking disposable emails hurt my conversion rate?
A: It depends on your user base. For B2B SaaS targeting professionals, less than 1% of your real users will be affected. For consumer apps targeting Gen Z, expect 2-5% friction. The right answer is 'block, then offer a one-time override' — a 'use a different email' page is better than a hard error.

Q: What about Apple Hide My Email?
A: Apple Hide My Email generates `xxx@privaterelay.appleid.com` addresses. These are technically disposable but they are also a real human with a real Apple ID on the other end. Most SaaS companies whitelist them. SignupDoggy does not block Apple Private Relay by default — it is in the `review` band, not the `block` band.

Q: What about SimpleLogin and 33mail alias services?
A: These create per-vendor aliases that forward to a real inbox. They are technically disposable but the user is a real person. SignupDoggy puts them in the `review` band by default; you can override per-account if needed.

Q: How often does the blocklist update?
A: The SignupDoggy blocklist is updated daily from the public GitHub list and from a daily crawl of 175 disposable-email provider websites. The full sync runs on a 24-hour cron and is exposed via the founder-only `/v1/admin/sync` endpoint.

Q: Can I whitelist a specific email?
A: Yes. POST to `/v1/whitelist` with the email and it will always return `recommendation: 'allow'`. Useful for VIP customers.

Q: What does the API cost?
A: $0.01 per call. Buy credits at $5/1k, $25/5k, $100/25k. Credits never expire.

---

About the author

Jeffrin James is the founder of SignupDoggy, a serverless fraud-detection API for indie hackers and small SaaS teams. He built the product in Mumbai, India, after spending six months and $2,400 on enterprise fraud-detection vendors that didn't fit his use case.

Tags: Disposable email, Node.js, Tutorial, API