mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
docs: infrastructure docs (#8086)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
committed by
Bereket Engida
co-authored by
Alex Yang
parent
099c3df37d
commit
927fb0a769
@@ -44,4 +44,8 @@ dyld
|
||||
nsis
|
||||
ftyp
|
||||
heix
|
||||
whsec
|
||||
sessionMiddleware
|
||||
SIEM
|
||||
sess
|
||||
reactivations
|
||||
whsec
|
||||
|
||||
@@ -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
@@ -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:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: () => (
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
Generated
+2841
-555
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user