CheckDisposable Emailcheckdisposable.email
← All guidesBetter Auth guide · TypeScript

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 →