docs/content/docs/infrastructure/plugins/sentinel.mdx
import { betterAuth } from "better-auth";
import { sentinel } from "@better-auth/infra";
export const auth = betterAuth({
plugins: [
sentinel({
security: {
// Configure security features here
},
}),
],
});
| Option | Type | Description |
|---|---|---|
apiUrl | string | Better Auth Infrastructure API URL |
kvUrl | string | KV store URL for rate limiting data |
apiKey | string | Your API key for authentication |
apiTimeout | number | Timeout in ms for Infra API HTTP requests (apiUrl). Default: 3000 |
kvTimeout | number | Timeout in ms for KV HTTP requests (kvUrl). Default: 1000 |
security | SecurityOptions | Security feature configuration |
interface SecurityOptions {
unknownDeviceNotification?: boolean;
credentialStuffing?: CredentialStuffingConfig;
impossibleTravel?: ImpossibleTravelConfig;
geoBlocking?: GeoBlockingConfig;
botBlocking?: boolean | { action: SecurityAction };
suspiciousIpBlocking?: boolean | { action: SecurityAction };
velocity?: VelocityConfig;
freeTrialAbuse?: FreeTrialAbuseConfig;
compromisedPassword?: CompromisedPasswordConfig;
emailValidation?: EmailValidationConfig;
emailNormalization?: { enabled?: boolean };
staleUsers?: StaleUsersConfig;
challengeDifficulty?: number;
}
type SecurityAction = "log" | "challenge" | "block";
Detects and blocks credential stuffing attacks by tracking failed login attempts per visitor.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
credentialStuffing: {
enabled: true,
thresholds: {
challenge: 3, // Issue PoW challenge after 3 failures
block: 5, // Block after 5 failures
},
windowSeconds: 3600, // 1 hour window
cooldownSeconds: 900, // 15 minute cooldown after block
},
},
}),
How it works:
Detects logins from geographically distant locations in impossibly short timeframes.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
impossibleTravel: {
enabled: true,
maxSpeedKmh: 1000, // Max realistic travel speed
action: "challenge", // "log", "challenge", or "block"
},
},
}),
Example: If a user logs in from New York and then 30 minutes later from Tokyo, this would be flagged as impossible travel (would require traveling faster than 1000 km/h).
Prevents users from creating multiple accounts to abuse free trials using device fingerprinting.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
freeTrialAbuse: {
enabled: true,
thresholds: {
challenge: 2,
block: 3,
},
maxAccountsPerVisitor: 3,
action: "block",
},
},
}),
How it works:
Checks passwords against the HaveIBeenPwned database to detect compromised credentials.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
compromisedPassword: {
enabled: true,
action: "block", // "log", "challenge", or "block"
minBreachCount: 1, // Minimum breaches to trigger
},
},
}),
Privacy: Uses k-anonymity - only the first 5 characters of the password hash are sent to the API, never the full password.
Detects when dormant accounts suddenly become active, which could indicate account takeover.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
staleUsers: {
enabled: true,
staleDays: 90, // Account considered stale after 90 days
action: "log", // "log", "challenge", or "block"
notifyUser: true, // Send email to user
notifyAdmin: true, // Send email to admin
adminEmail: "[email protected]",
},
},
}),
Notifications include:
Block or challenge users from specific countries.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
geoBlocking: {
allowList: ["US", "CA", "GB"], // Only allow these countries
// OR
denyList: ["XX", "YY"], // Block these countries
action: "block", // "challenge" or "block"
},
},
}),
Use ISO 3166-1 alpha-2 country codes.
Detect and block automated bot traffic.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
botBlocking: true,
// OR with custom action
botBlocking: {
action: "challenge", // "log", "challenge", or "block"
},
},
}),
Block requests from known malicious IP addresses.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
suspiciousIpBlocking: true,
// OR with custom action
suspiciousIpBlocking: {
action: "block",
},
},
}),
Limit the rate of various operations.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
velocity: {
enabled: true,
thresholds: {
challenge: 10,
block: 20,
},
maxSignupsPerVisitor: 5,
maxPasswordResetsPerIp: 10,
maxSignInsPerIp: 50,
windowSeconds: 3600,
action: "challenge",
},
},
}),
Block disposable email addresses and validate email domains.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
emailValidation: {
enabled: true,
strictness: "medium", // "low", "medium", or "high"
action: "block",
},
},
}),
Strictness levels:
low - Block only known disposable domainsmedium - Also check for valid MX recordshigh - Additional heuristic checksSentinel can normalize email addresses before sign-up and sign-in so aliases and provider quirks do not create duplicate accounts or mismatched logins. Normalization includes lowercasing, stripping plus-address tags on common providers (for example [email protected] → [email protected]), removing dots in Gmail-style addresses, and mapping googlemail.com to gmail.com.
Use security.emailNormalization when you want to control this separately from disposable-domain validation (emailValidation):
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
emailValidation: {
enabled: false, // skip disposable / MX checks
},
emailNormalization: {
enabled: true, // still normalize for deduplication and consistent sign-in
},
},
}),
When a security check results in a "challenge" action, Sentinel issues a Proof-of-Work (PoW) challenge that must be solved by the client.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
challengeDifficulty: 18, // Default difficulty level
},
}),
Higher difficulty = more computation required = slower for attackers.
The client plugin handles device fingerprinting and automatic PoW challenge solving.
import { createAuthClient } from "better-auth/client";
import { sentinelClient } from "@better-auth/infra/client";
export const authClient = createAuthClient({
plugins: [
sentinelClient({
autoSolveChallenge: true,
}),
],
});
| Option | Type | Default | Description |
|---|---|---|---|
autoSolveChallenge | boolean | true | Automatically solve PoW challenges |
kvTimeout | number | 1000 | Timeout in ms for KV identify and related HTTP requests |
The client automatically includes a visitor ID in requests via the X-Visitor-Id header. This fingerprint is used for:
When auto-solving is enabled, solved challenges are sent via the X-PoW-Solution header.
For Expo and React Native apps, use sentinelNativeClient from @better-auth/infra/native instead of using @better-auth/infra/client.
For React Native apps, install the following peer dependencies:
@react-native-async-storage/async-storage react-native-get-random-values
@react-native-async-storage/async-storage is optional. If it is not installed, the client uses a session-only in-memory visitor ID. For production, install it or pass a custom storage (for example a secure store).
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({
autoSolveChallenge: true,
}),
],
});
For Expo apps, install the packages below for richer native identity payloads.
<Callout type="info"> **Expo:** Follow the [Expo integration](/docs/integrations/expo) before adding infrastructure plugins. </Callout>expo-constants expo-device expo-crypto
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import { dashClient, sentinelNativeClient } from "@better-auth/infra/native";
import * as SecureStore from "expo-secure-store";
export const authClient = createAuthClient({
baseURL: "https://your-api.example.com",
plugins: [
expoClient({
scheme: "myapp",
storagePrefix: "myapp",
storage: SecureStore,
}),
dashClient(),
sentinelNativeClient({
autoSolveChallenge: true,
}),
],
});
sentinelNativeClient options| Option | Type | Default | Description |
|---|---|---|---|
identifyUrl | string | BETTER_AUTH_KV_URL, then https://kv.better-auth.com | KV identify endpoint base URL |
kvTimeout | number | 1000 | Timeout in ms for KV identify and related HTTP requests |
autoSolveChallenge | boolean | true | On 423 with X-PoW-Challenge, solve and retry once with X-PoW-Solution |
onChallengeReceived | (reason: string) => void | — | Called when a PoW challenge is received |
onChallengeSolved | (solveTimeMs: number) => void | — | Called after a successful solve |
onChallengeFailed | (error: Error) => void | — | Called if solving fails |
storage | { getItem, setItem } | Async Storage when installed | Persistent async storage for a stable per-install visitor ID |
Sentinel tracks the following security event types:
| Event Type | Description |
|---|---|
security_blocked | Request was blocked |
security_allowed | Request was allowed after challenge |
security_credential_stuffing | Credential stuffing detected |
security_impossible_travel | Impossible travel detected |
security_geo_blocked | Geo-blocking triggered |
security_bot_blocked | Bot detected and blocked |
security_suspicious_ip | Suspicious IP detected |
security_velocity_exceeded | Rate limit exceeded |
security_free_trial_abuse | Free trial abuse detected |
security_compromised_password | Compromised password detected |
security_stale_account | Stale account reactivation |
These events are visible in the Security dashboard and included in audit logs.
import { betterAuth } from "better-auth";
import { sentinel } from "@better-auth/infra";
export const auth = betterAuth({
plugins: [
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
// Core protections
credentialStuffing: {
enabled: true,
thresholds: { challenge: 3, block: 5 },
},
compromisedPassword: {
enabled: true,
action: "block",
},
emailValidation: {
enabled: true,
strictness: "medium",
action: "block",
},
// Location-based
impossibleTravel: {
enabled: true,
action: "challenge",
},
geoBlocking: {
denyList: ["XX"],
action: "block",
},
// Abuse prevention
freeTrialAbuse: {
enabled: true,
maxAccountsPerVisitor: 3,
action: "block",
},
velocity: {
enabled: true,
maxSignupsPerVisitor: 5,
action: "challenge",
},
// Bot protection
botBlocking: { action: "challenge" },
suspiciousIpBlocking: { action: "block" },
// Account monitoring
staleUsers: {
enabled: true,
staleDays: 90,
notifyUser: true,
notifyAdmin: true,
adminEmail: "[email protected]",
},
},
}),
],
});
Start with logging - Set actions to "log" initially to understand your traffic patterns before blocking.
Tune thresholds - Every application is different. Monitor false positives and adjust thresholds accordingly.
Use challenges before blocks - Challenges allow legitimate users through while stopping automated attacks.
Enable client-side auto-solve - Use sentinelClient({ autoSolveChallenge: true }) in the browser, or sentinelNativeClient({ autoSolveChallenge: true }) in Expo / React Native.
Monitor security events - Regularly review the Security dashboard to identify attack patterns.
Set up admin notifications - Enable notifyAdmin for critical events like stale account reactivations.
[Sentinel] Missing BETTER_AUTH_API_KEY. Security checks may fall back to allow mode.
Make sure your API key environment variable is set correctly.
If PoW challenges aren't being solved:
sentinelClient() for web (@better-auth/infra/client), or sentinelNativeClient() for Expo / React Native (@better-auth/infra/native)autoSolveChallenge is trueIf legitimate users are being blocked: