{"_id":"better-auth-audit-logs","_rev":"5-1b8f68f447e6307639cbefea6906f852","name":"better-auth-audit-logs","dist-tags":{"latest":"0.4.0"},"versions":{"0.1.0":{"name":"better-auth-audit-logs","version":"0.1.0","_id":"better-auth-audit-logs@0.1.0","maintainers":[{"name":"ejirocode","email":"Ejiroasiuwhu10@gmail.com"}],"dist":{"shasum":"1f2a031f5cb548ced7456143169bc345f1c6df5e","tarball":"https://registry.npmjs.org/better-auth-audit-logs/-/better-auth-audit-logs-0.1.0.tgz","fileCount":46,"integrity":"sha512-dOTRDxPHkGJvtNcgxar20zXqaOvDt6p8t82W6/DJT3z34A1YbmiU7fymJg73hlTOxI5ywzzjFBF8zYZrMIYvnw==","signatures":[{"sig":"MEYCIQDXKqCs7Ixl0xfRpgsJRCHR680yBjdVdaKT64xXlmCWlAIhAL1ATtFWJJR5h0BHGVG82gxCFusIA4PIMcktzaaQsP4f","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":78625},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./client":{"types":"./dist/client.d.ts","import":"./dist/client.js","require":"./dist/client.cjs"}},"gitHead":"30870a007bf8ea86dcb4e60247ed8ebd9bc1deb5","scripts":{"test":"bun test","build":"bun build src/index.ts src/client.ts --outdir dist --target node --external better-auth --external zod && bun build src/index.ts src/client.ts --outdir dist --target node --format cjs --entry-naming '[dir]/[name].cjs' --external better-auth --external zod && tsc -p tsconfig.build.json","check":"tsc --noEmit && bun test","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ejirocode","email":"Ejiroasiuwhu10@gmail.com"},"_npmVersion":"11.9.0","description":"Audit log plugin for [Better Auth](https://better-auth.com). Captures auth lifecycle events, stores structured log entries with IP and user agent, and exposes query endpoints — with PII redaction, custom storage backends, and a manual insertion escape hat","directories":{},"_nodeVersion":"22.18.0","typesVersions":{"*":{"client":["./dist/client.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.3.6","@types/bun":"latest","better-auth":"^1.4.19"},"peerDependencies":{"zod":">=3.0.0","typescript":"^5","better-auth":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/better-auth-audit-logs_0.1.0_1772125754128_0.10356151880422981","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"better-auth-audit-logs","version":"0.1.1","keywords":["better-auth","better-auth-plugin","audit-log","audit-trail","auth","authentication","security","logging","compliance","pii-redaction","session-tracking"],"author":{"name":"Ejiro Asiuwhu","email":"ejiroasiuwhu10@gmail.com"},"license":"MIT","_id":"better-auth-audit-logs@0.1.1","maintainers":[{"name":"ejirocode","email":"Ejiroasiuwhu10@gmail.com"}],"homepage":"https://github.com/ejirocodes/better-auth-audit-logs#readme","bugs":{"url":"https://github.com/ejirocodes/better-auth-audit-logs/issues"},"dist":{"shasum":"720bd7fff696d52085d936ccd12c78fc3e105bff","tarball":"https://registry.npmjs.org/better-auth-audit-logs/-/better-auth-audit-logs-0.1.1.tgz","fileCount":47,"integrity":"sha512-v+9InopzKYGgKyFITEYUPa9SZHCAXbTZ1DqEKIqQSs/uK+NmQMGlHLAItrKDVE1BocurQmLMgYgUaYr7BinbuA==","signatures":[{"sig":"MEUCIQD4jUmW5twgMA83YhgLmpNRHlUUSckLczQLEkWYVFtMRgIgYHJyaRnG+UMDE/yXzQyQR7Ce7SopEpF3d/75RpkGzgo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":80904},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./client":{"types":"./dist/client.d.ts","import":"./dist/client.js","require":"./dist/client.cjs"}},"gitHead":"0f5cabaf569fd6de7758b85b8f1195a8ec74ab44","scripts":{"test":"bun test","build":"bun build src/index.ts src/client.ts --outdir dist --target node --external better-auth --external zod && bun build src/index.ts src/client.ts --outdir dist --target node --format cjs --entry-naming '[dir]/[name].cjs' --external better-auth --external zod && tsc -p tsconfig.build.json","check":"tsc --noEmit && bun test","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ejirocode","email":"Ejiroasiuwhu10@gmail.com"},"repository":{"url":"git+https://github.com/ejirocodes/better-auth-audit-logs.git","type":"git"},"_npmVersion":"11.9.0","description":"Audit log plugin for Better Auth. Captures auth lifecycle events, stores structured log entries, and exposes query endpoints with PII redaction and custom storage backends.","directories":{},"_nodeVersion":"22.18.0","typesVersions":{"*":{"client":["./dist/client.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.3.6","@types/bun":"latest","better-auth":"^1.4.19"},"peerDependencies":{"zod":">=3.0.0","typescript":"^5","better-auth":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/better-auth-audit-logs_0.1.1_1772126522814_0.6543437994695804","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"better-auth-audit-logs","version":"0.2.0","keywords":["better-auth","better-auth-plugin","audit-log","audit-trail","auth","authentication","security","logging","compliance","pii-redaction","session-tracking"],"author":{"name":"Ejiro Asiuwhu","email":"ejiroasiuwhu10@gmail.com"},"license":"MIT","_id":"better-auth-audit-logs@0.2.0","maintainers":[{"name":"ejirocode","email":"Ejiroasiuwhu10@gmail.com"}],"homepage":"https://github.com/ejirocodes/better-auth-audit-logs#readme","bugs":{"url":"https://github.com/ejirocodes/better-auth-audit-logs/issues"},"dist":{"shasum":"5b1e78b85e5284886bac507b9c27ef7012fe58dd","tarball":"https://registry.npmjs.org/better-auth-audit-logs/-/better-auth-audit-logs-0.2.0.tgz","fileCount":47,"integrity":"sha512-CtLDaL73OieFLOfSOUpYFq2YtXDQgoRSuoYdezQKQjkBEXlE9bl6eaKESeUOIuSwUUoWowWwAKy+oHkNDNd6ZQ==","signatures":[{"sig":"MEYCIQChaw5+UvY+8TwCjvBnW5VUOdmpH6Qigf5kmYy+Qd1dwwIhAOh6uvih76Of5RWiTgdvAg/vy2+x+tvVhO3ublQE8kX9","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":76120},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./client":{"types":"./dist/client.d.ts","import":"./dist/client.js","require":"./dist/client.cjs"}},"gitHead":"476ce20356640da11ae29a6539152124816bff57","scripts":{"test":"bun test","build":"bun build src/index.ts src/client.ts --outdir dist --target node --external better-auth --external zod && bun build src/index.ts src/client.ts --outdir dist --target node --format cjs --entry-naming '[dir]/[name].cjs' --external better-auth --external zod && tsc -p tsconfig.build.json","check":"tsc --noEmit && bun test","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ejirocode","email":"Ejiroasiuwhu10@gmail.com"},"repository":{"url":"git+https://github.com/ejirocodes/better-auth-audit-logs.git","type":"git"},"_npmVersion":"10.9.4","description":"Audit log plugin for Better Auth. Captures auth lifecycle events, stores structured log entries, and exposes query endpoints with PII redaction and custom storage backends.","directories":{},"_nodeVersion":"22.22.1","typesVersions":{"*":{"client":["./dist/client.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.3.6","@types/bun":"latest","better-auth":"^1.5.5"},"peerDependencies":{"zod":">=3.0.0","typescript":"^5","better-auth":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/better-auth-audit-logs_0.2.0_1773993276751_0.5430367165792724","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"better-auth-audit-logs","version":"0.3.0","keywords":["better-auth","better-auth-plugin","audit-log","audit-trail","auth","authentication","security","logging","compliance","pii-redaction","session-tracking"],"author":{"name":"Ejiro Asiuwhu","email":"ejiroasiuwhu10@gmail.com"},"license":"MIT","_id":"better-auth-audit-logs@0.3.0","maintainers":[{"name":"ejirocode","email":"Ejiroasiuwhu10@gmail.com"}],"homepage":"https://github.com/ejirocodes/better-auth-audit-logs#readme","bugs":{"url":"https://github.com/ejirocodes/better-auth-audit-logs/issues"},"dist":{"shasum":"70821a68d1a93040e8a60f90be416b05647c2af0","tarball":"https://registry.npmjs.org/better-auth-audit-logs/-/better-auth-audit-logs-0.3.0.tgz","fileCount":55,"integrity":"sha512-1u5Hc4pXW71OVW0ULX9mnRI7yejzc+qXvXB48PUWNxQrXmfOfZFmmhD6Mn49cXgbRbtUgunrAcPl76zaOI18pw==","signatures":[{"sig":"MEUCIAdLoqFYF/AVqVFM/bWaeHkb4DbWVg67HPglZQd25ok4AiEAvPMlQDavZVtWQgSuSQN0vA3Ie8vH+OIc5/OFEhLleGw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":98067},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./client":{"types":"./dist/client.d.ts","import":"./dist/client.js","require":"./dist/client.cjs"}},"gitHead":"1aea0c86302b4e6e8048af86d58fce865671ffb2","scripts":{"test":"bun test","build":"bun build src/index.ts src/client.ts --outdir dist --target node --external better-auth --external zod && bun build src/index.ts src/client.ts --outdir dist --target node --format cjs --entry-naming '[dir]/[name].cjs' --external better-auth --external zod && tsc -p tsconfig.build.json","check":"tsc --noEmit && bun test","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ejirocode","email":"Ejiroasiuwhu10@gmail.com"},"repository":{"url":"git+https://github.com/ejirocodes/better-auth-audit-logs.git","type":"git"},"_npmVersion":"10.9.4","description":"Audit log plugin for Better Auth. Captures auth lifecycle events, stores structured log entries, and exposes query endpoints with PII redaction and custom storage backends.","directories":{},"_nodeVersion":"22.22.1","typesVersions":{"*":{"client":["./dist/client.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.3.6","@types/bun":"latest","better-auth":"^1.5.5"},"peerDependencies":{"zod":">=3.0.0","typescript":"^5","better-auth":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/better-auth-audit-logs_0.3.0_1774001126281_0.9020943278646989","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"better-auth-audit-logs","version":"0.4.0","description":"Audit log plugin for Better Auth. Captures auth lifecycle events, stores structured log entries, and exposes query endpoints with PII redaction and custom storage backends.","type":"module","license":"MIT","author":{"name":"Ejiro Asiuwhu","email":"ejiroasiuwhu10@gmail.com"},"repository":{"type":"git","url":"git+https://github.com/ejirocodes/better-auth-audit-logs.git"},"homepage":"https://github.com/ejirocodes/better-auth-audit-logs#readme","bugs":{"url":"https://github.com/ejirocodes/better-auth-audit-logs/issues"},"keywords":["better-auth","better-auth-plugin","audit-log","audit-trail","auth","authentication","security","logging","compliance","pii-redaction","session-tracking"],"main":"./dist/index.cjs","types":"./dist/index.d.ts","exports":{".":{"require":"./dist/index.cjs","import":"./dist/index.js","types":"./dist/index.d.ts"},"./client":{"require":"./dist/client.cjs","import":"./dist/client.js","types":"./dist/client.d.ts"}},"typesVersions":{"*":{"client":["./dist/client.d.ts"]}},"scripts":{"build":"bun build src/index.ts src/client.ts --outdir dist --target node --external better-auth --external zod && bun build src/index.ts src/client.ts --outdir dist --target node --format cjs --entry-naming '[dir]/[name].cjs' --external better-auth --external zod && tsc -p tsconfig.build.json","test":"bun test","typecheck":"tsc --noEmit","check":"tsc --noEmit && bun test","prepublishOnly":"bun run check && bun run build"},"peerDependencies":{"better-auth":">=1.4.18 <2","zod":">=4.3.6 <5","typescript":"^5"},"devDependencies":{"@types/bun":"latest","better-auth":"^1.5.5","zod":"^4.3.6"},"gitHead":"dbcabf2a0cdf8109c01ac5e9eedb37179c107e11","_id":"better-auth-audit-logs@0.4.0","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-PvmKnB1qkqZFeJN4O0CeOzju2V03+Ttdny9/w6t/5WDexJNNdYFLmqBJuCa0+ADHmnZBqPboCgJbo5zzm5gPpg==","shasum":"90102db7326fd438d70a713879739f42a0b8f832","tarball":"https://registry.npmjs.org/better-auth-audit-logs/-/better-auth-audit-logs-0.4.0.tgz","fileCount":69,"unpackedSize":144611,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/better-auth-audit-logs@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQC5lEq0ySn4lRObJ0psnyITxTMgW5YvFOul5I6zLNah8QIhAPnIelGMK+lnIeHNKhC91/6eMjLhoKZutt33hjg3Jz6a"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:bf4112ae-0003-403a-80e0-79e76a312892"}},"directories":{},"maintainers":[{"name":"ejirocode","email":"Ejiroasiuwhu10@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/better-auth-audit-logs_0.4.0_1788106080345_0.6442860897709868"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-26T17:09:14.054Z","modified":"2026-08-30T16:08:00.837Z","0.1.0":"2026-02-26T17:09:14.292Z","0.1.1":"2026-02-26T17:22:02.999Z","0.2.0":"2026-03-20T07:54:36.897Z","0.3.0":"2026-03-20T10:05:26.455Z","0.4.0":"2026-08-30T16:08:00.489Z"},"bugs":{"url":"https://github.com/ejirocodes/better-auth-audit-logs/issues"},"author":{"name":"Ejiro Asiuwhu","email":"ejiroasiuwhu10@gmail.com"},"license":"MIT","homepage":"https://github.com/ejirocodes/better-auth-audit-logs#readme","keywords":["better-auth","better-auth-plugin","audit-log","audit-trail","auth","authentication","security","logging","compliance","pii-redaction","session-tracking"],"repository":{"type":"git","url":"git+https://github.com/ejirocodes/better-auth-audit-logs.git"},"description":"Audit log plugin for Better Auth. Captures auth lifecycle events, stores structured log entries, and exposes query endpoints with PII redaction and custom storage backends.","maintainers":[{"name":"ejirocode","email":"Ejiroasiuwhu10@gmail.com"}],"readme":"# better-auth-audit-logs\n\n[![npm version](https://img.shields.io/npm/v/better-auth-audit-logs)](https://www.npmjs.com/package/better-auth-audit-logs)\n[![npm downloads](https://img.shields.io/npm/dm/better-auth-audit-logs)](https://www.npmjs.com/package/better-auth-audit-logs)\n[![license](https://img.shields.io/npm/l/better-auth-audit-logs)](https://github.com/ejirocodes/better-auth-audit-logs/blob/main/LICENSE)\n\nAudit log plugin for [Better Auth](https://better-auth.com). Automatically captures auth events with IP, user agent, and severity — zero config required.\n\n**Requires** `better-auth >= 1.4.18` (1.4, 1.5, 1.6 and 1.7 are supported) and `typescript >= 5`.\n\n## Quick start\n\n```bash\nnpm install better-auth-audit-logs\n```\n\n```ts\nimport { betterAuth } from \"better-auth\";\nimport { auditLog } from \"better-auth-audit-logs\";\n\nexport const auth = betterAuth({\n  plugins: [auditLog()],\n});\n```\n\nThen generate and run the migration:\n\n```bash\nnpx @better-auth/cli generate\n```\n\nThat's it. All auth events are now logged automatically.\n\n## Schema\n\nThe plugin adds an `auditLog` table. If you prefer to manage your schema manually, copy the relevant definition:\n\n<details>\n<summary>Prisma</summary>\n\n```prisma\nmodel AuditLog {\n  id        String   @id @default(cuid())\n  userId    String?\n  action    String\n  status    String\n  severity  String\n  ipAddress String?\n  userAgent String?\n  metadata  String?\n  createdAt DateTime @default(now())\n\n  // only needed with tamperDetection enabled\n  hash         String?\n  previousHash String?\n\n  user User? @relation(fields: [userId], references: [id], onDelete: SetNull)\n\n  @@index([userId])\n  @@index([action])\n  @@index([createdAt])\n  @@map(\"auditLog\")\n}\n```\n\n</details>\n\n<details>\n<summary>Drizzle</summary>\n\n```ts\nimport { sqliteTable, text, integer } from \"drizzle-orm/sqlite-core\";\nimport { user } from \"./auth-schema\"; // your existing user table\n\nexport const auditLog = sqliteTable(\"auditLog\", {\n  id: text(\"id\").primaryKey(),\n  userId: text(\"userId\").references(() => user.id, { onDelete: \"set null\" }),\n  action: text(\"action\").notNull(),\n  status: text(\"status\").notNull(),\n  severity: text(\"severity\").notNull(),\n  ipAddress: text(\"ipAddress\"),\n  userAgent: text(\"userAgent\"),\n  metadata: text(\"metadata\"),\n  createdAt: integer(\"createdAt\", { mode: \"timestamp\" }).notNull(),\n  // only needed with tamperDetection enabled\n  hash: text(\"hash\"),\n  previousHash: text(\"previousHash\"),\n});\n```\n\n</details>\n\n<details>\n<summary>MongoDB</summary>\n\n```ts\n// Collection: auditLog\n{\n  _id: ObjectId,\n  userId: String | null,       // references user collection\n  action: String,              // e.g. \"sign-in:email\"\n  status: String,              // \"success\" | \"failed\"\n  severity: String,            // \"low\" | \"medium\" | \"high\" | \"critical\"\n  ipAddress: String | null,\n  userAgent: String | null,\n  metadata: String | null,     // JSON string\n  createdAt: Date,\n  hash: String | null,         // only with tamperDetection enabled\n  previousHash: String | null\n}\n\n// Recommended indexes\ndb.auditLog.createIndex({ userId: 1 })\ndb.auditLog.createIndex({ action: 1 })\ndb.auditLog.createIndex({ createdAt: 1 })\n```\n\n</details>\n\n## Client plugin\n\n```ts\nimport { createAuthClient } from \"better-auth/client\";\nimport { auditLogClient } from \"better-auth-audit-logs/client\";\n\nexport const authClient = createAuthClient({\n  plugins: [auditLogClient()],\n});\n```\n\n```ts\n// List recent failed sign-ins\nconst { data } = await authClient.auditLog.listAuditLogs({\n  query: { status: \"failed\", limit: 20 },\n});\n\n// Single entry by ID\nconst { data: entry } = await authClient.auditLog.getAuditLog({\n  params: { id: \"log-entry-id\" },\n});\n\n// Manually log custom events (admin actions, data exports, etc.)\nawait authClient.auditLog.insertAuditLog({\n  action: \"admin:user-export\",\n  status: \"success\",\n  severity: \"high\",\n  metadata: { exportedCount: 500 },\n});\n```\n\n## What gets logged\n\nAll auth `POST` endpoints are captured by default:\n\n| Event | Path | Hook |\n|---|---|---|\n| Sign in | `/sign-in/email`, `/sign-in/social` | after |\n| Sign up | `/sign-up/email` | after |\n| Change/reset password | `/change-password`, `/reset-password` | after |\n| Change email | `/change-email` | after |\n| Two-factor | `/two-factor/*` | after |\n| OAuth callback | `/oauth/callback` | after |\n| Sign out | `/sign-out` | **before** |\n| Delete account | `/delete-user` | **before** |\n| Revoke session | `/revoke-session`, `/revoke-sessions`, `/revoke-other-sessions` | **before** |\n\n\"Before\" hooks fire for destructive events where the session would be lost after execution.\n\nSeverity is inferred automatically (`critical` for ban/impersonate, `high` for delete/revoke/failed sign-in, `medium` for sign-in/out, `low` for everything else) and can be overridden per-path.\n\n## Configuration\n\nAll options are optional:\n\n```ts\nauditLog({\n  enabled: true,             // disable without removing the plugin\n  nonBlocking: false,        // fire-and-forget — never blocks auth responses\n\n  // restrict to specific paths (empty = capture all)\n  paths: [\n    \"/sign-in/email\",\n    { path: \"/delete-user\", config: { severity: \"high\", capture: { requestBody: true } } },\n  ],\n\n  capture: {\n    ipAddress: true,         // capture client IP\n    userAgent: true,         // capture User-Agent header\n    requestBody: false,      // include request body in metadata\n  },\n\n  piiRedaction: {\n    enabled: false,          // redact sensitive fields when requestBody is captured\n    strategy: \"mask\",        // \"mask\" (***) | \"hash\" (SHA-256) | \"remove\" (delete key)\n    fields: [\"password\"],    // defaults: password, token, secret, apiKey, otp, etc.\n  },\n\n  retention: {\n    enabled: false,          // delete old entries as auth traffic comes in\n    days: 90,                // delete entries older than N days\n    intervalMs: 86_400_000,  // minimum gap between cleanups (default 24h)\n  },\n\n  tamperDetection: {\n    enabled: false,          // sign each entry into a hash chain\n    scope: \"user\",           // \"user\" (one chain per user) | \"global\" (one chain for everything)\n    secret: undefined,       // defaults to a key derived from Better Auth's secret\n  },\n\n  // intercept before write — return null to suppress. Receives the endpoint\n  // ctx as a second argument, so you can resolve the session or read the request.\n  beforeLog: async (entry, ctx) => {\n    if (entry.userId === \"service-account\") return null;\n    return entry;\n  },\n\n  // called after each successful write\n  afterLog: async (entry) => {\n    await analytics.track(\"auth.event\", entry);\n  },\n\n  storage: undefined,        // custom storage backend (see below)\n})\n```\n\nTo override the DB model name, pass `schema: { auditLog: { modelName: \"your_table_name\" } }`.\n\n## Retention\n\nWith `retention.enabled`, the plugin deletes entries older than `retention.days` while it writes new ones. The cleanup is throttled to one run per `intervalMs` per process (24 hours by default), runs in the background, and never blocks or fails an auth request — a failed cleanup is logged and retried on the next sweep.\n\nBecause the sweep rides on auth traffic, an instance that receives no auth requests never cleans up. Call `deleteExpiredAuditLogs` from your own scheduler when you need cleanup on a fixed cadence, or when you would rather keep it off the request path entirely:\n\n```ts\nimport { deleteExpiredAuditLogs } from \"better-auth-audit-logs\";\n\nconst deleted = await deleteExpiredAuditLogs(await auth.$context, { days: 90 });\n```\n\nPass `modelName` if you renamed the table, and `storage` if you use a custom backend:\n\n```ts\nawait deleteExpiredAuditLogs(await auth.$context, {\n  days: 90,\n  modelName: \"audit_trail\",\n  storage: clickhouse,\n});\n```\n\nA custom storage backend must implement `deleteOlderThan(date)` before retention can be enabled — the plugin throws at startup otherwise, rather than silently keeping logs forever.\n\nCleanup issues a single unbounded `DELETE` over everything past the cutoff. If you are enabling retention on a table that has been accumulating for a long time, run `deleteExpiredAuditLogs` once from a script before turning on the automatic sweep, so the first large delete does not land alongside a live request.\n\n## Tamper evidence\n\nA compliance audit (SOC 2, HIPAA) asks you to prove an audit trail is complete and unmodified. With `tamperDetection.enabled`, every entry is signed with HMAC-SHA256 over its own fields plus the signature of the entry written before it. Editing a row invalidates its own signature; deleting one breaks the link its successor holds.\n\n```ts\nauditLog({\n  tamperDetection: {\n    enabled: true,\n    scope: \"user\",\n    secret: process.env.AUDIT_CHAIN_SECRET,\n  },\n})\n```\n\nEnabling it adds two nullable columns, `hash` and `previousHash` — re-run `npx @better-auth/cli generate`. Entries written before you turned it on keep a null `hash`, and verification counts them as `unchained` rather than failing on them.\n\n### Verifying\n\n```ts\nimport { verifyAuditLogChain } from \"better-auth-audit-logs\";\n\nconst report = await verifyAuditLogChain(await auth.$context);\n\nif (!report.ok) {\n  await alerting.page(\"audit log integrity check failed\", report.findings);\n}\n```\n\n```ts\n{\n  ok: true,            // false only for `modified` or `orphaned` findings\n  chains: 412,         // chains covered — one per user under `scope: \"user\"`\n  entriesChecked: 9_120,\n  unchained: 300,      // entries written before tamper detection was enabled\n  findings: [],\n}\n```\n\nEach finding carries the entry it was raised on and the predecessor it expected:\n\n| `type` | Meaning |\n|---|---|\n| `modified` | The entry's contents no longer match its signature. Tampering. |\n| `orphaned` | An entry has been removed from the middle of a chain. Tampering. |\n| `truncated` | The oldest entry in range links further back — what retention or a `from` bound leaves behind. Not a failure. |\n| `forked` | Two entries claim the same predecessor. Concurrent writers, not tampering. Not a failure. |\n\nPass `from`/`to` to bound a run, `userId` to check one user, `storage` and `modelName` to match your `auditLog()` config, and `scope` to match the scope you write with. Verification loads the range into memory, so bound it on large tables.\n\n### What it protects against\n\nThe signature is keyed, not a bare SHA-256 hash, and the key lives in your app's environment rather than in the database. That is the whole point: an attacker who reaches only the database — a stolen dump, SQL injection, a rogue DBA — cannot recompute a valid signature, so they cannot rewrite history undetected. A bare hash chain would let them re-hash the entries they touched and leave the chain intact.\n\nChain structure proves no entry was altered or removed from the middle of a chain. It cannot prove the trail was not cut short at the head — deleting the newest entries leaves a shorter chain that still verifies. Nor does it defend against anyone holding the app's secret or running code in the app process. Closing either gap requires the trail to leave the database: forward entries to append-only storage from `afterLog`, or publish the chain head on a schedule somewhere you do not control.\n\nThe key is derived from Better Auth's `secret` with a domain separator, so it cannot be used to forge anything else that secret signs. Set `tamperDetection.secret` to key the chain independently — rotating it invalidates every existing signature, so treat it as permanent.\n\n### Concurrency\n\nEach write reads its chain's head before signing. Writes to the same chain are serialized inside a process, so a single instance never forks. Across instances two writers can read the same head and produce a `forked` pair — real, but not evidence of tampering, and it never hides a `modified` finding. `scope: \"user\"` is the default because a single user's requests rarely land on two instances at once; `scope: \"global\"` gives one continuous chain and forks under any concurrency.\n\n### Retention and user deletion\n\nBoth rewrite history by design, and verification sees them:\n\n- **Retention** trims the oldest entries, so the surviving chain start reports as `truncated`. A deletion of only the very oldest entries is indistinguishable from that, and reports the same way.\n- **Deleting a user** sets `userId` to null on their entries (`ON DELETE SET NULL`), which changes signed content and reports as `modified`. If you need erasure alongside tamper evidence, pseudonymize `userId` in `beforeLog` at write time so there is nothing to null out later.\n\n## Adding additional metadata to log entries\n\n`beforeLog` is the injection point for extra per-entry data. Because it receives the\nendpoint `ctx`, you can resolve the session and stash a value such as the active\norganization into `metadata` — which is stored as JSON and returned intact:\n\n```ts\nimport { getSessionFromCtx } from \"better-auth/api\";\n\nauditLog({\n  beforeLog: async (entry, ctx) => {\n    const session = await getSessionFromCtx(ctx);\n    return {\n      ...entry,\n      metadata: {\n        ...entry.metadata,\n        activeOrganizationId: session?.session?.activeOrganizationId ?? null,\n      },\n    };\n  },\n});\n```\n\n## Custom storage\n\nRoute writes to any external backend instead of Better Auth's database:\n\n```ts\nimport { auditLog, type AuditLogStorage } from \"better-auth-audit-logs\";\n\nconst clickhouse: AuditLogStorage = {\n  async write(entry) {\n    await fetch(\"https://ch.example.com/insert\", {\n      method: \"POST\",\n      body: JSON.stringify(entry),\n    });\n  },\n  // Optional — enables the query endpoints to work with your backend\n  async read(options) { /* ... */ },\n  async readById(id) { /* ... */ },\n  // Required by tamperDetection — entries newest first\n  async readChain(options) { /* ... */ },\n};\n\nauditLog({ storage: clickhouse })\n```\n\nA `MemoryStorage` adapter is included for testing:\n\n```ts\nimport { auditLog, MemoryStorage } from \"better-auth-audit-logs\";\n\nconst storage = new MemoryStorage();\nconst auth = betterAuth({ plugins: [auditLog({ storage })] });\n\n// assert in tests\nexpect(storage.entries).toHaveLength(1);\nexpect(storage.entries[0].action).toBe(\"sign-in:email\");\n```\n\n## API endpoints\n\nThree endpoints are registered under `/audit-log/`, all requiring an active session. Rate limited to 60 req/min.\n\n| Endpoint | Method | Description |\n|---|---|---|\n| `/audit-log/list` | `GET` | Paginated entries |\n| `/audit-log/:id` | `GET` | Single entry by ID |\n| `/audit-log/insert` | `POST` | Manually insert a custom event |\n\n**Query parameters** for `GET /audit-log/list`:\n\n| Parameter | Type | Default |\n|---|---|---|\n| `userId` | `string` | session user |\n| `action` | `string` | — |\n| `status` | `\"success\" \\| \"failed\"` | — |\n| `from` | ISO date string | — |\n| `to` | ISO date string | — |\n| `limit` | `number` | `50` (max 500) |\n| `offset` | `number` | `0` |\n\n## Design decisions\n\n- **Entries survive user deletion** — `userId` uses `ON DELETE SET NULL`. Deleting a user does not erase their audit trail.\n- **`userAgent` is not returned in API responses** — stored for forensics but excluded from client queries by default.\n- **Failed sign-ins have `userId: null`** — the user isn't authenticated yet, so there's no session to pull from. Under `scope: \"user\"` they share one chain, so a credential-stuffing burst is still covered by tamper evidence.\n\n## Recommended production config\n\n```ts\nauditLog({\n  nonBlocking: true,\n  piiRedaction: { enabled: true, strategy: \"hash\" },\n  retention: { enabled: true, days: 90 },\n  tamperDetection: { enabled: true },\n  afterLog: async (entry) => {\n    if (entry.severity === \"critical\" || entry.severity === \"high\") {\n      await alerting.emit(entry);\n    }\n  },\n})\n```\n\n## Acknowledgments\n\nThis plugin was inspired by the audit log design shared by [@Re4GD](https://github.com/Re4GD) in [better-auth/better-auth#1184](https://github.com/better-auth/better-auth/issues/1184). Additional inspiration from [@issamwahbi](https://github.com/issamwahbi) ([#3592](https://github.com/better-auth/better-auth/discussions/3592)) and [@ItsProless](https://github.com/ItsProless) ([#7952](https://github.com/better-auth/better-auth/discussions/7952)).\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"README.md"}