Docs

Supabase

Supabase hook

Install the Before User Created hook, then check on your dashboard that it works.

Install in Supabase

Supabase calls a hook just before it creates a user. SignupBouncer answers that hook. You don't change any code in your app.

  1. Sign up. Your dashboard shows your hook URL and hook secret.
  2. In Supabase, open Authentication, Hooks.
  3. Add a hook. Pick "Before User Created" and the HTTPS type.
  4. Paste the hook URL (it looks like https://signupbouncer.com/hook/supabase/app_...) and the hook secret (it starts with v1,whsec_). Save.
  5. Sign up to your app with a test address. The check appears in your dashboard within seconds.

If you manage Supabase with the CLI, put the hook URL and the hook secret in supabase/.env:

SIGNUPBOUNCER_HOOK_URL=https://signupbouncer.com/hook/supabase/app_...
SIGNUPBOUNCER_HOOK_SECRET=v1,whsec_...

Then add the hook block to supabase/config.toml. The supabase start command reads the hook URL and the hook secret from supabase/.env, and stops with a config error while either is missing. Keep supabase/.env out of git.

[auth.hook.before_user_created]
enabled = true
uri = "env(SIGNUPBOUNCER_HOOK_URL)"
secrets = "env(SIGNUPBOUNCER_HOOK_SECRET)"

What your users see

In enforce mode, we refuse a signup marked block with your block message. Supabase returns that message to your app as the signup error. In shadow mode every signup goes through.

While you keep our default block message, the words fit the reason for the refusal:

  • A burner domain: "Please sign up with a permanent email address."
  • A domain that looks like a typo of a big mail provider: "Did you mean ann@gmail.com? Please check your email address and try again."
  • Any other reason, such as a repeat inbox: "We couldn't create your account."

The message for other reasons leaves out the cause, so someone farming accounts learns nothing. It also leaves out a way to reach you. To add your support address, open the Rules tab and pick the app in the app menu. Rewrite the block message to fit every refusal, including a refused confirmation.

By default, we allow challenged signups. With "Treat as block", we refuse them. We still allow one whose score has any velocity points, unless a check you set to "Always challenge" made the challenge.

Supabase accepts only an allow or an error message, so the hook's answer has no separate typo suggestion. With your own block message, call the REST API from your own code and read suggestion to show "Did you mean ann@gmail.com?" as well.

If SignupBouncer is down

Supabase refuses the signup when a hook fails. So the hook URL goes through a small Cloudflare Worker. When our server hits an error, is overloaded or takes longer than 1.2 seconds, it answers allow itself.

When our server is down or does not answer within 1.5 seconds, the Worker answers allow. Either way the signup goes through unchecked, including signups from known burner domains.

If Cloudflare fails, or Supabase cannot reach the Worker, Supabase refuses the signup as it does for any hook that fails.

Which signups we check

Email and password, magic link, phone, and OAuth signups (Google, GitHub and others) pass through the hook. We don't check existing users when they sign in.

For OAuth and SSO signups that Supabase marks as email-verified, we check the IP and signup bursts only. For other OAuth signups we check the email too.

Check that the Supabase hook works

Sign up to your app with an address at mailinator.com. Within seconds, the signup shows on the Checks tab of your dashboard.

The label in the Result column says what the score marked the signup: Allow, Challenge or Block. When the hook did something else with the signup, a line under the label says so. In shadow mode, the mailinator.com signup shows Block with "Let through, Shadow" under it, and Supabase created the user all the same.

In the REST API, verdict holds the label and action holds what to do with the signup. A reason with a dashed border counts only on a higher plan, so it adds no points to your score.

If no check appears

  • The hook isn't on. In the Supabase project your app uses, open Authentication, Hooks, and look for the Before User Created hook with your hook URL.
  • The hook URL belongs to another of your apps. The hook URL ends in an app id, and we log the check under that app. Copy the hook URL again from the Integration tab of the app you're looking at.
  • The hook secret doesn't match. We let those signups through unchecked. Your dashboard shows a notice that hook calls had a signature that didn't match your secret, so copy the hook secret again from the Integration tab.
  • Your test didn't create a user. We check new users only, so signing in to an account that already exists adds no check.

Unchecked signups

On the Free plan, after 1,000 checks in a month, the Result column shows Not checked, with "Over quota" under it. Supabase creates those users without a check until the month ends.

On a monthly paid plan, signups go through unchecked once you reach your cap on extra checks. On a yearly plan, they go through unchecked once you've used the allowance.

In enforce mode, the Result "Let through, answer too late" means our answer came after the hook's time limit. Your Checks tab misses signups during our outages. The Free plan lists up to 50 verified social signups without an IP daily.

Turn the hook off

When you turn the hook off, Supabase creates users without asking us. Turn it off straight away if signups fail because Supabase can't reach the hook. Your SignupBouncer app keeps its settings and its check log.

  1. In Supabase, open Authentication, Hooks and turn off or remove the Before User Created hook.
  2. If you manage Supabase with the CLI, set enabled = false under [auth.hook.before_user_created] in supabase/config.toml. Then run supabase stop and supabase start.
  3. Sign up to your app with a test address. Supabase creates the user, and your Checks tab shows no new check.

To turn the hook back on, add it again with the hook URL and the hook secret from your Integration tab.

If you added the /auth/confirm route for the second look at confirmation, remove the route as well. Removing the route doesn't unban anyone the route banned. Follow the steps to remove the confirmation route to lift a ban.