Signup protection recipe
Block fake signups at the edge with a Cloudflare Worker
A Worker on your sign-up route reads the email from the JSON or form body, checks it with SimpleVerifier, answers 422 for disposable, invalid and misspelled addresses, and forwards everything else to your origin with an x-email-check header your app can store. Your app code doesn't change.
- Where it runs
- Runs in front of your origin, so blocked sign-ups never reach your app or database.
- Timeout
- The Worker waits up to 8 seconds and then forwards the request unchecked.
You need an API key from the API keys page (Pro, Business or Agency), stored server-side as SIMPLEVERIFIER_KEY. Reviewed 2026-10-31.
Block, warn or allow
- Block: disposable or temporary inbox (reason_code "disposable" (action "do_not_send"))
- Block: invalid: bad format, no mail server, mailbox not found (action "do_not_send" (reason_code "syntax", "no_mx", "mailbox_not_found", "domain_not_hosted"))
- Block: misspelled domain (gmial.com, hotmial.com) (reason_code "typo_domain" (action "do_not_send"))
- Warn: role inbox (info@, sales@, admin@) (reason_code "role")
- Warn: catch-all domain (reason_code "catch_all")
- Warn: looks invented (test@, john.doe@, example.com) (likely_invented true)
- Allow: valid, or the check could not finish (action "send", "send_with_care", "retry_later", or any non-2xx answer or timeout)
The reasoning for each row is on the signup protection overview.
1. Route the Worker onto your sign-up path
The key is a Worker secret: wrangler secret put SIMPLEVERIFIER_KEY.
wrangler.toml name = "signup-guard" main = "src/index.js" compatibility_date = "2026-10-01" routes = [{ pattern = "app.example.com/api/signup", zone_name = "example.com" }]2. Write the Worker
It reads a clone of the body, so the original request goes to your origin untouched.
src/index.js const API_URL = "https://simpleverifier.com/api/v1/verify"; const WARN_CODES = new Set(["role", "catch_all"]); const ALLOW = { decision: "allow", reasonCode: null, message: null }; function blockMessage(reasonCode, reason) { if (reasonCode === "disposable") return "Please use a permanent email address, not a temporary inbox."; const hint = reason.match(/did you mean ([^?\s]+)\??/i)?.[1]; if (hint) return `Check the spelling of your email address. Did you mean ${hint}?`; return "This email address can't receive mail. Please check it."; } async function checkSignupEmail(email, apiKey, timeoutMs = 8000) { if (!apiKey || !email) return ALLOW; let result; try { const res = await fetch(API_URL, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim() }), signal: AbortSignal.timeout(timeoutMs), }); if (!res.ok) return ALLOW; // fail open result = await res.json(); } catch { return ALLOW; // timeout or network error: fail open } const reasonCode = result.reason_code ?? null; if (result.action === "do_not_send") { return { decision: "block", reasonCode, message: blockMessage(reasonCode ?? "", result.reason ?? "") }; } if (result.likely_invented || WARN_CODES.has(reasonCode)) return { decision: "warn", reasonCode, message: null }; return { decision: "allow", reasonCode, message: null }; } async function emailFrom(request) { const type = request.headers.get("content-type") ?? ""; try { if (type.includes("application/json")) return String((await request.json()).email ?? ""); if (type.includes("form")) return String((await request.formData()).get("email") ?? ""); } catch {} return ""; } export default { async fetch(request, env) { if (request.method !== "POST") return fetch(request); const email = await emailFrom(request.clone()); const verdict = await checkSignupEmail(email, env.SIMPLEVERIFIER_KEY); if (verdict.decision === "block" && env.SIGNUP_GUARD_DRY_RUN !== "1") { return Response.json({ error: verdict.message, field: "email" }, { status: 422 }); } const forwarded = new Request(request); forwarded.headers.set("x-email-check", verdict.decision); if (verdict.reasonCode) forwarded.headers.set("x-email-reason", verdict.reasonCode); return fetch(forwarded); }, };3. Deploy
Strip x-email-check from incoming requests at your origin if it is reachable without the Worker, so nobody can set it themselves.
npx wrangler secret put SIMPLEVERIFIER_KEY npx wrangler deploy
Common questions
- What does a browser form see on a 422?
- A JSON error. If your form posts natively (no JavaScript), return a redirect back to the form with the message in the query string instead of Response.json.
- Does the Worker count toward my API calls for every request?
- Only for POST requests on the routed path that carry an email, one call each. GET requests and other paths pass straight through.
- Which plan includes the API?
- Pro ($29.99/month, 10,000 calls a month included), Business and Agency. Each address checked is one call. Starter and Growth have no API access.
- How do I try it without blocking anyone?
- Set SIGNUP_GUARD_DRY_RUN=1. The code logs what it would block and lets everyone through. Read the logs for a few days, then remove the variable.
Other recipes
- Block fake and disposable signups in Supabase Auth
- Block fake and disposable signups in Clerk
- Block fake and disposable signups in Firebase Auth
- Block fake and disposable signups in Next.js
- Express middleware that blocks fake signups
- Filter fake form signups in Zapier or n8n
- Filter fake Webflow and Framer form signups