Integration guide · Supabase

How to block disposable emails in Supabase Auth

Supabase Auth can run your code before a user is created. Use that hook to reject throwaway and dead-domain emails, so fake accounts never reach auth.users or your welcome emails.

By the Vetto team · Updated

Quick answer

Add an HTTP Before User Created hook (an Edge Function). For every new signup, call a disposable-email check; if the verdict is “block”, return a 400 with an error object and Supabase stops the signup. If the check times out, return {} and let the user in.

Why a hook and not just the form

A check in your signup form is good UX, but bots can call the Supabase Auth API directly and skip your form. The hook runs inside Supabase’s signup flow, so it covers email, magic link and OAuth signups however they arrive.

1. Create the Edge Function

The hook payload carries the new user in user, with the address in user.email. Supabase signs HTTP hooks with Standard Webhooks, so verify the signature first:

index.ts
// supabase/functions/before-user-created-hook/index.ts
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'

const reject = (message: string) =>
  new Response(JSON.stringify({ error: { http_code: 400, message } }), {
    status: 400,
    headers: { 'Content-Type': 'application/json' },
  })
const allow = () =>
  new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } })

Deno.serve(async (req) => {
  const payload = await req.text()
  const secret = Deno.env.get('BEFORE_USER_CREATED_HOOK_SECRET')!.replace('v1,whsec_', '')

  let user
  try {
    ;({ user } = new Webhook(secret).verify(payload, Object.fromEntries(req.headers)) as any)
  } catch {
    return reject('Invalid request')
  }

  const email = user.email || ''
  if (!email) return allow() // phone signups

  try {
    const res = await fetch('https://usevetto.com/v1/check', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${Deno.env.get('VETTO_SECRET_KEY')}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ email }),
      signal: AbortSignal.timeout(2500), // the hook has 5 s in total
    })
    const v = await res.json()
    if (v.verdict === 'block') return reject(v.message || 'Please use a real email address.')
  } catch {
    // Timeout or network error: fail open, never lock real people out.
  }
  return allow()
})

2. Set the secrets and deploy

terminal
# .env (never commit it)
BEFORE_USER_CREATED_HOOK_SECRET="v1,whsec_..."   # from Authentication → Hooks
VETTO_SECRET_KEY="sk_..."                         # from your Vetto dashboard

supabase secrets set --env-file .env
supabase functions deploy before-user-created-hook --no-verify-jwt

3. Turn the hook on

  1. Supabase Dashboard → Authentication → Hooks → Before User Created.
  2. Choose HTTP, paste your function URL and generate the secret (that is the value for BEFORE_USER_CREATED_HOOK_SECRET).
  3. Sign up with a throwaway address like x7@mailinator.com: you should get the error. A normal Gmail or Outlook address goes through.

What the check catches

  • Throwaway inboxes (mailinator.com, yopmail.com and 9,000+ more), plus domains that live on known throwaway mail servers.
  • Dead domains with no mail server, which can never receive your confirmation email.
  • Repeat signups from the same Gmail inbox through dots, +tags or googlemail.com (returned as a repeat count you can act on).
  • Privacy relays like Apple Hide My Email and DuckDuckGo are allowed: they reach a real person.

Not using a hook? Check from your own API

If you create users from your own backend with auth.admin.createUser or a server action, call the same /v1/check endpoint there before creating the user. For scripted traffic, add Supabase captcha as well. To see how many of your existing users are junk, export them and run the free email list audit.

Related: Firebase · Auth.js / NextAuth · how to detect fake signups · free trial abuse.

Frequently asked questions

Can Supabase block disposable email signups?

Yes. Supabase Auth has a Before User Created hook that runs before the user row exists. Your hook can check the email and return an error to stop the signup. Supabase lists blocking disposable email domains as one of its use cases.

Does the Before User Created hook run for Google or GitHub (OAuth) signups?

Yes. The hook runs for new users from OAuth providers too, and the payload includes the provider in user.app_metadata.provider, so you can treat each provider differently if you want.

Which Supabase plans include the Before User Created hook?

The Supabase docs list it on the Free and Pro plans.

What happens if the check is slow or down?

HTTP hooks have about 5 seconds in total, so the example gives the check 2.5 seconds and then lets the signup through. A slow check should never lock real people out.

Do I need a captcha as well?

They solve different problems. A captcha stops scripts; a domain check stops people (and scripts) using throwaway or dead addresses. Supabase supports hCaptcha and Turnstile for the scripted part.

Give your signup form sharp eyes.

Free on one site. One line of HTML. Remove it any time and your form works exactly as before.

Founding offer: Pro for life for $69 once, first 100 buyers, until Nov 27.