AbuseGraph + Better Auth
Check Better Auth signup and login with the HTTPS API and optional browser signals.
Quick path
- Copy a test key from the console; no package installation is required for the browser or HTTPS API path.
- Collect browser signals on signup/login forms →
toCheckBody→ send to your server. - Use
databaseHooksbeforehooks to gate account creation and session issuance.
Keys
| Key | Env | Use |
|---|---|---|
| Secret | ABUSEGRAPH_SECRET_KEY (sk_test_… / sk_live_…) | Server /api/v1/check |
| Publishable | NEXT_PUBLIC_ABUSEGRAPH_PUBLISHABLE_KEY (pk_test_…) | Browser SDK |
| Site | ABUSEGRAPH_SITE | DNS-verified domain |
Free includes 1,000 live API checks / month after DNS verify. Test keys never bill.
Env
ABUSEGRAPH_SITE=yourdomain.com
ABUSEGRAPH_CHECK_URL=https://abusegraph.com/api/v1/check
ABUSEGRAPH_SECRET_KEY=sk_test_…
ABUSEGRAPH_SECRET_KEY_TEST=sk_test_…
NEXT_PUBLIC_ABUSEGRAPH_PUBLISHABLE_KEY=pk_test_…Use sk_live_… / pk_live_… in production after DNS verify.
Server check
Call the HTTPS API from your server. Keep ABUSEGRAPH_SECRET_KEY in server-only environment configuration. Events: signup | login | password_reset | email_change; sessionId is optional.
const API = "https://abusegraph.com/api/v1/check";
export async function scoreAuth(input: Record<string, unknown>) {
const site = process.env.ABUSEGRAPH_SITE!;
const response = await fetch(API, {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.ABUSEGRAPH_SECRET_KEY!,
"x-abusegraph-site": site,
},
body: JSON.stringify({
...input,
event: typeof input.event === "string" ? input.event : "signup",
site,
}),
});
if (!response.ok) throw new Error(`AbuseGraph check failed: ${response.status}`);
return response.json();
}Run this before creating an account or issuing a session. Handle block and challenge according to your policy; 402 means the live monthly allowance is exhausted.
Browser signals (recommended)
Load the browser SDK from the AbuseGraph CDN before your signup/login code. The publishable key is safe to expose; send the resulting body to your own backend, which makes the authoritative check with its server-held secret key.
<script src="https://abusegraph.com/sdk/abusegraph.min.js"></script>
<script>
const sdk = AbuseGraph.init({ publicKey: "pk_test_…" });
async function collectForAuth(email, event = "signup") {
const collected = await sdk.collect();
const evaluated = await sdk.evaluate(collected, { email });
return sdk.toCheckBody(collected, {
email,
event,
verdictToken: evaluated.verdictToken?.token,
});
}
</script>POST the returned JSON to your own endpoint. Never treat the browser result as the authoritative decision or put an sk_ key in browser code.
Better Auth hooks
Use before hooks to gate account and session creation. The browser payload may contain risk signals, so place trusted identity and event fields after it. This example returns false on a block, which aborts the pending write:
// auth.ts
databaseHooks: {
user: {
create: {
before: async (user, ctx) => {
const browserFields = ctx.body?.abusegraph ?? {}
const result = await scoreAuth({
...browserFields,
email: user.email,
userId: user.id,
event: "signup",
})
if (result.verdict === "block") {
return false
}
},
},
},
session: {
create: {
before: async (session, ctx) => {
const result = await scoreAuth({
email: ctx.user.email,
userId: ctx.user.id,
event: "login",
})
if (result.verdict === "block") {
return false
}
},
},
},
}Lifecycle events
- login —
session.create.beforehook withevent: "login"(theafterhook is for monitoring only). - password_reset — call
scoreAuthin your reset route withevent: "password_reset". - email_change — call before applying the email update with
event: "email_change".
Platform
Lists, rules, and linked accounts apply on every check — configure in the console. Optional session: POST /api/v1/session. Native apps: iOS, Android, React Native.
Verify
Sign up once with sk_test_…, then open Activity in the console. Blocked signups should not retain a session.