AbuseGraph + Better Auth
Hook Better Auth user create and session create with createCheckClient — forward browser SDK signals for a full score.
Quick path
- Install
@abusegraph/sdkand@abusegraph/server; paste keys from the console. - Collect browser signals on signup/login forms →
toCheckBody→ send to your server. - Call
createCheckClient.checkindatabaseHooksbefore leaving a live session.
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
Use @abusegraph/server createCheckClient. Events: signup | login | password_reset | email_change. Optional sessionId.
import { createCheckClient } from "@abusegraph/server"
const abusegraph = createCheckClient({
secretKey:
process.env.NODE_ENV === "production"
? process.env.ABUSEGRAPH_SECRET_KEY! // sk_live_…
: process.env.ABUSEGRAPH_SECRET_KEY_TEST ??
process.env.ABUSEGRAPH_SECRET_KEY!, // sk_test_…
site: process.env.ABUSEGRAPH_SITE!,
})
export async function scoreAuth(input: {
email?: string
userId?: string
sessionId?: string
event?: "signup" | "login" | "password_reset" | "email_change"
[key: string]: unknown
}) {
return abusegraph.check({ ...input, event: input.event ?? "signup" })
}Browser signals (recommended)
init + collect + evaluate + toCheckBody — NEVER bare sdk.check() alone as the authoritative decision.
import { init } from "@abusegraph/sdk"
const sdk = init({
publicKey: process.env.NEXT_PUBLIC_ABUSEGRAPH_PUBLISHABLE_KEY!, // pk_test_…
})
export async function collectForAuth(
email: string,
event: "signup" | "login" | "password_reset" | "email_change" = "signup",
) {
const collected = await sdk.collect()
const edge = await sdk.evaluate(collected, { email })
return sdk.toCheckBody(collected, {
email,
event,
verdictToken: edge.verdictToken.token,
})
}Better Auth hooks
Pass browserFields from the client (hidden field / cookie) into the hook:
// auth.ts
databaseHooks: {
user: {
create: {
after: async (user, ctx) => {
const browserFields = ctx.body?.abusegraph ?? {}
const result = await scoreAuth({
email: user.email,
userId: user.id,
event: "signup",
...browserFields,
})
if (result.verdict === "block") {
// delete / ban — do not leave a live session
}
},
},
},
session: {
create: {
after: async (session, ctx) => {
const result = await scoreAuth({
email: ctx.user.email,
userId: ctx.user.id,
event: "login",
})
if (result.verdict === "block") {
// revoke session
}
},
},
},
}Lifecycle events
- login —
session.createhook withevent: "login". - password_reset — call
scoreAuthin your reset route withevent: "password_reset". - email_change — call after 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.