@better-auth/infra
v0.4.3
Published
Dashboard and analytics plugin for Better Auth
Readme
Better Auth Infrastructure
Infra plugins for Better Auth:
dash()for dashboard/admin APIs, analytics tracking, and infra endpoints.dashClient()for dashboard client actions (including audit log queries).sentinel()for security checks and abuse protection.sentinelClient()for browser fingerprint headers + optional PoW auto-solving.sentinelNativeClient()(from@better-auth/infra/native) for React Native / Expo.
Installation
npm install @better-auth/infra
# or
pnpm add @better-auth/infra
# or
bun add @better-auth/infraServer Usage
import { betterAuth } from "better-auth";
import { dash, sentinel } from "@better-auth/infra";
export const auth = betterAuth({
// ...your Better Auth config
plugins: [
dash({
apiUrl: process.env.BETTER_AUTH_API_URL,
kvUrl: process.env.BETTER_AUTH_KV_URL,
apiKey: process.env.BETTER_AUTH_API_KEY,
}),
sentinel({
apiUrl: process.env.BETTER_AUTH_API_URL,
kvUrl: process.env.BETTER_AUTH_KV_URL,
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
credentialStuffing: {
enabled: true,
thresholds: { challenge: 3, block: 5 },
},
},
}),
],
});Client Usage
import { createAuthClient } from "better-auth/client";
import { dashClient, sentinelClient } from "@better-auth/infra/client";
export const authClient = createAuthClient({
plugins: [
dashClient(),
sentinelClient({
autoSolveChallenge: true,
}),
],
});
// Resolve user from session or pass explicit user/org context
const auditLogs = await authClient.dash.getAuditLogs({
session: await authClient.getSession().then((r) => r.data),
organizationId: "org_123",
limit: 20,
});Native client
Use this plugin for your React Native / Expo app. This plugin is designed to work along with the server side sentinel and dash plugins and will ensure your app is protected from abuse and bot attacks.
Install peers
pnpm add react-native @react-native-async-storage/async-storage
# Optional, for richer device metadata in the identify payload:
pnpm add expo-constants expo-device
# Optional, secure visitor-id RNG (Expo):
pnpm add expo-crypto
# Or plain React Native — first import in the app entry:
# import "react-native-get-random-values";@react-native-async-storage/async-storage is optional; if it is not installed, a session-only in-memory visitor id is used. For production, install it or pass storage (for example secure storage).
Example
import { createAuthClient } from "better-auth/client";
import { dashClient, sentinelNativeClient } from "@better-auth/infra/native";
export const authClient = createAuthClient({
baseURL: "https://your-api.example.com",
plugins: [
dashClient(),
sentinelNativeClient({
identifyUrl: process.env.EXPO_PUBLIC_BETTER_AUTH_KV_URL,
autoSolveChallenge: true,
}),
],
});Options (sentinelNativeClient)
identifyUrl?: string— identify endpoint base (defaults toBETTER_AUTH_KV_URLfrom env, thenhttps://kv.better-auth.com).identifyOptions?: { timeout?: number; retry?: { attempts?: number; baseDelay?: number; maxDelay?: number } }— identify HTTP timeout (ms, default1000) and POST retry (defaults2/400/600).kvTimeout?: number— deprecated, useidentifyOptions.timeout.autoSolveChallenge?: boolean— Whentrue(default),423responses that includeX-PoW-Challengeare solved and the request is retried once withX-PoW-Solution.onChallengeReceived?/onChallengeSolved?/onChallengeFailed?— PoW lifecycle hooks (reason string, solve time in ms, or error).storage?: { getItem, setItem }— Async key/value storage for a stable per-install visitor id (recommended for production).
Audit Log APIs
dashClient() API
dashClient() adds:
authClient.dash.getAuditLogs(input)
getAuditLogs(input) accepts:
limit?: number(default50, max100)offset?: number(default0)organizationId?: stringidentifier?: stringeventType?: stringuserId?: stringuser?: { id?: string | null }session?: { user?: { id?: string | null } }
The resolved user ID is determined in this order:
input.userIddashClient({ resolveUserId })input.user?.idinput.session?.user?.id
Response shape:
events: DashAuditLog[]total: numberlimit: numberoffset: number
Example:
const session = await authClient.getSession().then((r) => r.data);
const logs = await authClient.dash.getAuditLogs({
session,
organizationId: "org_123",
limit: 50,
offset: 0,
});To fetch all events, keep paginating with offset until events.length < limit.
Filtering
Use getAuditLogs filters directly in the query:
eventType: only return a specific event type (for exampleuser_signed_in)organizationId: scope logs to one organizationidentifier: narrow organization logs to a specific identifieruserId/user/session: resolve which user the logs should be scoped to
Examples:
// 1) Filter by event type
const signIns = await authClient.dash.getAuditLogs({
session,
eventType: "user_signed_in",
limit: 20,
});
// 2) Filter by org + identifier
const orgMemberEvents = await authClient.dash.getAuditLogs({
session,
organizationId: "org_123",
identifier: "[email protected]",
limit: 50,
});
// 3) Combine filters
const orgSignIns = await authClient.dash.getAuditLogs({
session,
organizationId: "org_123",
eventType: "user_signed_in",
limit: 50,
});Search Patterns
getAuditLogs does not currently expose a dedicated full-text search query param.
For text search, fetch pages and filter client-side.
const matchesText = (event: { eventType: string; eventKey: string; eventData: Record<string, unknown> }, query: string) => {
const q = query.toLowerCase().trim();
if (!q) return true;
const haystack = [
event.eventType,
event.eventKey,
JSON.stringify(event.eventData ?? {}),
]
.join(" ")
.toLowerCase();
return haystack.includes(q);
};
const page = await authClient.dash.getAuditLogs({ session, limit: 100, offset: 0 });
const filtered = page.data?.events.filter((event) => matchesText(event, "password")) ?? [];You can apply the same pattern for:
- date range filtering (by
createdAt) - location filtering (by
location.country,location.city, etc.) - multi-field compound filters
Pagination Helper
async function getAllAuditLogs(session: unknown) {
const limit = 100;
let offset = 0;
const all: Array<{
eventType: string;
createdAt: string;
eventData: Record<string, unknown>;
}> = [];
while (true) {
const result = await authClient.dash.getAuditLogs({ session, limit, offset });
const events = result.data?.events ?? [];
all.push(...events);
if (events.length < limit) break;
offset += limit;
}
return all;
}dash() Event Endpoints
The dash() plugin registers these event endpoints:
getUserEventsonGET /events/listgetAuditLogsonGET /events/audit-logsgetEventTypesonGET /events/types
getAuditLogs supports query params:
limit,offset,eventTypeorganizationId,identifieruserId(must match the authenticated session user)
Option Types
DashOptions
apiUrl?: stringkvUrl?: stringapiKey?: stringapiOptions?: { timeout?: number }(ms, default3000)kvOptions?: { timeout?: number; retry?: { attempts?: number; baseDelay?: number; maxDelay?: number } }— KV timeout default1000; retry defaults2/400/600apiTimeout?: number— deprecated, useapiOptions.timeoutkvTimeout?: number— deprecated, usekvOptions.timeoutactivityTracking?: { enabled?: boolean; updateInterval?: number }managedDirectorySync?: { enabled?: boolean; ssoPairing?: boolean; membershipProjection?: { enabled?: boolean; role?: string } }— opt into the 1.7+ managed directory-sync control plane (schema + reservation APIs).enabledalone turns on reservation tables/APIs. When enabled, dashinitcan also wire sibling-plugin callbacks:ssoPairinginstalls SSOresolveUser/guardProviderMutation(needed only for paired directories; requiressso({})or richer options — baresso()leavesoptionsundefined), andmembershipProjectioninstalls SCIMprojection.reconcileUserfor org membership sync. Defaults:enabled: false,ssoPairing: true,membershipProjection.enabled: true,membershipProjection.role: "member". Required withscim({ managedConnections })for managed mode; does not affect or replace legacy pre-1.7 SCIM directory sync.
dash() no longer accepts sentinel security config.
SentinelOptions
apiUrl?: stringkvUrl?: stringapiKey?: stringapiOptions?: { timeout?: number }— same asDashOptionskvOptions?: { timeout?: number; retry?: { attempts?: number; baseDelay?: number; maxDelay?: number } }— same asDashOptionsapiTimeout?: number— deprecated, useapiOptions.timeoutkvTimeout?: number— deprecated, usekvOptions.timeoutsecurity?: SecurityOptions
All security configuration now belongs in sentinel().
DashClientOptions
resolveUserId?: ({ userId, user, session }) => string | undefined
See Audit Log APIs above for full method details.
SentinelNativeClientOptions
Exported from @better-auth/infra/native. See React Native client for the full option list and usage.
Migration
If you previously passed security config to dash(), move it to sentinel():
// before
dash({
apiKey: process.env.BETTER_AUTH_API_KEY,
// no longer supported in dash
security: {
credentialStuffing: { enabled: true },
},
});
// after
dash({ apiKey: process.env.BETTER_AUTH_API_KEY });
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
credentialStuffing: { enabled: true },
},
});Security Notes
- Use
sentinel()to enforce security checks.dash()is focused on telemetry/admin behavior. - Provide
BETTER_AUTH_API_KEY; without it, sentinel cannot securely call infra APIs and will warn at startup. - If you run behind a proxy/CDN, validate your upstream header trust model (
x-forwarded-for, etc.) to avoid spoofed client IP attribution.
