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
import { createClient } from 'npm:@supabase/supabase-js@2'
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:
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
profilestable and follow Never block a signup. - Deleted users: remove their row from
signup_emailsonly 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.