Block fake signups in Clerk and Supabase Auth
Clerk and Supabase ship a static disposable-email list - it misses fresh throwaway domains. Add a Mailsurity check via webhook or edge function to catch the rest.
What Clerk and Supabase give you out of the box
Both Clerk and Supabase can block disposable emails, but off a static list. That catches the well-known providers and misses the long tail: freshly registered throwaway domains, and the thousands of rotating domains that all point at the same handful of throwaway mail servers. Those sail straight through a built-in list.
Clerk: check on the user.created webhook
Add a webhook on Clerk's user.created event. Always verify the signature with svix first (reject forged calls), then run the email through the Mailsurity SDK and ban the user if it's disposable. This layers on top of Clerk's own filtering rather than replacing it. You'll need npm i mailsurity svix @clerk/nextjs and your CLERK_WEBHOOK_SECRET.
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { clerkClient } from '@clerk/nextjs/server';
import { Mailsurity } from 'mailsurity';
const ms = new Mailsurity({ apiKey: process.env.MAILSURITY_API_KEY! });
export async function POST(req: Request) {
// 1. Verify the webhook signature and reject forged calls.
const payload = await req.text();
const h = await headers();
let evt: any;
try {
evt = new Webhook(process.env.CLERK_WEBHOOK_SECRET!).verify(payload, {
'svix-id': h.get('svix-id')!,
'svix-timestamp': h.get('svix-timestamp')!,
'svix-signature': h.get('svix-signature')!,
});
} catch {
return new Response('Invalid signature', { status: 400 });
}
if (evt.type !== 'user.created') return Response.json({ ok: true });
// 2. Check the email; ban the user if it's disposable.
const email = evt.data.email_addresses?.[0]?.email_address;
if (email && (await ms.isDisposable(email))) {
await (await clerkClient()).users.banUser({ userId: evt.data.id });
}
return Response.json({ ok: true });
}Clerk: block before the account is created (custom sign-up UI)
The webhook approach above creates the user, then bans them. That's fine for Clerk's hosted <SignUp/> component, which doesn't let you intercept the form before submission. If you build your own sign-up form with useSignUp(), you can check the email before calling signUp.create() and skip creating (and cleaning up) the account at all.
import { mailsurity } from '@/lib/mailsurity';
export async function POST(req: Request) {
const { email } = await req.json();
if (await mailsurity.isDisposable(email)) {
return Response.json(
{ error: 'Please use a permanent email address.' },
{ status: 422 },
);
}
return Response.json({ ok: true });
}const { isLoaded, signUp } = useSignUp();
async function handleSubmit(email: string, password: string) {
if (!isLoaded) return;
// Check before calling Clerk, to block the signup pre-creation.
const res = await fetch('/api/check-signup', {
method: 'POST',
body: JSON.stringify({ email }),
});
const { error } = await res.json();
if (error) return setFormError(error);
await signUp.create({ emailAddress: email, password });
// ...continue Clerk's verification flow
}Supabase: check in an Edge Function
With Supabase, call the check from your signup Edge Function before the insert (or from a trigger on auth.users), and reject the request for disposable addresses.
import { Mailsurity } from 'npm:mailsurity';
const ms = new Mailsurity({ apiKey: Deno.env.get('MAILSURITY_API_KEY')! });
Deno.serve(async (req) => {
const { email } = await req.json();
if (await ms.isDisposable(email)) {
return new Response(
JSON.stringify({ error: 'Disposable email not allowed' }),
{ status: 422, headers: { 'Content-Type': 'application/json' } },
);
}
return new Response(JSON.stringify({ ok: true }), {
headers: { 'Content-Type': 'application/json' },
});
});The point: catch what the built-in list misses
Mailsurity fingerprints the mail servers behind throwaway addresses, so a brand-new disposable domain is flagged the moment it points at a known throwaway host, without waiting for a list to be updated. Run the audit on your real signups to see exactly how many are getting past your current auth setup.
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.