How to block signup fraud on Vercel
Deploy a Vercel signup route that blocks disposable-email signup fraud in one API call, using the open-source vercel-signup-guard example.
Last updated July 25, 2026.
Disposable emails are the easiest signup-fraud vector on Vercel
A Next.js app deployed to Vercel usually accepts signups through a route handler running on Vercel's serverless functions. Those functions are stateless and request-scoped, so there's no long-lived server to bolt a background job onto. That makes throwaway email addresses an easy way to run up fake signups: a bot fills out the form, gets a working inbox from a rotating temp-mail domain, and your activation metrics and free-tier limits take the hit. That's signup fraud, and blocking it has to happen inline, in the same request that creates the user, with no added infrastructure. (For the broader cost picture, including free-trial abuse, credit draining, and seat inflation, see how disposable emails drive SaaS signup fraud.)
Step 1: start from the example
The vercel-signup-guard example is a minimal Next.js app with exactly this: a signup form, a route handler, and a Mailsurity check in between. Clone it and run it locally first.
git clone https://github.com/akshaybhange/disposable-checker
cd disposable-checker/examples/vercel-signup-guard
npm install
cp .env.example .env.local # paste your MAILSURITY_API_KEY
npm run devTry you@gmail.com (allowed) and you@mailinator.com (blocked) to see the check work before you deploy anything.
Step 2: the guard in two files
lib/mailsurity.ts: the shared client
This file creates one shared, server-side client. Keep it server-side: your API key is a secret, and the SDK throws if it's ever evaluated in a browser context. This is the same client the Mailsurity npm SDK quickstart walks through in more depth, including the full mailsurity package API.
import { Mailsurity } from 'mailsurity';
/**
* One shared, server-side Mailsurity client. The API key is secret — never
* import this into a Client Component. Fail-open is on by default, so a
* transient outage never blocks a real signup.
*/
export const mailsurity = new Mailsurity({
apiKey: process.env.MAILSURITY_API_KEY!,
});app/api/signup/route.ts: the route handler
This is the route handler Vercel runs on every signup request. Three checks gate the success path, in order: the body has to parse as JSON, email has to be present and a string, and mailsurity.isDisposable(email) has to come back false. The first two failures return a 400, the third returns a 422; only once all three checks pass does the route fall through to the normal success path that creates the user.
import { mailsurity } from '@/lib/mailsurity';
/**
* Signup endpoint. Rejects disposable / throwaway email addresses before
* creating the user. The disposable check fails open — if Mailsurity is
* unreachable, the signup proceeds rather than blocking a real customer.
*/
export async function POST(req: Request) {
let email: string | undefined;
try {
({ email } = await req.json());
} catch {
return Response.json({ error: 'Invalid JSON body.' }, { status: 400 });
}
if (!email || typeof email !== 'string') {
return Response.json({ error: 'Email is required.' }, { status: 400 });
}
if (await mailsurity.isDisposable(email)) {
return Response.json(
{ error: 'Please use a permanent email address.' },
{ status: 422 },
);
}
// ...create the user as normal. (Demo just echoes success.)
return Response.json({ ok: true, email });
}Step 3: deploy to Vercel
The example's README has a one-click "Deploy with Vercel" button that clones the examples/vercel-signup-guard folder into a new Vercel project and prompts you for the one required environment variable, MAILSURITY_API_KEY, before the build completes. From the CLI, the equivalent is adding the variable explicitly before you deploy:
vercel env add MAILSURITY_API_KEY
vercel deployEither way, nothing else needs configuring: there's no database, no queue, and no cron job for this guard to run.
Vercel-specific details worth getting right
Three things about running this on Vercel specifically, rather than on a generic Node server:
Preview and Production don't share environment variables. MAILSURITY_API_KEY added via the one-click deploy or a bare vercel env add only lands in whichever environment you pick when prompted. Every branch push gets its own Preview Deployment, and a Preview Deployment missing the key won't error loudly, it fails open and quietly lets every email through, since a missing key surfaces as an auth failure that the SDK treats the same as an outage. Scope the key to all three environments explicitly so a preview branch doesn't silently stop checking:
vercel env add MAILSURITY_API_KEY development
vercel env add MAILSURITY_API_KEY preview
vercel env add MAILSURITY_API_KEY productionCold starts eat into your function's budget, not the guard's. The SDK's 2-second timeout starts when the Mailsurity request goes out, not when Vercel spins up your function. A cold start on an idle route adds latency before the guard even runs, so it counts against your own function's execution limit. Picking a function region close to where Mailsurity's API actually runs shaves a little more off that round trip, though at p99 under 100ms it rarely matters unless the function is already cold.
Test the guard on the Preview Deployment URL, not just locally. A local .env.local and a Preview Deployment's environment variables are two different things. Before merging, open the branch's Preview Deployment URL and try you@mailinator.com again. If it's allowed through where it should be blocked, that's almost always a missing Preview-scoped MAILSURITY_API_KEY, not a bug in the guard itself.
Node.js runtime, not Edge
The route handler above uses Next.js' default Node.js runtime. There's no export const runtime = 'edge' in the example, and you don't need one: the SDK's calls are plain fetch requests, which work identically on Node.js or Edge functions. Picking a runtime here is about your own app's needs elsewhere, not a requirement of the disposable-email check.
Why fail-open matters here especially
The SDK fails open by default: on a timeout, network error, 5xx, or even a 402 (out of Mailsurity credits), isDisposable() resolves to false instead of throwing, so an outage or a billing gap never turns into rejected real signups. That matters more on Vercel than on a server you control, since a serverless function has a hard execution limit and no retry queue of its own, so the check needs to fail safe within that window rather than hang the request. The default request timeout is 2 seconds, and Mailsurity's own p99 latency is under 100ms, comfortably inside it.
Why this catches more than a static blocklist
A hardcoded list of bad domains only knows what was true when you wrote it. Mailsurity checks against 220k+ tracked disposable domains through a layered detection cascade: per-team rules, a curated legit-domain list, the tracked-domain list itself, cached verdicts, and finally live heuristics for anything in between. That way a brand-new throwaway domain still gets caught instead of slipping through until someone updates a .json file. The core check is the same one used in the general Next.js disposable-email guide; this guide is about wiring it into a route handler you actually ship to Vercel.
FAQ
Does blocking disposable emails slow down my Vercel signup route?
Not on a warm function — Mailsurity's own p99 latency (under 100ms) sits comfortably inside the SDK's 2-second fail-open budget. The one Vercel-specific exception is a cold start, covered above: if the guard seems to add real latency, check whether that's a cold invocation before assuming Mailsurity itself is slow.
What happens if my Mailsurity credits run out mid-launch?
Signups keep working — a 402 hits the same fail-open path as a timeout or 5xx, covered above. The practical move is to not find out from a launch-day traffic spike: check your credit balance on the dashboard's analytics tab before you expect a surge, since the 500 free credits (no card required) are meant for testing the guard end-to-end, not for absorbing a real launch.
Can I run this check from a Server Action instead of a Route Handler?
Yes. mailsurity.isDisposable(email) is just a server-side async call, so a form bound to a Server Action can call it directly before creating the user, no fetch and no separate /api/signup endpoint needed. The example above uses a Route Handler instead because it's called from a plain fetch in app/page.tsx and because a dedicated endpoint is easier to reuse if you ever need to validate a signup from somewhere other than that one form.
See how many disposable signups slip past your current setup
Paste a list of your signups. Mailsurity shows how many a standard blocklist misses that we catch by mail-server fingerprint. No signup needed; the part before the @ never leaves your browser.
Need the full field and error reference? See the API reference.