Email validation · Integrations · Supabase

Integration guide

Email validation in Supabase with Trueguard.

To add email validation in Supabase, point the Before User Created hook at an Edge Function that calls the Trueguard email validation API. Invalid and disposable signups are rejected before the user row exists. If the check fails, the signup goes through.

In this guide

Endpoint1 POST
Median responseSub-1s
Free tier100 / month
Standard plan$49.99 / 50,000

How do I validate emails in Supabase Auth?

Supabase calls this Edge Function for each new user. It uses native fetch and one webhook library, so there is nothing to install.

supabase/functions/before-user-created-hook/index.ts

import { Webhook } from "https://esm.sh/standardwebhooks@1.0.0"; const API = "https://api.trueguard.io/v2/email/validation"; const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" }, }); const allow = () => json({}); // Keep status 200: Supabase Auth turns a 400 from a hook // into a generic 500 and your message is lost. const reject = (message: string) => json({ error: { http_code: 400, message } }); Deno.serve(async (req) => { const payload = await req.text(); let email: string | undefined; try { const secret = Deno.env .get("BEFORE_USER_CREATED_HOOK_SECRET")! .replace("v1,whsec_", ""); const event = new Webhook(secret).verify( payload, Object.fromEntries(req.headers), ) as { user: { email?: string } }; email = event.user.email; } catch { // Not proven to be from Supabase, so this never fails open. return json( { error: { http_code: 401, message: "Invalid signature" } }, 401, ); } // Phone and anonymous signups have no email. if (!email) { return allow(); } try { const res = await fetch(API, { method: "POST", headers: { "X-API-KEY": Deno.env.get("TRUEGUARD_KEY")!, "Content-Type": "application/json", }, body: JSON.stringify({ email }), signal: AbortSignal.timeout(3000), }); if (!res.ok) { console.error("Trueguard returned", res.status); return allow(); } const { deliverability, quality } = await res.json(); if (deliverability.status === "invalid") { return reject( "We can't deliver to this address. Check it for typos.", ); } if (quality.isDisposable) { return reject( "Temporary email addresses can't be used here. Please use a permanent one.", ); } return allow(); } catch (err) { // Timeout or network error: let the signup through. console.error("Trueguard call failed", err); return allow(); } });

Add your key and the next new user is checked. 100 validations a month are free, no card.

Get your free API key

One header, one body field

Set X-API-KEY to your key and POST a JSON body with a single email field. HTTPS only. The response carries the seven signals plus one deliverability status.

Response fields you will branch on

deliverability.status

safe, risky, invalid or unknown. This is the field to branch on.

syntax.isValid

RFC 5322 format check. Deterministic, so a false here is always a typo or a malformed address.

quality.isDisposable

True for throwaway providers. Usually a hard block at signup.

deliverability.isCatchall

True when the domain accepts any address. The status is then risky, unless a second check confirmed or ruled out the mailbox.

quality.isRole

info@, support@, sales@. Route these differently rather than rejecting them.

Run it once before you write any code.

The same endpoint the snippet above calls. 10 free checks a day, no key needed.

Or try

Where does the email check go in Supabase?

Before User Created runs on Free and Pro plans, as a Postgres hook or an HTTP hook. Only the HTTP hook can wait for a third-party API.

Postgres hook with pg_net

A Postgres hook has 2 seconds, and pg_net is asynchronous, so it cannot hand a verdict back to the waiting hook.

HTTP hook on an Edge Function

An HTTP hook should complete in 5 seconds and can wait for the answer. Your Trueguard key stays in Supabase secrets, and the check finishes before the user row exists.

What the endpoint returns

{ "email": "founder@yourcompany.com", "rawEmail": "founder@yourcompany.com", "syntax": { "isValid": true }, "deliverability": { "status": "safe", "isSmtpValid": true, "isMxValid": true, "isCatchall": false, "isInboxFull": false, "isDeliverable": true, "isDisabled": false, "mxRecords": ["mx1.yourcompany.com", "mx2.yourcompany.com"] }, "domain": { "name": "yourcompany.com", "age": 2841, "isLive": true, "isRisky": false }, "quality": { "isDisposable": false, "isFree": false, "isRole": false, "isSubaddress": false } }

How do I enable the hook in Supabase?

Five steps, and the hook is off until your secrets are in place.

  1. 01

    Create the function.

    Replace the generated index.ts with the hook above. The template wraps the handler in withSupabase(...). This hook uses plain Deno.serve and verifies the signature itself.
    supabase functions new before-user-created-hook
  2. 02

    Set your two secrets.

    In the dashboard, open Authentication > Hooks, add Before User Created as an HTTP hook, and generate the secret. Keep the hook off until step 4. Your Trueguard key comes from the Trueguard dashboard.
    # .env, keep it out of git BEFORE_USER_CREATED_HOOK_SECRET="v1,whsec_..." TRUEGUARD_KEY="your_api_key" supabase secrets set --env-file .env
  3. 03

    Deploy without JWT verification.

    The hook runs before a JWT exists. The Standard Webhooks signature authenticates the call instead.
    supabase functions deploy before-user-created-hook --no-verify-jwt
  4. 04

    Enable the hook.

    In Authentication > Hooks, set the URL to https://<project-ref>.supabase.co/functions/v1/before-user-created-hook and turn the hook on.
  5. 05

    Test both paths.

    Sign up with a disposable address and confirm your message appears. Sign up with a real address and confirm the user appears. If every signup returns a 500, check the hook secret first: a signature failure is the one path that fails closed.

Which signups should the hook reject?

Reject two cases: an address that cannot receive mail and a temporary inbox. Let everything else through, including role addresses and catch-all domains, because those can belong to paying customers.

SignalHook actionWhy
invalidRejectThe address cannot receive mail. Malformed addresses land here too: the API answers 200 with status invalid, so the hook needs no regex.
quality.isDisposable (any status)RejectA temporary inbox. Checked after invalid, so the flag applies even when the status is unknown.
safeAllowThe mailbox can receive mail.
safe with isCatchall: trueAllowTrueguard's advanced catch-all detection validated the mailbox on a catch-all domain.
risky with isCatchall: trueAllowThe domain accepts all mail and the mailbox was not validated. Real people use these domains.
risky, high-risk TLDAllowA weak reason to lose a signup on its own.
unknownAllowThe mail server gave no clear answer, which says nothing about the person.
quality.isRoleAllowNever changes the status. team@ can be a paying customer.
quality.isFreeAllowNever changes the status. Personal addresses are normal signups.
Trueguard error (401, 403, 429, 5xx) or no answer in 3 sAllow and logYour quota or an outage should not cost you a signup.
No user.emailAllow, no API callPhone and anonymous signups. No validation used.
Signature check failsReject (401)The request is not proven to come from Supabase.

How do I avoid blocking real users?

Fail open on anything that is not a verdict.

A timeout, 401, 403, 429 or 5xx returns {}. The error goes to your Edge Function logs, so check them. On the free plan, signups past 100 a month pass unchecked.

Reject two cases only.

invalid and disposable. unknown, role, free and an unvalidated catch-all all pass.

Write a message the user can act on.

The text reaches your signup form. Tell people what to change: fix a typo or use a permanent address.

A hook can only allow or reject.

The user row does not exist yet, so there is nothing to flag or tag. To watch risky signups, run a separate check in your backend once the account exists.

Why must a Supabase hook reject with status 200?

Respond with HTTP 200 and an error object. The Before User Created docs show an example that returns status 400, but the current hookshttp.go turns a 400 into a generic 500, and the hooks overview status table says the same. Read on 2026-11-08. Every response needs Content-Type: application/json.

const body = JSON.stringify({ error: { http_code: 400, message: "Please use a permanent email." }, }); const headers = { "Content-Type": "application/json" }; // Status 400: the client gets a generic 500. return new Response(body, { status: 400, headers }); // Status 200: the client gets your message. return new Response(body, { status: 200, headers });

Status

What Supabase Auth does

200 {}

The signup continues.

200 error

The signup is rejected and your http_code and message go to the client. An empty message is ignored.

204

The signup continues.

400

Generic 500. Your message is lost.

401

Generic 500. This hook uses it only for a bad signature.

429/503

Auth retries, up to 3 times inside the 5 seconds.

Other

Generic 500.

Which Supabase signups run the hook?

Read from the supabase/auth source on 2026-11-08. Supabase does not document this list, so re-test after an Auth upgrade.

Email and passwordEmail OTPMagic linkOAuthWeb3PhoneAnonymousAdmin invite

Email, OTP and magic link

The hook runs for a new user only. Returning users cost no validations.

Phone and anonymous

The email can be empty. The code returns an empty object before calling Trueguard, so no validation is used.

OAuth and Web3

The hook runs when the sign-in creates a new account. If there is no email, the check is skipped.

Admin invite and createUser

An invite fires the hook. We found no hook call in admin createUser, so those accounts go unchecked.

What does each Supabase signup cost?

Only new-user signups fire the hook, so logins cost nothing. The free plan covers 100 signup checks a month. Standard is $49.99 for 50,000, which is $0.001 per signup at full use. Past that it is $0.001 per check, so 60,000 signups cost $49.99 + 10,000 × $0.001 = $59.99. Every check counts, rejected addresses included. A rejected address leaves no user row, so a retry is a new check.

Free

$0 forever

100 validations / month

Overage: n/a

Get started free

Standard

Popular

$49.99 / month

50,000 validations / month

Overage: $0.001 per validation

Get your API key

Custom

Contact us

500,000+ validations / month

Overage: Negotiated

Talk to us

Frequently asked questions

The full field reference lives in the API documentation.

How do I block disposable emails in Supabase?

Use Trueguard in a Supabase Before User Created hook. The hook sends each new address to the Trueguard email validation API and rejects the signup when deliverability.status is invalid or quality.isDisposable is true. The user row is never created.

Does this work on the Supabase Free plan?

Yes. Before User Created is available on Free and Pro plans. Trueguard's free plan adds 100 validations a month with no card.

Why does my hook return a 500 when I reject a signup?

Your function probably responded with status 400. Supabase Auth turns that into a generic 500. Respond with status 200 and a body of error.http_code and error.message, and the client receives your message.

What happens if Trueguard is slow or down?

The signup goes through. The hook waits 3 seconds, inside Supabase's 5 second limit, and returns an empty object on a timeout or API error. The median Trueguard response is under a second.

Should I reject risky or catch-all addresses?

Reject a risky address only when it is disposable. Trueguard's advanced catch-all detection can validate a mailbox on a catch-all domain: validated comes back safe with isCatchall true, and not validated comes back risky. This hook allows both.

Can I use a Postgres hook instead?

Not for this check. Postgres hooks have 2 seconds, and pg_net is asynchronous, so the function cannot wait for the API's answer. Use the HTTP hook.

Does every login use a validation?

No. The hook fires for new users only. Phone and anonymous signups with no email skip the API call. Rejected addresses do count, as does each retry of one.

Stop fake Supabase signups before the row exists.

100 validations a month, free, no card. Deploy the hook, add your key, and the next new user is checked.

Get your free API key

No credit card required.

trueguard-logo© 2026 Trueguardinfo@trueguard.io