Better Auth

Better Auth lets you run code before any auth endpoint with hooks.before. This recipe checks /sign-up/email requests. See the Better Auth hooks docs.

// auth.ts
import { betterAuth } from "better-auth";
import { createAuthMiddleware, APIError } from "better-auth/api";

async function burnguard(body: Record<string, unknown>) {
  try {
    const res = await fetch(`${process.env.BURNGUARD_URL}/v1/check`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.BURNGUARD_SECRET_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(1500),
    });
    return res.ok ? await res.json() : { decision: "allow" };
  } catch {
    return { decision: "allow" };
  }
}

export const auth = betterAuth({
  // ...your database and emailAndPassword config
  hooks: {
    before: createAuthMiddleware(async (ctx) => {
      if (ctx.path !== "/sign-up/email") return;
      const check = await burnguard({
        event: "signup",
        email: ctx.body?.email,
        ip: ctx.headers?.get("x-forwarded-for")?.split(",")[0]?.trim(),
        user_agent: ctx.headers?.get("user-agent") ?? undefined,
        device_token: ctx.body?.deviceToken, // send it from your client with the signup call
      });
      if (check.decision === "block") {
        throw new APIError("FORBIDDEN", { message: "We couldn't create this account." });
      }
      // For "limit" and "review", remember the decision (e.g. in your own table keyed by email)
      // and apply it when you grant free credits after the user is created.
    }),
  },
});

On the client, pass the token with the signup call:

const deviceToken = await (window as any).BurnGuard?.token().catch(() => undefined);
await authClient.signUp.email({ email, password, name, deviceToken } as any);

See Decisions and credit gating for how to handle limit and review.