Block disposable email signups with Better Auth
Better Auth fires a `databaseHooks.user.create.before` hook for every signup path — email/password, Google OAuth, magic link, anything. Add a single check to this hook and you cover them all.
The code
// lib/auth.ts
import { betterAuth } from 'better-auth';
import { APIError } from 'better-auth/api';
async function isDisposable(email: string) {
try {
const r = await fetch(
`https://api.checkdisposable.email/v1/check?email=${encodeURIComponent(email)}`,
{ headers: { Authorization: `Bearer ${process.env.CDE_KEY!}` } }
);
if (!r.ok) return false;
const data = await r.json();
return data.is_disposable === true;
} catch {
return false;
}
}
export const auth = betterAuth({
// ...your existing config
databaseHooks: {
user: {
create: {
before: async (user) => {
if (await isDisposable(user.email)) {
throw new APIError('BAD_REQUEST', {
message: 'Please use a real email address.',
code: 'DISPOSABLE_EMAIL',
});
}
return { data: user };
},
},
},
},
});Notes
- Covers every signup path
- The hook fires for email+password signup, Google OAuth, Apple, magic link, passkey signup, and any future Better Auth provider. One check, no scattered call sites.
- How the error reaches the client
- Better Auth's authClient surfaces the thrown APIError as `res.error.message`. Catch it in your signup form and render the message — no special-case branching needed.
- We dogfood this exact pattern
- CheckDisposable Email itself uses this hook on our own signup flow. You can't sign up for our service with a mailinator.com address — same check, same code.
Get a free API key
500 checks/month, no credit card. No credit card. 30 seconds.
Sign up free →