Skip to main content

Check signup emails in Supabase Auth

Supabase Auth can call an HTTP endpoint before it creates a user, with the Before User Created hook. This guide makes that endpoint an Edge Function that asks Signitize about the email. The hook covers every way of signing up (email and password, magic link, OAuth), with no change to your frontend.

1. A table for canonical emails​

Run in the SQL editor:

create table public.signup_emails (
canonical_email text primary key,
email text not null,
created_at timestamptz not null default now()
);

alter table public.signup_emails enable row level security;

Row level security with no policies keeps the table private; the Edge Function uses the service role. The table remembers which inbox already signed up, so [email protected] is refused once [email protected] has an account.

2. The Edge Function​

supabase functions new before-user-created
supabase/functions/before-user-created/index.ts
import { createClient } from 'npm:@supabase/supabase-js@2'
import { Webhook } from 'https://esm.sh/[email protected]'

const webhook = new Webhook(Deno.env.get('BEFORE_USER_CREATED_HOOK_SECRET')!.replace('v1,whsec_', ''))
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!)

const allow = () => Response.json({})
const reject = (message: string) => Response.json({ error: { http_code: 400, message } }, { status: 400 })

async function checkEmail(email: string) {
try {
const response = await fetch('https://api.signitize.io/v1/check', {
method: 'POST',
headers: {
Authorization: `Bearer ${Deno.env.get('SIGNITIZE_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
})
if (response.ok) return { status: 'checked', ...(await response.json()) }
if (response.status === 400) return { status: 'invalid' }
} catch {
// timeout or network error
}
return { status: 'unavailable' }
}

Deno.serve(async (req) => {
const payload = await req.text()
let email: string | undefined
try {
email = (webhook.verify(payload, Object.fromEntries(req.headers)) as { user: { email?: string } }).user.email
} catch {
return Response.json({ error: { message: 'Invalid signature' } }, { status: 401 })
}
if (!email) return allow() // phone signups have no email

const check = await checkEmail(email)
if (check.status === 'invalid') return reject('Enter a valid email address.')
if (check.status !== 'checked') return allow() // Signitize unavailable: never block a signup

if (check.disposable) return reject('Please use a permanent email address.')
if (!check.deliverable) return reject('This email address cannot receive mail. Please check it for typos.')

const { data: existing } = await supabase
.from('signup_emails')
.select('email')
.eq('canonical_email', check.email)
.maybeSingle()
if (existing && existing.email !== email) return reject('You already have an account.')

await supabase.from('signup_emails').upsert({ canonical_email: check.email, email })
return allow()
})

The same typed address may sign up again (for example after a failed first attempt); a different alias of the same inbox is refused.

3. Secrets and deploy​

In the Supabase dashboard, open Authentication > Hooks, add a Before User Created hook of type HTTPS and generate its secret. Then:

.env
SIGNITIZE_API_KEY=sk_live_...
BEFORE_USER_CREATED_HOOK_SECRET=v1,whsec_...
supabase secrets set --env-file .env
supabase functions deploy before-user-created --no-verify-jwt

Set the hook URL to https://<project-ref>.supabase.co/functions/v1/before-user-created and enable it. --no-verify-jwt is needed because Supabase Auth signs the request with the hook secret, not with a user JWT; the function verifies that signature itself.

4. Show the message​

supabase.auth.signUp() returns the hook's message as the error:

const { error } = await supabase.auth.signUp({ email, password })
if (error) showError(error.message) // "Please use a permanent email address."

Notes​

  • Failing open: when Signitize doesn't answer within 3 seconds, the user is created unchecked. To recheck those users later, store a flag in a profiles table and follow Never block a signup.
  • Deleted users: remove their row from signup_emails only if you want the inbox to be able to sign up again. Keep it for banned users.
  • Mail always goes to the address the user typed; Supabase handles that.