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
- Deploy the function:
supabase functions deploy burnguard-hook --no-verify-jwt. - 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. - Set
BURNGUARD_URLandBURNGUARD_SECRET_KEYas 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.