Platform guides
Block disposable emails in Supabase Auth
Use IronDome with Supabase’s Before User Created hook to reject disposable-email signups. Follow setup, test outcomes and troubleshoot failures.
On this page
Use IronDome as a Supabase Auth Before User Created HTTP hook. Supabase sends a signed signup event to IronDome before creating the user. IronDome classifies the email and allows or rejects that signup.
This integration requires no customer-hosted Edge Function, Supabase service-role key, or API key in the hook payload.
Before you start
You need an IronDome account with Supabase integration access, your Supabase project reference, and permission to configure that project's Auth hooks. Test in a development project before enabling production signup protection.
The hook applies to creation of new users. It does not rescreen existing users, sign-ins, or email changes. Phone-only and anonymous users without an email proceed without an email classification.
Create an installation
Installation management uses your signed-in account session at app.irondome.dev, not an email-check API key. Each installation belongs to your account and one Supabase project.
Setup currently uses the management API rather than a dashboard setup wizard. Sign in at https://app.irondome.dev, open that page's browser developer tools, and use its Console in the top-level app context. Run the helper below there, not in your Supabase project, application frontend, or terminal. Mutations require X-IDM-Request: 1; the browser supplies the matching Origin and session cookie. A stale session may require signing in again.
async function manageInstallation(path, input) {
const response = await fetch(path, {
method: input === undefined ? 'GET' : 'POST',
credentials: 'same-origin',
headers: input === undefined ? {} : {
'Content-Type': 'application/json',
'X-IDM-Request': '1',
},
...(input === undefined ? {} : { body: JSON.stringify(input) }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error?.code ?? 'installation_request_failed');
}
return result;
}
const installation = await manageInstallation('/api/integrations/supabase', {
project_ref: 'YOUR_PROJECT_REF',
});
Replace YOUR_PROJECT_REF with your project's 20-character lowercase reference. The response contains id, url, secret, and publication_status. Save the ID and signing secret securely. Do not log the response or send its secret to your application's frontend.
The returned hook URL has this form:
https://api.irondome.dev/v1/hooks/supabase/INSTALLATION_ID/before-user-created
Use the returned URL exactly. Do not put your API key or signing secret in the URL. The signing secret is a full value beginning with v1,whsec_; it is separate from an API key.
Wait for publication
Creation is asynchronous. Read your installations with:
const { installations } =
await manageInstallation('/api/integrations/supabase');
Find the returned ID and inspect publication.publication_status. Wait for published before enabling the hook. A pending response means publication is not complete; creating more installations does not fix it.
If publication remains pending or health becomes critical, keep the hook disabled and contact support. Publication acknowledgement does not establish that every edge location has observed a change.
Configure Supabase Auth
- In the Supabase Dashboard, open Authentication → Hooks and configure Before User Created as an HTTP hook.
- Paste the installation's returned hook URL.
- Set the signing secret to the complete returned
secretvalue, includingv1,whsec_. - Enable the hook after IronDome publication completes.
If your project already has a Before User Created policy, review how to compose it rather than replacing it without preserving its behavior. See Supabase's hook guide for project configuration.
Signup outcomes
| Outcome | Hook response | Signup result |
|---|---|---|
| Non-disposable email | HTTP 204 |
Proceed with normal account creation |
| Disposable email | HTTP 200, error.http_code: 403 |
Reject and ask for a permanent address |
| Invalid email | HTTP 200, error.http_code: 400 |
Reject and ask for a corrected address |
| Free quota exhausted | HTTP 200, error.http_code: 429 |
Reject until quota or authorized plan changes |
| Authorization or service unavailable | HTTP 200, error.http_code: 503 |
Reject; user can try later |
| Invalid signature or malformed event | Transport HTTP 401 or 400 |
Reject the delivery |
Supabase interprets the error envelope: transport HTTP 200 does not always mean signup is allowed. Do not substitute an unchecked allow response when protection is unavailable.
Successful classifications, including disposable outcomes, use your account's plan. Free hook usage shares the organization's monthly allowance with API checks. Do not add another API check for the same signup when this hook already enforces it. See plans and limits.
Test the hook before production
Use an authorized development project. Confirm that its IronDome installation is published, its hook is configured, and your application's Supabase client points to that same project. Choose two fresh, unregistered addresses you control: one permanent and one known disposable address. Reusing an existing user can make the result ambiguous; a made-up example.com address is not a detector test fixture.
Run one signup at a time through your development application's normal signup flow. For an email/password flow, use its existing client with Supabase's signUp method, not the IronDome management console:
// Development app only. Supply TEST_EMAIL and TEST_PASSWORD locally.
const { data, error } = await supabase.auth.signUp({
email: TEST_EMAIL,
password: TEST_PASSWORD,
});
console.log({
userReturned: Boolean(data.user),
errorStatus: error?.status ?? null,
errorCode: error?.code ?? null,
});
// Do not log sessions, tokens, passwords, or raw user records.
Check the client result and Authentication → Users in the same development project:
- Permanent address: signup proceeds and the user exists. A missing session can be normal when email confirmation is required.
- Disposable address: the client reports the disposable-email rejection and no user is created. An unrelated signup error is not proof that the hook blocked the address; exact client error codes can vary by Supabase version.
The disposable outcome has this hook callback body, sent with transport HTTP 200:
{
"error": {
"http_code": 403,
"message": "Disposable email addresses are not allowed. Please use a permanent email address."
}
}
This is not the browser's signup response: Supabase interprets the hook's error envelope before returning to your client. Record the test time, client outcome, and presence or absence of user records without sharing addresses or secrets. Keep mailbox ownership verification and other signup abuse controls in place.
Rotate the signing secret
Keep the installation ID and use the same management helper:
const rotated = await manageInstallation(
`/api/integrations/supabase/${installation.id}/rotate`,
{},
);
- Save the returned new
secret. IronDome schedules a registration accepting both generations. - Wait for that registration's publication status to become
published. - Replace the secret in Supabase with the new full value.
- Perform a controlled signup and confirm it succeeds. Allow time for the signed delivery to appear in accounting.
- Retire the previous generation:
await manageInstallation(
`/api/integrations/supabase/${installation.id}/retire`,
{},
);
Retirement requires published rotation state and an observed delivery signed by the new secret. A 409 new_secret_delivery_not_observed means proof has not arrived; confirm the Supabase secret and delivery before trying again. Wait for publication of the new-only registration after retirement.
Do not promise immediate propagation or zero-downtime rotation. Keep both secrets protected until retirement is confirmed.
Disconnect
- Disable or remove the Before User Created hook in Supabase first. Decide whether signup should continue without this protection or be paused.
- Disable the IronDome installation:
await manageInstallation(
`/api/integrations/supabase/${installation.id}/disable`,
{},
);
- Inspect the installation until disable publication completes. Retain its ID and status for troubleshooting, then remove unused local secret copies.
Disabling only in IronDome while leaving the Supabase hook active can block signup. A pending disable is not proof of global revocation, and publication has no guaranteed global cutoff time. For urgent containment, disable signup or disconnect the hook directly in Supabase.
Troubleshoot setup
Supabase requires HTTP Auth hooks to finish within five seconds. A timeout blocks signup; it does not mean the email was allowed. Check service status and the installation's publication state before retrying. This provider limit is not a promised IronDome response time. See Supabase Auth Hooks.
| Problem | Check |
|---|---|
invalid_project |
Use the project reference, not its URL, display name, or API key. |
integration_unavailable |
Integration access or configuration is unavailable; contact support. |
integration_capacity_or_account_unavailable |
Account access or installation capacity needs attention. |
rotation_already_pending |
Finish the current rotation before starting another. |
rotation_publication_pending |
Wait for the two-secret registration to publish. |
new_secret_delivery_not_observed |
Switch Supabase to the new secret and verify a signed signup delivery. |
| Signup fails after disconnect | Remove the configured Supabase hook; disabling IronDome alone is insufficient. |
A non-disposable result still does not establish mailbox ownership or deliverability. Keep your normal verification and signup-abuse controls.