Supabase Auth

Supabase Auth can call an HTTP endpoint before a user is created (the before-user-created hook, available on Free and Pro). Returning an error rejects the signup. See the Supabase docs for the full payload.

1. An Edge Function that calls BurnGuard

// supabase/functions/burnguard-hook/index.ts
import { Webhook } from "https://esm.sh/standardwebhooks@1.0.0";

const hookSecret = Deno.env.get("BEFORE_USER_CREATED_HOOK_SECRET")!.replace("v1,whsec_", "");

Deno.serve(async (req) => {
  const payload = await req.text();
  const headers = Object.fromEntries(req.headers);
  let event: any;
  try {
    event = new Webhook(hookSecret).verify(payload, headers);
  } catch {
    return new Response("invalid signature", { status: 401 });
  }

  const email: string | undefined = event.user?.email;
  if (!email) return Response.json({}); // phone / OAuth without email: allow

  let result: { decision: string; recommended_credit_fraction?: number } = { decision: "allow" };
  try {
    const res = await fetch(`${Deno.env.get("BURNGUARD_URL")}/v1/check`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${Deno.env.get("BURNGUARD_SECRET_KEY")}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ event: "signup", email, ip: event.metadata?.ip_address }),
      signal: AbortSignal.timeout(1500),
    });
    if (res.ok) result = await res.json();
  } catch {
    // fail open
  }

  const decision = result.decision;
  if (decision === "block") {
    return Response.json({ error: { http_code: 403, message: "We couldn't create this account." } });
  }
  return Response.json({});
});

2. Enable the hook

  1. Deploy the function: supabase functions deploy burnguard-hook --no-verify-jwt.
  2. In the dashboard, open Authentication → Hooks, add a Before User Created hook of type HTTPS pointing at the function URL, and copy the generated secret into BEFORE_USER_CREATED_HOOK_SECRET.
  3. Set BURNGUARD_URL and BURNGUARD_SECRET_KEY as function secrets.

Credit gating with Supabase

The hook can only allow or reject. For limit and review, store the decision and apply it when you grant credits:

// inside the hook, after the check
await fetch(`${Deno.env.get("SUPABASE_URL")}/rest/v1/signup_checks`, {
  method: "POST",
  headers: {
    apikey: Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
    Authorization: `Bearer ${Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ email, decision, credit_fraction: result.recommended_credit_fraction ?? 1 }),
});

Then, wherever you create the user's credit balance, read signup_checks by email.

The browser device token isn't available inside the hook. To get device reuse detection, call /v1/check from your own signup endpoint instead and then call supabase.auth.signUp only when the decision allows it. Read more: Block disposable emails and fake signups in Supabase Auth.