docs: infrastructure docs (#8086)

Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
Maxwell
2026-03-01 13:50:09 -08:00
committed by Bereket Engida
co-authored by Alex Yang
parent 099c3df37d
commit 927fb0a769
14 changed files with 4764 additions and 571 deletions
+5 -1
View File
@@ -44,4 +44,8 @@ dyld
nsis
ftyp
heix
whsec
sessionMiddleware
SIEM
sess
reactivations
whsec
+60
View File
@@ -0,0 +1,60 @@
"use client";
import { useTheme } from "next-themes";
import { use, useEffect, useId, useState } from "react";
export function Mermaid({ chart }: { chart: string }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return;
return <MermaidContent chart={chart} />;
}
const cache = new Map<string, Promise<unknown>>();
function cachePromise<T>(
key: string,
setPromise: () => Promise<T>,
): Promise<T> {
const cached = cache.get(key);
if (cached) return cached as Promise<T>;
const promise = setPromise();
cache.set(key, promise);
return promise;
}
function MermaidContent({ chart }: { chart: string }) {
const id = useId();
const { resolvedTheme } = useTheme();
const { default: mermaid } = use(
cachePromise("mermaid", () => import("mermaid")),
);
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
fontFamily: "inherit",
themeCSS: "margin: 1.5rem auto 0;",
theme: resolvedTheme === "dark" ? "dark" : "default",
});
const { svg, bindFunctions } = use(
cachePromise(`${chart}-${resolvedTheme}`, () => {
return mermaid.render(id, chart.replaceAll("\\n", "\n"));
}),
);
return (
<div
ref={(container) => {
if (container) bindFunctions?.(container);
}}
dangerouslySetInnerHTML={{ __html: svg }}
/>
);
}
@@ -0,0 +1,119 @@
---
title: Getting Started
description: This guide will help you integrate Better Auth Infrastructure into your application.
---
<Steps>
<Step>
## Prerequisites
Before you begin, make sure you have:
1. A working [Better Auth](/docs/installation) installation
2. An account & API Key from the [Better Auth Infrastructure](/dashboard) dashboard
</Step>
<Step>
## Installation
Install the `@better-auth/infra` package:
```package-install
@better-auth/infra
```
</Step>
<Step>
## Environment Variables
Once you've gotten the API Key from the infrastructure dashboard,
go ahead and add the `BETTER_AUTH_API_KEY` environment variable in your production environment variables.
```dotenv
# Required: Your Better Auth Infrastructure API key
BETTER_AUTH_API_KEY=your_api_key_here
```
<Callout>
You can get your API key by signing-up and creating a new project in the
[Better Auth Infrastructure dashboard](/dashboard).
</Callout>
</Step>
</Steps>
## Server Setup
### Basic Configuration
Add the `dash()` plugin to your Better Auth configuration:
```ts
import { betterAuth } from "better-auth";
import { dash } from "@better-auth/infra"; // [!code highlight]
export const auth = betterAuth({
// ... your existing Better Auth config
plugins: [
dash({ // [!code highlight]
apiKey: process.env.BETTER_AUTH_API_KEY, // [!code highlight]
}), // [!code highlight]
],
});
```
If you're on **pro** plan or above, you can use the `sentinel()` plugin to enable security checks, abuse protection, and more:
```ts
import { betterAuth } from "better-auth";
import { dash, sentinel } from "@better-auth/infra"; // [!code highlight]
export const auth = betterAuth({
plugins: [
dash({ /* ... */ }),
sentinel({ // [!code highlight]
apiKey: process.env.BETTER_AUTH_API_KEY, // [!code highlight]
}) // [!code highlight]
],
});
```
## Client Setup
### Basic Client Configuration
Add the client plugins to your auth client:
```ts
import { createAuthClient } from "better-auth/client";
import { dashClient, sentinelClient } from "@better-auth/infra/client";
export const authClient = createAuthClient({
plugins: [
dashClient(),
sentinelClient({
autoSolveChallenge: true, // Automatically solve PoW challenges
}),
],
});
```
### Plugin Overview
- **`dash()`** - Enables analytics tracking, audit logging, dashboard admin APIs, and more
- **`sentinel()`** - Enables security checks and abuse protection
You can use either plugin independently, but using both together provides the full Better Auth Infrastructure experience.
## Next Steps
Now that you have the basic setup, explore these topics:
- [Dashboard](/docs/infrastructure/plugins/dashboard) - Analytics, activity tracking, and admin APIs
- [Audit Logs](/docs/infrastructure/plugins/audit-logs) - Track and query authentication events
- [Sentinel](/docs/infrastructure/plugins/sentinel) - Configure advanced security
- [Email Service](/docs/infrastructure/services/email) - Set up transactional emails
- [SMS Service](/docs/infrastructure/services/sms) - Enable SMS verification
@@ -0,0 +1,40 @@
---
title: Better Auth Infrastructure
description: Enterprise-grade dashboard, security, and managed services for Better Auth.
---
Better Auth Infrastructure is a paid service that extends your authentication system with a management dashboard,
abuse protection, transactional messaging, and enterprise features - all without managing additional infrastructure.
## Key Features
<Cards>
<Card title="Dashboard" description="Manage users, organizations, sessions, and view analytics from a single interface." />
<Card title="Security" description="Protect against credential stuffing, bots, disposable emails, and impossible travel." />
<Card title="Email & SMS" description="Send verification emails, password resets, and OTP codes with pre-built templates." />
<Card title="Enterprise" description="SSO/SAML, directory sync (SCIM), log drains, and role-based dashboard access." />
</Cards>
You're one plugin away from getting started:
```ts
import { betterAuth } from "better-auth";
import { dash } from "@better-auth/infra";
import { sentinel } from "@better-auth/infra";
export const auth = betterAuth({
plugins: [dash()],
});
```
Check out our [Getting Started](/docs/infrastructure/getting-started) guide for more details.
## Next Steps
<Cards>
<Card href="/docs/infrastructure/getting-started" title="Getting Started" description="Install and configure the infrastructure package." />
<Card href="/docs/infrastructure/plugins/dashboard" title="Dashboard" description="Set up analytics, activity tracking, and admin APIs." />
<Card href="/docs/infrastructure/plugins/audit-logs" title="Audit Logs" description="Track and query authentication events." />
<Card href="/docs/infrastructure/plugins/sentinel" title="Sentinel" description="Enable abuse protection and security checks." />
</Cards>
@@ -0,0 +1,197 @@
---
title: Audit Logs
description: Track and query authentication events across your application with automatic audit logging.
---
Better Auth Infrastructure automatically collects audit logs for authentication events in your application. Once you've added the `dash()` plugin, events are tracked without any additional configuration.
## How Audit Logs Are Collected
The `dash()` plugin hooks into your Better Auth instance and automatically records events as they happen. No manual instrumentation is needed — sign-ups, sign-ins, password changes, and more are all captured.
```ts
import { betterAuth } from "better-auth";
import { dash } from "@better-auth/infra";
export const auth = betterAuth({
plugins: [
dash({
apiKey: process.env.BETTER_AUTH_API_KEY,
}),
],
});
```
That's it. Once `dash()` is active, audit logs are collected automatically.
## Tracked Events
<Tabs items={["User", "Session", "Account", "Verification", "Organization", "Security"]}>
<Tab value="User">
| Event | Trigger |
|-------|---------|
| `user_signed_up` | New user registration |
| `user_profile_updated` | User updates their profile |
| `user_profile_image_updated` | User changes their avatar |
| `user_email_verified` | Email verification completed |
| `user_banned` | User is banned |
| `user_unbanned` | User is unbanned |
| `user_deleted` | User account deleted |
</Tab>
<Tab value="Session">
| Event | Trigger |
|-------|---------|
| `user_signed_in` | Successful sign-in |
| `user_signed_out` | User signs out |
| `session_created` | New session created |
| `session_revoked` | Single session revoked |
| `sessions_revoked_all` | All sessions revoked |
| `user_impersonated` | Admin starts impersonating user |
| `user_impersonation_stopped` | Admin stops impersonating |
</Tab>
<Tab value="Account">
| Event | Trigger |
|-------|---------|
| `account_linked` | Social account linked |
| `account_unlinked` | Social account unlinked |
| `password_changed` | Password updated |
</Tab>
<Tab value="Verification">
| Event | Trigger |
|-------|---------|
| `password_reset_requested` | Password reset initiated |
| `password_reset_completed` | Password reset finished |
| `email_verification_sent` | Verification email sent |
</Tab>
<Tab value="Organization">
Tracked when using the [organization plugin](/docs/plugins/organization).
| Event | Trigger |
|-------|---------|
| `organization_created` | New organization created |
| `organization_updated` | Organization settings changed |
| `member_added` | Member added to organization |
| `member_removed` | Member removed from organization |
| `member_role_updated` | Member role changed |
| `member_invited` | Invitation sent |
| `invite_accepted` | Invitation accepted |
| `invite_rejected` | Invitation rejected |
| `invite_cancelled` | Invitation cancelled |
| `team_created` | Team created |
| `team_updated` | Team updated |
| `team_deleted` | Team deleted |
| `team_member_added` | Member added to team |
| `team_member_removed` | Member removed from team |
</Tab>
<Tab value="Security">
Tracked when using the [Sentinel](/docs/infrastructure/plugins/sentinel) plugin.
| Event | Trigger |
|-------|---------|
| `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 |
</Tab>
</Tabs>
## Fetching Audit Logs
### Client Setup
Add `dashClient()` to your auth client:
```ts
import { createAuthClient } from "better-auth/client";
import { dashClient } from "@better-auth/infra/client";
export const authClient = createAuthClient({
plugins: [dashClient()],
});
```
### Basic Query
```ts
const session = await authClient.getSession();
const logs = await authClient.dash.getAuditLogs({
session: session.data,
limit: 50,
offset: 0,
});
logs.data?.events; // Array of audit log events
logs.data?.total; // Total count
logs.data?.limit; // Page size
logs.data?.offset; // Current offset
```
### Query Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `limit` | `number` | Results per page (max 100, default 50) |
| `offset` | `number` | Pagination offset (default 0) |
| `organizationId` | `string` | Filter by organization |
| `identifier` | `string` | Filter by identifier |
| `eventType` | `string` | Filter by event type |
| `userId` | `string` | Filter by user ID |
| `user` | `object` | User object with ID |
| `session` | `object` | Session object with user |
### Filtering
```ts
// Filter by event type
const signIns = await authClient.dash.getAuditLogs({
session,
eventType: "user_signed_in",
});
// Filter by organization
const orgLogs = await authClient.dash.getAuditLogs({
session,
organizationId: "org_123",
});
// Combine filters
const orgSignIns = await authClient.dash.getAuditLogs({
session,
organizationId: "org_123",
eventType: "user_signed_in",
});
```
### Pagination
```ts
async function getAllAuditLogs(session: unknown) {
const limit = 100;
let offset = 0;
const allEvents = [];
while (true) {
const result = await authClient.dash.getAuditLogs({
session,
limit,
offset,
});
const events = result.data?.events ?? [];
allEvents.push(...events);
if (events.length < limit) break;
offset += limit;
}
return allEvents;
}
```
@@ -0,0 +1,101 @@
---
title: Dashboard
description: The `dash()` plugin connects your Better Auth instance to Better Auth Infrastructure, enabling analytics tracking, activity monitoring, and admin dashboard APIs.
---
The Dashboard plugin is the core connection between your Better Auth instance and Better Auth Infrastructure. It powers the web dashboard with real-time data, tracks user activity, and enables admin APIs.
## Installation
```ts
import { betterAuth } from "better-auth";
import { dash } from "@better-auth/infra";
export const auth = betterAuth({
plugins: [
dash({
apiKey: process.env.BETTER_AUTH_API_KEY,
}),
],
});
```
## Configuration Options
| Option | Type | Description |
|--------|------|-------------|
| `apiUrl` | `string` | Better Auth Infrastructure API URL |
| `kvUrl` | `string` | KV store URL for caching |
| `apiKey` | `string` | Your API key for authentication |
| `activityTracking` | `object` | Activity tracking configuration |
## Activity Tracking
Track when users were last active in your application. When enabled, a `lastActiveAt` field is automatically updated on user activity.
```ts
dash({
apiKey: process.env.BETTER_AUTH_API_KEY,
activityTracking: {
enabled: true,
updateInterval: 300000, // Update interval in ms (default: 5 minutes)
},
}),
```
### Schema Changes
When activity tracking is enabled, the plugin adds a field to your user schema:
```ts
user: {
fields: {
lastActiveAt: {
type: "date",
},
},
}
```
Make sure to run database migrations after enabling activity tracking.
## Client Setup
```ts
import { createAuthClient } from "better-auth/client";
import { dashClient } from "@better-auth/infra/client";
export const authClient = createAuthClient({
plugins: [dashClient()],
});
```
### Client Configuration
```ts
dashClient({
resolveUserId: ({ userId, user, session }) => {
return userId || user?.id || session?.user?.id;
},
}),
```
## What the Dashboard Plugin Enables
Once `dash()` is active, the Better Auth Infrastructure dashboard gives you:
- **User management** — view, search, ban, and delete users
- **Session monitoring** — see active sessions and revoke them
- **Organization overview** — manage organizations and members
- **Analytics** — track sign-ups, sign-ins, and active users over time
- **Audit logs** — query event history ([learn more](/docs/infrastructure/plugins/audit-logs))
## Best Practices
1. **Always set an API key** — without it, the plugin cannot communicate with the infrastructure API.
2. **Use activity tracking wisely** — the update interval affects database writes. For high-traffic apps, consider a longer interval.
3. **Monitor audit log retention** — different plans have different retention periods. Check your plan limits.
4. **Secure your endpoints** — dashboard endpoints require authentication. Make sure your dashboard users have appropriate permissions.
@@ -0,0 +1,498 @@
---
title: Security Plugin (sentinel)
description: The `sentinel()` plugin provides comprehensive security and abuse protection for your authentication system. It detects and prevents various attack vectors including credential stuffing, impossible travel, free trial abuse, and more.
---
## Installation
```tsx
import { betterAuth } from "better-auth";
import { sentinel } from "@better-auth/infra";
export const auth = betterAuth({
plugins: [
sentinel({
apiUrl: process.env.BETTER_AUTH_API_URL,
kvUrl: process.env.BETTER_AUTH_KV_URL,
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
// Configure security features here
},
}),
],
});
```
## Configuration Options
### SentinelOptions
| 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 |
| `security` | `SecurityOptions` | Security feature configuration |
### SecurityOptions
```ts
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;
staleUsers?: StaleUsersConfig;
challengeDifficulty?: number;
}
type SecurityAction = "log" | "challenge" | "block";
```
## Security Features
### Credential Stuffing Protection
Detects and blocks credential stuffing attacks by tracking failed login attempts per visitor.
```ts
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:**
1. Tracks failed login attempts per visitor ID
2. After reaching the challenge threshold, issues a Proof-of-Work challenge
3. After reaching the block threshold, blocks the visitor entirely
4. Automatically clears failed attempts on successful login
### Impossible Travel Detection
Detects logins from geographically distant locations in impossibly short timeframes.
```ts
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).
### Free Trial Abuse Prevention
Prevents users from creating multiple accounts to abuse free trials using device fingerprinting.
```ts
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
freeTrialAbuse: {
enabled: true,
thresholds: {
challenge: 2,
block: 3,
},
maxAccountsPerVisitor: 3,
action: "block",
},
},
}),
```
**How it works:**
1. Tracks account creations per visitor fingerprint
2. When threshold is exceeded, blocks new account creation
3. Useful for preventing free tier abuse
### Compromised Password Detection
Checks passwords against the HaveIBeenPwned database to detect compromised credentials.
```ts
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.
### Stale Account Monitoring
Detects when dormant accounts suddenly become active, which could indicate account takeover.
```ts
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: "admin@yourapp.com",
},
},
}),
```
**Notifications include:**
- Login time and location
- Days since last activity
- Device information
### Geo-Blocking
Block or challenge users from specific countries.
```ts
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.
### Bot Blocking
Detect and block automated bot traffic.
```ts
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
botBlocking: true,
// OR with custom action
botBlocking: {
action: "challenge", // "log", "challenge", or "block"
},
},
}),
```
### Suspicious IP Detection
Block requests from known malicious IP addresses.
```ts
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
suspiciousIpBlocking: true,
// OR with custom action
suspiciousIpBlocking: {
action: "block",
},
},
}),
```
### Velocity / Rate Limiting
Limit the rate of various operations.
```ts
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",
},
},
}),
```
### Email Validation
Block disposable email addresses and validate email domains.
```ts
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 domains
- `medium` - Also check for valid MX records
- `high` - Additional heuristic checks
## Proof-of-Work Challenges
When a security check results in a "challenge" action, Sentinel issues a Proof-of-Work (PoW) challenge that must be solved by the client.
### How PoW Works
1. Server issues a cryptographic challenge
2. Client must find a solution that satisfies difficulty requirements
3. Solution is computationally expensive but verification is fast
4. Prevents automated attacks while allowing legitimate users through
### Challenge Difficulty
```ts
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
challengeDifficulty: 18, // Default difficulty level
},
}),
```
Higher difficulty = more computation required = slower for attackers.
## Client Integration
### sentinelClient()
The client plugin handles device fingerprinting and automatic PoW challenge solving.
```ts
import { createAuthClient } from "better-auth/client";
import { sentinelClient } from "@better-auth/infra/client";
export const authClient = createAuthClient({
plugins: [
sentinelClient({
autoSolveChallenge: true,
}),
],
});
```
### Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `autoSolveChallenge` | `boolean` | `true` | Automatically solve PoW challenges |
### Browser Fingerprinting
The client automatically includes a visitor ID in requests via the `X-Visitor-ID` header. This fingerprint is used for:
- Credential stuffing detection
- Free trial abuse prevention
- Device tracking
### PoW Solution Header
When auto-solving is enabled, solved challenges are sent via the `X-PoW-Solution` header.
## Protected Endpoints
Sentinel protects the following authentication endpoints:
### Sign-In Endpoints
- `/sign-in/email`
- `/sign-in/username`
- `/sign-in/email-otp`
- `/sign-in/social`
- `/sign-in/passkey`
- `/sign-in/magic-link`
- `/sign-in/sso`
- `/sign-in/anonymous`
### Sign-Up Endpoints
- `/sign-up/email`
### Password Endpoints
- `/forget-password`
- `/reset-password`
- `/change-password`
- `/set-password`
### Two-Factor Endpoints
- `/two-factor/verify-totp`
- `/two-factor/verify-backup`
- `/two-factor/verify-otp`
### OTP Endpoints
- `/email-otp/send`
- `/phone/send-otp`
### Other Sensitive Endpoints
- `/magic-link/verify`
- `/organization/create`
- `/change-email`
- `/link-social`
- `/passkey/add`
## Security Events
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.
## Complete Example
```ts
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: "security@yourapp.com",
},
},
}),
],
});
```
## Best Practices
1. **Start with logging** - Set actions to "log" initially to understand your traffic patterns before blocking.
2. **Tune thresholds** - Every application is different. Monitor false positives and adjust thresholds accordingly.
3. **Use challenges before blocks** - Challenges allow legitimate users through while stopping automated attacks.
4. **Enable client-side auto-solve** - Make sure `sentinelClient({ autoSolveChallenge: true })` is configured for smooth user experience.
5. **Monitor security events** - Regularly review the Security dashboard to identify attack patterns.
6. **Set up admin notifications** - Enable `notifyAdmin` for critical events like stale account reactivations.
## Troubleshooting
### Missing API Key Warning
```
[Sentinel] Missing BETTER_AUTH_API_KEY. Security checks may fall back to allow mode.
```
Make sure your API key environment variable is set correctly.
### Challenges Not Working
If PoW challenges aren't being solved:
1. Verify `sentinelClient()` is installed on the client
2. Check that `autoSolveChallenge` is `true`
3. Ensure the client can reach the server
### High False Positive Rate
If legitimate users are being blocked:
1. Increase thresholds
2. Change action from "block" to "challenge"
3. Review security events to identify patterns
@@ -0,0 +1,434 @@
---
title: Email Service
description: Better Auth Infrastructure provides a managed transactional email service with pre-built templates for common authentication flows. Send verification emails, password resets, invitations, and more without managing email infrastructure.
---
## Overview
The email service offers:
- Pre-built, professionally designed email templates
- Multiple provider support (AWS SES, SendGrid, Resend)
- Type-safe template variables
- No infrastructure to manage
- Deliverability optimization
## Installation
The email service is included in the `@better-auth/infra` package:
```ts
import { sendEmail, createEmailSender } from "@better-auth/infra";
```
## Quick Start
### Send a Single Email
```ts
import { sendEmail } from "@better-auth/infra";
await sendEmail({
template: "verify-email",
to: "user@example.com",
variables: {
verificationUrl: "https://yourapp.com/verify?token=abc123",
userEmail: "user@example.com",
userName: "John",
appName: "Your App",
},
});
```
### Create a Reusable Sender
```ts
import { createEmailSender } from "@better-auth/infra";
const emailSender = createEmailSender({
apiKey: process.env.BETTER_AUTH_API_KEY,
apiUrl: process.env.BETTER_AUTH_API_URL,
});
// Send multiple emails
await emailSender.send({
template: "reset-password",
to: "user@example.com",
variables: {
resetLink: "https://yourapp.com/reset?token=xyz",
userEmail: "user@example.com",
},
});
```
## Available Templates
### verify-email
Sends an email verification link to new users.
```ts
await sendEmail({
template: "verify-email",
to: "user@example.com",
variables: {
verificationUrl: "https://yourapp.com/verify?token=abc",
userEmail: "user@example.com",
verificationCode: "123456", // Optional: for code-based verification
userName: "John", // Optional
appName: "Your App", // Optional
expirationMinutes: "60", // Optional
},
});
```
### reset-password
Sends a password reset link.
```ts
await sendEmail({
template: "reset-password",
to: "user@example.com",
variables: {
resetLink: "https://yourapp.com/reset?token=xyz",
userEmail: "user@example.com",
userName: "John", // Optional
appName: "Your App", // Optional
expirationMinutes: "60", // Optional
},
});
```
### change-email
Confirms an email address change request.
```ts
await sendEmail({
template: "change-email",
to: "newemail@example.com",
variables: {
confirmationLink: "https://yourapp.com/confirm-email?token=abc",
newEmail: "newemail@example.com",
currentEmail: "oldemail@example.com",
userName: "John", // Optional
appName: "Your App", // Optional
expirationMinutes: "60", // Optional
},
});
```
### sign-in-otp
Sends a one-time password for passwordless sign-in.
```ts
await sendEmail({
template: "sign-in-otp",
to: "user@example.com",
variables: {
otpCode: "123456",
userEmail: "user@example.com",
appName: "Your App", // Optional
expirationMinutes: "10", // Optional
},
});
```
### verify-email-otp
Sends an OTP code for email verification.
```ts
await sendEmail({
template: "verify-email-otp",
to: "user@example.com",
variables: {
otpCode: "123456",
userEmail: "user@example.com",
appName: "Your App", // Optional
expirationMinutes: "10", // Optional
},
});
```
### reset-password-otp
Sends an OTP code for password reset.
```ts
await sendEmail({
template: "reset-password-otp",
to: "user@example.com",
variables: {
otpCode: "123456",
userEmail: "user@example.com",
appName: "Your App", // Optional
expirationMinutes: "10", // Optional
},
});
```
### magic-link
Sends a magic link for passwordless authentication.
```ts
await sendEmail({
template: "magic-link",
to: "user@example.com",
variables: {
magicLink: "https://yourapp.com/auth/magic?token=abc",
userEmail: "user@example.com",
appName: "Your App", // Optional
expirationMinutes: "15", // Optional
},
});
```
### two-factor
Sends a two-factor authentication code.
```ts
await sendEmail({
template: "two-factor",
to: "user@example.com",
variables: {
otpCode: "123456",
userEmail: "user@example.com",
userName: "John", // Optional
appName: "Your App", // Optional
expirationMinutes: "5", // Optional
},
});
```
### invitation
Sends an organization invitation.
```ts
await sendEmail({
template: "invitation",
to: "newmember@example.com",
variables: {
inviteLink: "https://yourapp.com/invite?token=abc",
inviterName: "John Smith",
inviterEmail: "john@company.com",
organizationName: "Acme Corp",
role: "Member",
appName: "Your App", // Optional
expirationDays: "7", // Optional
},
});
```
### application-invite
Sends an application-level invitation (inviting users to the platform).
```ts
await sendEmail({
template: "application-invite",
to: "newuser@example.com",
variables: {
inviteLink: "https://yourapp.com/join?token=abc",
inviterName: "John Smith",
inviterEmail: "john@company.com",
inviteeEmail: "newuser@example.com",
appName: "Your App", // Optional
expirationDays: "7", // Optional
},
});
```
### delete-account
Sends account deletion confirmation.
```ts
await sendEmail({
template: "delete-account",
to: "user@example.com",
variables: {
deletionLink: "https://yourapp.com/confirm-delete?token=abc",
userEmail: "user@example.com",
userName: "John", // Optional
appName: "Your App", // Optional
expirationMinutes: "60", // Optional
},
});
```
### stale-account-user
Notifies a user that their dormant account was accessed.
```ts
await sendEmail({
template: "stale-account-user",
to: "user@example.com",
variables: {
userEmail: "user@example.com",
daysSinceLastActive: "90",
loginTime: "February 20, 2026, 3:45 PM UTC",
userName: "John", // Optional
appName: "Your App", // Optional
loginLocation: "New York, US", // Optional
loginDevice: "Chrome on Windows", // Optional
loginIp: "192.168.1.1", // Optional
},
});
```
### stale-account-admin
Notifies an admin about dormant account reactivation.
```ts
await sendEmail({
template: "stale-account-admin",
to: "admin@yourapp.com",
variables: {
userEmail: "user@example.com",
userId: "user_123",
adminEmail: "admin@yourapp.com",
daysSinceLastActive: "90",
loginTime: "February 20, 2026, 3:45 PM UTC",
userName: "John", // Optional
appName: "Your App", // Optional
loginLocation: "New York, US", // Optional
loginDevice: "Chrome on Windows", // Optional
loginIp: "192.168.1.1", // Optional
},
});
```
## Configuration
### EmailConfig
```ts
interface EmailConfig {
apiKey?: string; // Your Better Auth Infrastructure API key
apiUrl?: string; // Custom API URL (optional)
}
```
### Environment Variables
The email service automatically reads from environment variables:
```dotenv
BETTER_AUTH_API_KEY=your_api_key_here
BETTER_AUTH_API_URL=https://api.betterauth.com # Optional
```
## Response Format
### SendEmailResult
```ts
interface SendEmailResult {
success: boolean;
messageId?: string; // Email provider message ID
error?: string; // Error message if failed
}
```
### Example Usage
```ts
const result = await sendEmail({
template: "verify-email",
to: "user@example.com",
variables: {
verificationUrl: "https://yourapp.com/verify?token=abc",
userEmail: "user@example.com",
},
});
if (result.success) {
console.log("Email sent:", result.messageId);
} else {
console.error("Failed to send email:", result.error);
}
```
## Plan Requirements
| Feature | Starter | Pro | Business | Enterprise |
|---------|---------|-----|----------|------------|
| Transactional Email | - | Yes | Yes | Yes |
Transactional email is available on Pro plans and above.
## Integration with Better Auth
The email service integrates seamlessly with Better Auth's authentication flows. Here's a complete example:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { sendEmail } from "@better-auth/infra";
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
async sendResetPassword({ user, url }) {
await sendEmail({
template: "reset-password",
to: user.email,
variables: {
resetLink: url,
userEmail: user.email,
userName: user.name,
appName: "Your App",
},
});
},
},
emailVerification: {
sendOnSignUp: true,
async sendVerificationEmail({ user, url }) {
await sendEmail({
template: "verify-email",
to: user.email,
variables: {
verificationUrl: url,
userEmail: user.email,
userName: user.name,
appName: "Your App",
},
});
},
},
plugins: [
organization({
async sendInvitationEmail(data) {
const inviteLink = `https://yourapp.com/accept-invitation/${data.id}`;
await sendEmail({
template: "invitation",
to: data.email,
variables: {
inviteLink,
inviterName: data.inviter.user.name,
inviterEmail: data.inviter.user.email,
organizationName: data.organization.name,
role: data.role,
appName: "Your App",
},
});
},
}),
],
});
```
<Callout type="warn">
Avoid awaiting the email sending in production to prevent timing attacks. On serverless platforms, use `waitUntil` or similar to ensure the email is sent without blocking the response.
</Callout>
@@ -0,0 +1,289 @@
---
title: SMS Service
description: Better Auth Infrastructure provides a managed SMS service for sending OTP codes for phone verification and two-factor authentication. Send verification codes without managing SMS providers.
---
## Overview
The SMS service offers:
- Pre-built SMS templates for common auth flows
- E.164 phone number format support
- Type-safe template variables
- No infrastructure to manage
- Global delivery support
<Callout type="warn">
Note that SMS delivery is intended to only be used for authentication flows.
</Callout>
## Installation
The SMS service is included in the `@better-auth/infra` package:
```ts
import { sendSMS, createSMSSender } from "@better-auth/infra";
```
## Quick Start
### Send a Single SMS
```ts
import { sendSMS } from "@better-auth/infra";
await sendSMS({
to: "+1234567890",
code: "123456",
template: "phone-verification",
});
```
### Create a Reusable Sender
```ts
import { createSMSSender } from "@better-auth/infra";
const smsSender = createSMSSender({
apiKey: process.env.BETTER_AUTH_API_KEY,
apiUrl: process.env.BETTER_AUTH_API_URL,
});
// Send multiple SMS messages
await smsSender.send({
to: "+1234567890",
code: "123456",
template: "two-factor",
});
```
## Available Templates
### phone-verification
Sends a verification code for phone number verification.
```ts
await sendSMS({
to: "+1234567890",
code: "123456",
template: "phone-verification",
});
```
**Example message:**
> Your verification code is 123456. It expires in 10 minutes.
### two-factor
Sends a two-factor authentication code.
```ts
await sendSMS({
to: "+1234567890",
code: "123456",
template: "two-factor",
});
```
**Example message:**
> Your two-factor authentication code is 123456. Do not share this code with anyone.
### sign-in-otp
Sends a one-time password for passwordless sign-in.
```ts
await sendSMS({
to: "+1234567890",
code: "123456",
template: "sign-in-otp",
});
```
**Example message:**
> Your sign-in code is 123456. It expires in 10 minutes.
### Default (No Template)
If you don't specify a template, a generic verification message is sent:
```ts
await sendSMS({
to: "+1234567890",
code: "123456",
});
```
**Example message:**
> Your verification code is 123456.
## Phone Number Format
Phone numbers must be in E.164 format:
```
+[country code][number]
```
**Examples:**
- US: `+14155551234`
- UK: `+447911123456`
- Germany: `+4915112345678`
- Japan: `+819012345678`
**Common mistakes:**
- Missing `+` prefix: `14155551234` ❌
- Including spaces: `+1 415 555 1234` ❌
- Including dashes: `+1-415-555-1234` ❌
- Including parentheses: `+1 (415) 555-1234` ❌
## Configuration
### SMSConfig
```ts
interface SMSConfig {
apiKey?: string; // Your Better Auth Infrastructure API key
apiUrl?: string; // Custom API URL (optional)
}
```
### Environment Variables
The SMS service automatically reads from environment variables:
```dotenv
BETTER_AUTH_API_KEY=your_api_key_here
BETTER_AUTH_API_URL=https://api.betterauth.com # Optional
```
## API Reference
### sendSMS
Send a single SMS message.
```ts
async function sendSMS(
options: SendSMSOptions,
config?: SMSConfig
): Promise<SendSMSResult>
```
### SendSMSOptions
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `to` | `string` | Yes | Phone number in E.164 format |
| `code` | `string` | Yes | The OTP code to send |
| `template` | `SMSTemplateId` | No | Template to use (defaults to generic) |
### createSMSSender
Create a reusable SMS sender instance.
```ts
const sender = createSMSSender(config?: SMSConfig);
// Use the sender
await sender.send(options: SendSMSOptions);
```
## Response Format
### SendSMSResult
```ts
interface SendSMSResult {
success: boolean;
messageId?: string; // SMS provider message ID
error?: string; // Error message if failed
}
```
### Example Usage
```ts
const result = await sendSMS({
to: "+1234567890",
code: "123456",
template: "phone-verification",
});
if (result.success) {
console.log("SMS sent:", result.messageId);
} else {
console.error("Failed to send SMS:", result.error);
}
```
## Error Handling
Common error scenarios:
```ts
const result = await sendSMS({
to: "+1234567890",
code: "123456",
});
if (!result.success) {
switch (result.error) {
case "API key not configured":
// Missing BETTER_AUTH_API_KEY
break;
case "Invalid phone number":
// Phone number not in E.164 format
break;
default:
// Other delivery error
console.error("SMS error:", result.error);
}
}
```
## Integration with Better Auth
When using the `dash()` or `sentinel()` plugins with Better Auth's phone authentication, SMS messages are automatically sent for:
- Phone number verification
- Phone-based two-factor authentication
- Phone OTP sign-in
You don't need to call `sendSMS()` manually for these flows - the plugins handle it automatically.
### Better Auth Phone Plugin Integration
```ts
import { betterAuth } from "better-auth";
import { phoneNumber } from "better-auth/plugins";
import { dash } from "@better-auth/infra";
export const auth = betterAuth({
plugins: [
phoneNumber({
sendOTP: async ({ phoneNumber, code }) => {
// This is handled automatically when dash() is configured
// But you can customize if needed:
await sendSMS({
to: phoneNumber,
code,
template: "phone-verification",
});
},
}),
dash({
apiKey: process.env.BETTER_AUTH_API_KEY,
}),
],
});
```
## Plan Requirements
| Feature | Starter | Pro | Business | Enterprise |
|---------|---------|-----|----------|------------|
| Transactional SMS | - | Yes | Yes | Yes |
Transactional SMS is available on Pro plans and above.
+102 -3
View File
@@ -3,8 +3,107 @@
"type": "module",
"private": true,
"dependencies": {
"fumadocs-ui": "16.6.7",
"lucide-react": "^0.575.0",
"react": "^19.1.0"
"@ai-sdk/openai-compatible": "^2.0.28",
"@ai-sdk/react": "^3.0.80",
"@better-auth/utils": "catalog:",
"@better-fetch/fetch": "catalog:",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-presence": "^1.1.5",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@scalar/nextjs-api-reference": "^0.9.14",
"@tanstack/react-table": "^8.21.3",
"@vercel/analytics": "^1.6.1",
"@vercel/og": "^0.8.6",
"ai": "^6.0.78",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.1.1",
"date-fns": "^4.1.0",
"dotenv": "^17.2.4",
"embla-carousel-react": "^8.6.0",
"feed": "^5.2.0",
"foxact": "^0.2.52",
"framer-motion": "^12.29.2",
"fumadocs-core": "16.5.2",
"fumadocs-mdx": "14.2.7",
"fumadocs-typescript": "^5.1.2",
"fumadocs-ui": "16.5.2",
"geist": "^1.5.1",
"gray-matter": "^4.0.3",
"hast-util-to-jsx-runtime": "^2.3.6",
"highlight.js": "^11.11.1",
"input-otp": "^1.4.2",
"jotai": "^2.17.1",
"js-beautify": "^1.15.4",
"jsrsasign": "^11.1.0",
"lottie-react": "^2.4.1",
"lucide-react": "^0.563.0",
"mermaid": "^11.12.3",
"motion": "^12.34.0",
"next": "16.1.6",
"next-themes": "^0.4.6",
"prism-react-renderer": "^2.4.1",
"react": "catalog:react19",
"react-day-picker": "9.13.0",
"react-dom": "catalog:react19",
"react-hook-form": "^7.71.1",
"react-markdown": "^10.1.0",
"react-remove-scroll": "^2.7.2",
"react-resizable-panels": "^4.5.2",
"react-use-measure": "^2.1.7",
"recharts": "^3.7.0",
"rehype-highlight": "^7.0.2",
"remark": "^15.0.1",
"remark-gfm": "^4.0.1",
"remark-mdx": "^3.1.1",
"remark-rehype": "^11.1.2",
"remark-stringify": "^11.0.0",
"resend": "^6.8.0",
"shiki": "^3.22.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "catalog:tailwind",
"unist-util-visit": "^5.1.0",
"vaul": "^1.1.2",
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/postcss": "catalog:tailwind",
"@types/hast": "^3.0.4",
"@types/js-beautify": "^1.14.3",
"@types/jsrsasign": "^10.5.15",
"@types/mdx": "^2.0.13",
"@types/react": "catalog:react19",
"@types/react-dom": "catalog:react19",
"mini-svg-data-uri": "^1.4.4",
"postcss": "^8.5.6",
"tailwindcss": "catalog:tailwind",
"typescript": "catalog:"
}
}
+73
View File
@@ -1,6 +1,9 @@
import type { Folder, Root } from "fumadocs-core/page-tree";
import type { LucideIcon } from "lucide-react";
import {
AppWindow,
BadgeCent,
BarChart4,
Binoculars,
Book,
CircleHelp,
@@ -9,15 +12,20 @@ import {
Gauge,
Key,
KeyRound,
Lock,
Logs,
LucideAArrowDown,
Mail,
Mailbox,
Phone,
ScanFace,
Server,
Settings,
ShieldCheck,
TriangleAlertIcon,
UserCircle,
UserSquare2,
Users,
Users2,
} from "lucide-react";
import type { ReactNode, SVGProps } from "react";
@@ -1535,6 +1543,71 @@ C0.7,239.6,62.1,0.5,62.2,0.4c0,0,54,13.8,119.9,30.8S302.1,62,302.2,62c0.2,0,0.2,
},
],
},
{
title: "Infrastructure",
Icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1.2em"
height="1.2em"
>
<path
fill="currentColor"
d="M2.25 5.7c0-.943.661-1.95 1.75-1.95h16c1.089 0 1.75 1.007 1.75 1.95v3.6c0 .943-.661 1.95-1.75 1.95H4c-1.089 0-1.75-1.007-1.75-1.95zM6 6.75a.75.75 0 0 0 0 1.5h2a.75.75 0 0 0 0-1.5zM2.25 14.7c0-.943.661-1.95 1.75-1.95h16c1.089 0 1.75 1.007 1.75 1.95v3.6c0 .943-.661 1.95-1.75 1.95H4c-1.089 0-1.75-1.007-1.75-1.95zM6 15.75a.75.75 0 0 0 0 1.5h2a.75.75 0 0 0 0-1.5z"
/>
</svg>
),
list: [
{
title: "Introduction",
href: "/docs/infrastructure/introduction",
icon: () => <Book className="size-4" />,
},
{
title: "Getting Started",
href: "/docs/infrastructure/getting-started",
icon: () => <Gauge className="size-4" />,
},
{
title: "Plugins",
group: true,
icon: () => <Server className="size-4" />,
href: "",
},
{
title: "Dashboard",
href: "/docs/infrastructure/plugins/dashboard",
icon: () => <AppWindow className="size-4" />,
},
{
title: "Audit Logs",
href: "/docs/infrastructure/plugins/audit-logs",
icon: () => <Logs className="size-4" />,
},
{
title: "Sentinel",
href: "/docs/infrastructure/plugins/sentinel",
icon: () => <ShieldCheck className="size-4" />,
},
{
title: "Services",
group: true,
icon: () => <Server className="size-4" />,
href: "",
},
{
title: "Email",
href: "/docs/infrastructure/services/email",
icon: () => <Mail className="size-4" />,
},
{
title: "SMS",
href: "/docs/infrastructure/services/sms",
icon: () => <Phone className="size-4" />,
},
],
},
{
title: "Plugins",
Icon: () => (
+1 -1
View File
@@ -28,7 +28,7 @@ export interface DynamicCodeblockProps {
* @defaultValue true
*/
allowCopy?: boolean;
options?: Omit<HighlightOptions, "lang">;
options?: Omit<CodeBlockProps, "lang"> & { components?: Record<string, FC> };
}
const PropsContext = createContext<CodeBlockProps | undefined>(undefined);
@@ -671,17 +671,10 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => {
? true
: false,
supportsArrays: config.provider === "pg" ? true : false,
customTransformOutput: ({ data, fieldAttributes }) => {
// not all providers support dates
// one such example case is https://github.com/better-auth/better-auth/issues/7819
if (fieldAttributes.type === "date") {
if (data === null || data === undefined) {
return data;
}
return new Date(data);
}
return data;
},
// not all providers support dates
// one such example case is https://github.com/better-auth/better-auth/issues/7819
// it's safe to set to `false` without checking for provider, because it it is dates the transformation doesn't apply anyway.
supportsDates: false,
transaction:
(config.transaction ?? false)
? (cb) =>
+2841 -555
View File
File diff suppressed because it is too large Load Diff