AbuseGraphDocs
Guides

AbuseGraph + Better Auth

Hook Better Auth user create and session create with createCheckClient — forward browser SDK signals for a full score.

Quick path

  1. Install @abusegraph/sdk and @abusegraph/server; paste keys from the console.
  2. Collect browser signals on signup/login forms → toCheckBody → send to your server.
  3. Call createCheckClient.check in databaseHooks before leaving a live session.

Keys

KeyEnvUse
SecretABUSEGRAPH_SECRET_KEY (sk_test_… / sk_live_…)Server /api/v1/check
PublishableNEXT_PUBLIC_ABUSEGRAPH_PUBLISHABLE_KEY (pk_test_…)Browser SDK
SiteABUSEGRAPH_SITEDNS-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" })
}

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

  • loginsession.create hook with event: "login".
  • password_reset — call scoreAuth in your reset route with event: "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.