# 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.

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](https://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.

```js
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:

```text
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:

```js
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

1. In the Supabase Dashboard, open **Authentication → Hooks** and configure **Before User Created** as an HTTP hook.
2. Paste the installation's returned hook URL.
3. Set the signing secret to the complete returned `secret` value, including `v1,whsec_`.
4. 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](https://supabase.com/docs/guides/auth/auth-hooks/before-user-created-hook) 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](/docs/plans).

## 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](https://supabase.com/docs/reference/javascript/auth-signup), not the IronDome management console:

```js
// 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`:

```json
{
  "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](/docs/guides/signup#ownership-and-abuse-controls) in place.

## Rotate the signing secret

Keep the installation ID and use the same management helper:

```js
const rotated = await manageInstallation(
  `/api/integrations/supabase/${installation.id}/rotate`,
  {},
);
```

1. Save the returned new `secret`. IronDome schedules a registration accepting both generations.
2. Wait for that registration's publication status to become `published`.
3. Replace the secret in Supabase with the new full value.
4. Perform a controlled signup and confirm it succeeds. Allow time for the signed delivery to appear in accounting.
5. Retire the previous generation:

```js
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

1. Disable or remove the Before User Created hook in Supabase first. Decide whether signup should continue without this protection or be paused.
2. Disable the IronDome installation:

```js
await manageInstallation(
  `/api/integrations/supabase/${installation.id}/disable`,
  {},
);
```

3. 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](https://status.irondome.dev) and the installation's publication state before retrying. This provider limit is not a promised IronDome response time. See [Supabase Auth Hooks](https://supabase.com/docs/guides/auth/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.

