admin: Superforms + Zod validation, three-address-defaults schema, profiles toast fix
- Add Superforms + Zod 4 for all admin form validation - Replace address isDefault with three separate boolean defaults (Mailing, Billing, Shipping) - Update profiles page with three-checkbox address form and toast improvements - Convert all admin page server actions to pass request directly to crud - Export superFormErrorsToFieldErrors helper for profiles server actions - Regenerate Prisma client for new schema fields
This commit is contained in:
@@ -23,15 +23,18 @@ description: Configure Better Auth server and client, set up database adapters,
|
||||
## Quick Reference
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32`
|
||||
- `BETTER_AUTH_URL` - Base URL (e.g., `https://example.com`)
|
||||
|
||||
Only define `baseURL`/`secret` in config if env vars are NOT set.
|
||||
|
||||
### File Location
|
||||
|
||||
CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path.
|
||||
|
||||
### CLI Commands
|
||||
|
||||
- `npx @better-auth/cli@latest migrate` - Apply schema (built-in adapter)
|
||||
- `npx @better-auth/cli@latest generate` - Generate schema for Prisma/Drizzle
|
||||
- `npx @better-auth/cli mcp --cursor` - Add MCP to AI tools
|
||||
@@ -42,18 +45,18 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
|
||||
|
||||
## Core Config Options
|
||||
|
||||
| Option | Notes |
|
||||
|--------|-------|
|
||||
| `appName` | Optional display name |
|
||||
| `baseURL` | Only if `BETTER_AUTH_URL` not set |
|
||||
| `basePath` | Default `/api/auth`. Set `/` for root. |
|
||||
| `secret` | Only if `BETTER_AUTH_SECRET` not set |
|
||||
| `database` | Required for most features. See adapters docs. |
|
||||
| `secondaryStorage` | Redis/KV for sessions & rate limits |
|
||||
| `emailAndPassword` | `{ enabled: true }` to activate |
|
||||
| `socialProviders` | `{ google: { clientId, clientSecret }, ... }` |
|
||||
| `plugins` | Array of plugins |
|
||||
| `trustedOrigins` | CSRF whitelist |
|
||||
| Option | Notes |
|
||||
| ------------------ | ---------------------------------------------- |
|
||||
| `appName` | Optional display name |
|
||||
| `baseURL` | Only if `BETTER_AUTH_URL` not set |
|
||||
| `basePath` | Default `/api/auth`. Set `/` for root. |
|
||||
| `secret` | Only if `BETTER_AUTH_SECRET` not set |
|
||||
| `database` | Required for most features. See adapters docs. |
|
||||
| `secondaryStorage` | Redis/KV for sessions & rate limits |
|
||||
| `emailAndPassword` | `{ enabled: true }` to activate |
|
||||
| `socialProviders` | `{ google: { clientId, clientSecret }, ... }` |
|
||||
| `plugins` | Array of plugins |
|
||||
| `trustedOrigins` | CSRF whitelist |
|
||||
|
||||
---
|
||||
|
||||
@@ -70,11 +73,13 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
|
||||
## Session Management
|
||||
|
||||
**Storage priority:**
|
||||
|
||||
1. If `secondaryStorage` defined → sessions go there (not DB)
|
||||
2. Set `session.storeSessionInDatabase: true` to also persist to DB
|
||||
3. No database + `cookieCache` → fully stateless mode
|
||||
|
||||
**Cookie cache strategies:**
|
||||
|
||||
- `compact` (default) - Base64url + HMAC. Smallest.
|
||||
- `jwt` - Standard JWT. Readable but signed.
|
||||
- `jwe` - Encrypted. Maximum security.
|
||||
@@ -104,9 +109,10 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
|
||||
## Security
|
||||
|
||||
**In `advanced`:**
|
||||
|
||||
- `useSecureCookies` - Force HTTPS cookies
|
||||
- `disableCSRFCheck` - ⚠️ Security risk
|
||||
- `disableOriginCheck` - ⚠️ Security risk
|
||||
- `disableOriginCheck` - ⚠️ Security risk
|
||||
- `crossSubDomainCookies.enabled` - Share cookies across subdomains
|
||||
- `ipAddress.ipAddressHeaders` - Custom IP headers for proxies
|
||||
- `database.generateId` - Custom ID generation or `"serial"`/`"uuid"`/`false`
|
||||
@@ -128,9 +134,11 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
|
||||
## Plugins
|
||||
|
||||
**Import from dedicated paths for tree-shaking:**
|
||||
|
||||
```
|
||||
import { twoFactor } from "better-auth/plugins/two-factor"
|
||||
```
|
||||
|
||||
NOT `from "better-auth/plugins"`.
|
||||
|
||||
**Popular plugins:** `twoFactor`, `organization`, `passkey`, `magicLink`, `emailOtp`, `username`, `phoneNumber`, `admin`, `apiKey`, `bearer`, `jwt`, `multiSession`, `sso`, `oauthProvider`, `oidcProvider`, `openAPI`, `genericOAuth`.
|
||||
@@ -172,4 +180,4 @@ For separate client/server projects: `createAuthClient<typeof auth>()`.
|
||||
- [Options Reference](https://better-auth.com/docs/reference/options)
|
||||
- [LLMs.txt](https://better-auth.com/llms.txt)
|
||||
- [GitHub](https://github.com/better-auth/better-auth)
|
||||
- [Init Options Source](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts)
|
||||
- [Init Options Source](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts)
|
||||
|
||||
@@ -8,14 +8,15 @@ description: Configure rate limiting, manage auth secrets, set up CSRF protectio
|
||||
### Configuring the Secret
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
secret: process.env.BETTER_AUTH_SECRET, // or via `BETTER_AUTH_SECRET` env
|
||||
secret: process.env.BETTER_AUTH_SECRET // or via `BETTER_AUTH_SECRET` env
|
||||
});
|
||||
```
|
||||
|
||||
Better Auth looks for secrets in this order:
|
||||
|
||||
1. `options.secret` in your config
|
||||
2. `BETTER_AUTH_SECRET` environment variable
|
||||
3. `AUTH_SECRET` environment variable
|
||||
@@ -34,14 +35,14 @@ Enabled in production by default. Applies to all endpoints. Plugins can override
|
||||
### Default Configuration
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
rateLimit: {
|
||||
enabled: true, // Default: true in production
|
||||
window: 10, // Time window in seconds (default: 10)
|
||||
max: 100, // Max requests per window (default: 100)
|
||||
},
|
||||
rateLimit: {
|
||||
enabled: true, // Default: true in production
|
||||
window: 10, // Time window in seconds (default: 10)
|
||||
max: 100 // Max requests per window (default: 100)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -95,12 +96,12 @@ Multi-layer protection: origin header validation, Fetch Metadata checks, and fir
|
||||
### Configuration
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
advanced: {
|
||||
disableCSRFCheck: false, // Default: false (keep enabled)
|
||||
},
|
||||
advanced: {
|
||||
disableCSRFCheck: false // Default: false (keep enabled)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -111,14 +112,11 @@ Only disable for testing or with an alternative CSRF mechanism.
|
||||
### Configuring Trusted Origins
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
baseURL: "https://api.example.com",
|
||||
trustedOrigins: [
|
||||
"https://app.example.com",
|
||||
"https://admin.example.com",
|
||||
],
|
||||
baseURL: 'https://api.example.com',
|
||||
trustedOrigins: ['https://app.example.com', 'https://admin.example.com']
|
||||
});
|
||||
```
|
||||
|
||||
@@ -128,10 +126,10 @@ The `baseURL` origin is automatically trusted. Also configurable via env: `BETTE
|
||||
|
||||
```ts
|
||||
trustedOrigins: [
|
||||
"*.example.com", // Matches any subdomain
|
||||
"https://*.example.com", // Protocol-specific wildcard
|
||||
"exp://192.168.*.*:*/*", // Custom schemes (e.g., Expo)
|
||||
]
|
||||
'*.example.com', // Matches any subdomain
|
||||
'https://*.example.com', // Protocol-specific wildcard
|
||||
'exp://192.168.*.*:*/*' // Custom schemes (e.g., Expo)
|
||||
];
|
||||
```
|
||||
|
||||
### Dynamic Trusted Origins
|
||||
@@ -140,10 +138,10 @@ Compute trusted origins based on the request:
|
||||
|
||||
```ts
|
||||
trustedOrigins: async (request) => {
|
||||
// Validate against database, header, etc.
|
||||
const tenant = getTenantFromRequest(request);
|
||||
return [`https://${tenant}.myapp.com`];
|
||||
}
|
||||
// Validate against database, header, etc.
|
||||
const tenant = getTenantFromRequest(request);
|
||||
return [`https://${tenant}.myapp.com`];
|
||||
};
|
||||
```
|
||||
|
||||
Validates `callbackURL`, `redirectTo`, `errorCallbackURL`, `newUserCallbackURL`, and `origin` against trusted origins. Invalid URLs receive 403.
|
||||
@@ -153,13 +151,13 @@ Validates `callbackURL`, `redirectTo`, `errorCallbackURL`, `newUserCallbackURL`,
|
||||
### Session Expiration
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 7, // 7 days (default)
|
||||
updateAge: 60 * 60 * 24, // Refresh session every 24 hours (default)
|
||||
},
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 7, // 7 days (default)
|
||||
updateAge: 60 * 60 * 24 // Refresh session every 24 hours (default)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -186,17 +184,17 @@ Defaults: `secure: true` (HTTPS/production), `sameSite: "lax"`, `httpOnly: true`
|
||||
### Custom Cookie Configuration
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
advanced: {
|
||||
useSecureCookies: true, // Force secure cookies
|
||||
cookiePrefix: "myapp", // Custom prefix (default: "better-auth")
|
||||
defaultCookieAttributes: {
|
||||
sameSite: "strict", // Stricter CSRF protection
|
||||
path: "/auth", // Limit cookie scope
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
useSecureCookies: true, // Force secure cookies
|
||||
cookiePrefix: 'myapp', // Custom prefix (default: "better-auth")
|
||||
defaultCookieAttributes: {
|
||||
sameSite: 'strict', // Stricter CSRF protection
|
||||
path: '/auth' // Limit cookie scope
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -221,12 +219,12 @@ PKCE is automatic for all OAuth flows. State tokens are 32-char random strings e
|
||||
### State Parameter Storage
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
account: {
|
||||
storeStateStrategy: "cookie", // Options: "cookie" (default), "database"
|
||||
},
|
||||
account: {
|
||||
storeStateStrategy: 'cookie' // Options: "cookie" (default), "database"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -245,15 +243,15 @@ Enable if storing OAuth tokens for API access on behalf of users. Use `skipState
|
||||
### IP Address Configuration
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
advanced: {
|
||||
ipAddress: {
|
||||
ipAddressHeaders: ["x-forwarded-for", "x-real-ip"], // Headers to check
|
||||
disableIpTracking: false, // Keep enabled for rate limiting
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
ipAddress: {
|
||||
ipAddressHeaders: ['x-forwarded-for', 'x-real-ip'], // Headers to check
|
||||
disableIpTracking: false // Keep enabled for rate limiting
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -262,50 +260,50 @@ Set `ipv6Subnet` (128, 64, 48, 32; default 64) to group IPv6 addresses. Enable `
|
||||
## Database Hooks for Security Auditing
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
databaseHooks: {
|
||||
session: {
|
||||
create: {
|
||||
after: async ({ data, ctx }) => {
|
||||
await auditLog("session.created", {
|
||||
userId: data.userId,
|
||||
ip: ctx?.request?.headers.get("x-forwarded-for"),
|
||||
userAgent: ctx?.request?.headers.get("user-agent"),
|
||||
});
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
before: async ({ data }) => {
|
||||
await auditLog("session.revoked", { sessionId: data.id });
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
update: {
|
||||
after: async ({ data, oldData }) => {
|
||||
if (oldData?.email !== data.email) {
|
||||
await auditLog("user.email_changed", {
|
||||
userId: data.id,
|
||||
oldEmail: oldData?.email,
|
||||
newEmail: data.email,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
account: {
|
||||
create: {
|
||||
after: async ({ data }) => {
|
||||
await auditLog("account.linked", {
|
||||
userId: data.userId,
|
||||
provider: data.providerId,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
databaseHooks: {
|
||||
session: {
|
||||
create: {
|
||||
after: async ({ data, ctx }) => {
|
||||
await auditLog('session.created', {
|
||||
userId: data.userId,
|
||||
ip: ctx?.request?.headers.get('x-forwarded-for'),
|
||||
userAgent: ctx?.request?.headers.get('user-agent')
|
||||
});
|
||||
}
|
||||
},
|
||||
delete: {
|
||||
before: async ({ data }) => {
|
||||
await auditLog('session.revoked', { sessionId: data.id });
|
||||
}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
update: {
|
||||
after: async ({ data, oldData }) => {
|
||||
if (oldData?.email !== data.email) {
|
||||
await auditLog('user.email_changed', {
|
||||
userId: data.id,
|
||||
oldEmail: oldData?.email,
|
||||
newEmail: data.email
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
account: {
|
||||
create: {
|
||||
after: async ({ data }) => {
|
||||
await auditLog('account.linked', {
|
||||
userId: data.userId,
|
||||
provider: data.providerId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -314,19 +312,19 @@ Return `false` from a `before` hook to prevent an operation.
|
||||
## Background Tasks
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
advanced: {
|
||||
backgroundTasks: {
|
||||
handler: (promise) => {
|
||||
// Platform-specific handler
|
||||
// Vercel: waitUntil(promise)
|
||||
// Cloudflare: ctx.waitUntil(promise)
|
||||
waitUntil(promise);
|
||||
},
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
backgroundTasks: {
|
||||
handler: (promise) => {
|
||||
// Platform-specific handler
|
||||
// Vercel: waitUntil(promise)
|
||||
// Cloudflare: ctx.waitUntil(promise)
|
||||
waitUntil(promise);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -339,80 +337,76 @@ Built-in: consistent response messages, dummy operations on invalid requests, ba
|
||||
## Complete Security Configuration Example
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
secret: process.env.BETTER_AUTH_SECRET,
|
||||
baseURL: "https://api.example.com",
|
||||
trustedOrigins: [
|
||||
"https://app.example.com",
|
||||
"https://*.preview.example.com",
|
||||
],
|
||||
|
||||
// Rate limiting
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
storage: "secondary-storage",
|
||||
customRules: {
|
||||
"/api/auth/sign-in/email": { window: 60, max: 5 },
|
||||
"/api/auth/sign-up/email": { window: 60, max: 3 },
|
||||
},
|
||||
},
|
||||
|
||||
// Session security
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
||||
updateAge: 60 * 60 * 24, // 24 hours
|
||||
freshAge: 60 * 60, // 1 hour for sensitive actions
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 300,
|
||||
strategy: "jwe", // Encrypted session data
|
||||
},
|
||||
},
|
||||
|
||||
// OAuth security
|
||||
account: {
|
||||
encryptOAuthTokens: true,
|
||||
storeStateStrategy: "cookie",
|
||||
},
|
||||
|
||||
|
||||
// Advanced settings
|
||||
advanced: {
|
||||
useSecureCookies: true,
|
||||
cookiePrefix: "myapp",
|
||||
defaultCookieAttributes: {
|
||||
sameSite: "lax",
|
||||
},
|
||||
ipAddress: {
|
||||
ipAddressHeaders: ["x-forwarded-for"],
|
||||
ipv6Subnet: 64,
|
||||
},
|
||||
backgroundTasks: {
|
||||
handler: (promise) => waitUntil(promise),
|
||||
},
|
||||
},
|
||||
|
||||
// Security auditing
|
||||
databaseHooks: {
|
||||
session: {
|
||||
create: {
|
||||
after: async ({ data, ctx }) => {
|
||||
console.log(`New session for user ${data.userId}`);
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
update: {
|
||||
after: async ({ data, oldData }) => {
|
||||
if (oldData?.email !== data.email) {
|
||||
console.log(`Email changed for user ${data.id}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
secret: process.env.BETTER_AUTH_SECRET,
|
||||
baseURL: 'https://api.example.com',
|
||||
trustedOrigins: ['https://app.example.com', 'https://*.preview.example.com'],
|
||||
|
||||
// Rate limiting
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
storage: 'secondary-storage',
|
||||
customRules: {
|
||||
'/api/auth/sign-in/email': { window: 60, max: 5 },
|
||||
'/api/auth/sign-up/email': { window: 60, max: 3 }
|
||||
}
|
||||
},
|
||||
|
||||
// Session security
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
||||
updateAge: 60 * 60 * 24, // 24 hours
|
||||
freshAge: 60 * 60, // 1 hour for sensitive actions
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 300,
|
||||
strategy: 'jwe' // Encrypted session data
|
||||
}
|
||||
},
|
||||
|
||||
// OAuth security
|
||||
account: {
|
||||
encryptOAuthTokens: true,
|
||||
storeStateStrategy: 'cookie'
|
||||
},
|
||||
|
||||
// Advanced settings
|
||||
advanced: {
|
||||
useSecureCookies: true,
|
||||
cookiePrefix: 'myapp',
|
||||
defaultCookieAttributes: {
|
||||
sameSite: 'lax'
|
||||
},
|
||||
ipAddress: {
|
||||
ipAddressHeaders: ['x-forwarded-for'],
|
||||
ipv6Subnet: 64
|
||||
},
|
||||
backgroundTasks: {
|
||||
handler: (promise) => waitUntil(promise)
|
||||
}
|
||||
},
|
||||
|
||||
// Security auditing
|
||||
databaseHooks: {
|
||||
session: {
|
||||
create: {
|
||||
after: async ({ data, ctx }) => {
|
||||
console.log(`New session for user ${data.userId}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
update: {
|
||||
after: async ({ data, oldData }) => {
|
||||
if (oldData?.email !== data.email) {
|
||||
console.log(`Email changed for user ${data.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ Before writing any code, gather requirements by scanning the project and asking
|
||||
### Step 1: Scan the project
|
||||
|
||||
Analyze the codebase to auto-detect:
|
||||
|
||||
- **Framework** — Look for `next.config`, `svelte.config`, `nuxt.config`, `astro.config`, `vite.config`, or Express/Hono entry files.
|
||||
- **Database/ORM** — Look for `prisma/schema.prisma`, `drizzle.config`, `package.json` deps (`pg`, `mysql2`, `better-sqlite3`, `mongoose`, `mongodb`).
|
||||
- **Existing auth** — Look for existing auth libraries (`next-auth`, `lucia`, `clerk`, `supabase/auth`, `firebase/auth`) in `package.json` or imports.
|
||||
@@ -75,8 +76,9 @@ Use the `AskQuestion` tool to ask the user **all applicable questions in a singl
|
||||
- `allow_multiple: true`
|
||||
|
||||
10. **Auth UI style** (always ask)
|
||||
- Prompt: "What style do you want for the auth pages? Pick one or describe your own."
|
||||
- Options: Minimal & clean | Centered card with background | Split layout (form + hero image) | Floating / glassmorphism | Other (I'll describe)
|
||||
|
||||
- Prompt: "What style do you want for the auth pages? Pick one or describe your own."
|
||||
- Options: Minimal & clean | Centered card with background | Split layout (form + hero image) | Floating / glassmorphism | Other (I'll describe)
|
||||
|
||||
### Step 3: Summarize the plan
|
||||
|
||||
@@ -180,15 +182,18 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
**Location:** `lib/auth.ts` or `src/lib/auth.ts`
|
||||
|
||||
**Minimal config needs:**
|
||||
|
||||
- `database` - Connection or adapter
|
||||
- `emailAndPassword: { enabled: true }` - For email/password auth
|
||||
|
||||
**Standard config adds:**
|
||||
|
||||
- `socialProviders` - OAuth providers (google, github, etc.)
|
||||
- `emailVerification.sendVerificationEmail` - Email verification handler
|
||||
- `emailAndPassword.sendResetPassword` - Password reset handler
|
||||
|
||||
**Full config adds:**
|
||||
|
||||
- `plugins` - Array of feature plugins
|
||||
- `session` - Expiry, cookie cache settings
|
||||
- `account.accountLinking` - Multi-provider linking
|
||||
@@ -217,14 +222,14 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
|
||||
## Route Handler Setup
|
||||
|
||||
| Framework | File | Handler |
|
||||
|-----------|------|---------|
|
||||
| Framework | File | Handler |
|
||||
| ------------------ | -------------------------------- | ------------------------------------------------ |
|
||||
| Next.js App Router | `app/api/auth/[...all]/route.ts` | `toNextJsHandler(auth)` → export `{ GET, POST }` |
|
||||
| Next.js Pages | `pages/api/auth/[...all].ts` | `toNextJsHandler(auth)` → default export |
|
||||
| Express | Any file | `app.all("/api/auth/*", toNodeHandler(auth))` |
|
||||
| SvelteKit | `src/hooks.server.ts` | `svelteKitHandler(auth)` |
|
||||
| SolidStart | Route file | `solidStartHandler(auth)` |
|
||||
| Hono | Route file | `auth.handler(c.req.raw)` |
|
||||
| Next.js Pages | `pages/api/auth/[...all].ts` | `toNextJsHandler(auth)` → default export |
|
||||
| Express | Any file | `app.all("/api/auth/*", toNodeHandler(auth))` |
|
||||
| SvelteKit | `src/hooks.server.ts` | `svelteKitHandler(auth)` |
|
||||
| SolidStart | Route file | `solidStartHandler(auth)` |
|
||||
| Hono | Route file | `auth.handler(c.req.raw)` |
|
||||
|
||||
**Next.js Server Components:** Add `nextCookies()` plugin to auth config.
|
||||
|
||||
@@ -232,11 +237,11 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
|
||||
## Database Migrations
|
||||
|
||||
| Adapter | Command |
|
||||
|---------|---------|
|
||||
| Built-in Kysely | `npx @better-auth/cli@latest migrate` (applies directly) |
|
||||
| Prisma | `npx @better-auth/cli@latest generate --output prisma/schema.prisma` then `npx prisma migrate dev` |
|
||||
| Drizzle | `npx @better-auth/cli@latest generate --output src/db/auth-schema.ts` then `npx drizzle-kit push` |
|
||||
| Adapter | Command |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| Built-in Kysely | `npx @better-auth/cli@latest migrate` (applies directly) |
|
||||
| Prisma | `npx @better-auth/cli@latest generate --output prisma/schema.prisma` then `npx prisma migrate dev` |
|
||||
| Drizzle | `npx @better-auth/cli@latest generate --output src/db/auth-schema.ts` then `npx drizzle-kit push` |
|
||||
|
||||
**Re-run after adding plugins.**
|
||||
|
||||
@@ -244,28 +249,28 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
|
||||
## Database Adapters
|
||||
|
||||
| Database | Setup |
|
||||
|----------|-------|
|
||||
| SQLite | Pass `better-sqlite3` or `bun:sqlite` instance directly |
|
||||
| PostgreSQL | Pass `pg.Pool` instance directly |
|
||||
| MySQL | Pass `mysql2` pool directly |
|
||||
| Prisma | `prismaAdapter(prisma, { provider: "postgresql" })` from `better-auth/adapters/prisma` |
|
||||
| Drizzle | `drizzleAdapter(db, { provider: "pg" })` from `better-auth/adapters/drizzle` |
|
||||
| MongoDB | `mongodbAdapter(db)` from `better-auth/adapters/mongodb` |
|
||||
| Database | Setup |
|
||||
| ---------- | -------------------------------------------------------------------------------------- |
|
||||
| SQLite | Pass `better-sqlite3` or `bun:sqlite` instance directly |
|
||||
| PostgreSQL | Pass `pg.Pool` instance directly |
|
||||
| MySQL | Pass `mysql2` pool directly |
|
||||
| Prisma | `prismaAdapter(prisma, { provider: "postgresql" })` from `better-auth/adapters/prisma` |
|
||||
| Drizzle | `drizzleAdapter(db, { provider: "pg" })` from `better-auth/adapters/drizzle` |
|
||||
| MongoDB | `mongodbAdapter(db)` from `better-auth/adapters/mongodb` |
|
||||
|
||||
---
|
||||
|
||||
## Common Plugins
|
||||
|
||||
| Plugin | Server Import | Client Import | Purpose |
|
||||
|--------|---------------|---------------|---------|
|
||||
| `twoFactor` | `better-auth/plugins` | `twoFactorClient` | 2FA with TOTP/OTP |
|
||||
| `organization` | `better-auth/plugins` | `organizationClient` | Teams/orgs |
|
||||
| `admin` | `better-auth/plugins` | `adminClient` | User management |
|
||||
| `bearer` | `better-auth/plugins` | - | API token auth |
|
||||
| `openAPI` | `better-auth/plugins` | - | API docs |
|
||||
| `passkey` | `@better-auth/passkey` | `passkeyClient` | WebAuthn |
|
||||
| `sso` | `@better-auth/sso` | - | Enterprise SSO |
|
||||
| Plugin | Server Import | Client Import | Purpose |
|
||||
| -------------- | ---------------------- | -------------------- | ----------------- |
|
||||
| `twoFactor` | `better-auth/plugins` | `twoFactorClient` | 2FA with TOTP/OTP |
|
||||
| `organization` | `better-auth/plugins` | `organizationClient` | Teams/orgs |
|
||||
| `admin` | `better-auth/plugins` | `adminClient` | User management |
|
||||
| `bearer` | `better-auth/plugins` | - | API token auth |
|
||||
| `openAPI` | `better-auth/plugins` | - | API docs |
|
||||
| `passkey` | `@better-auth/passkey` | `passkeyClient` | WebAuthn |
|
||||
| `sso` | `@better-auth/sso` | - | Enterprise SSO |
|
||||
|
||||
**Plugin pattern:** Server plugin + client plugin + run migrations.
|
||||
|
||||
@@ -274,6 +279,7 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
## Auth UI Implementation
|
||||
|
||||
**Sign in flow:**
|
||||
|
||||
1. `signIn.email({ email, password })` or `signIn.social({ provider, callbackURL })`
|
||||
2. Handle `error` in response
|
||||
3. Redirect on success
|
||||
@@ -302,13 +308,13 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| "Secret not set" | Add `BETTER_AUTH_SECRET` env var |
|
||||
| "Invalid Origin" | Add domain to `trustedOrigins` |
|
||||
| Cookies not setting | Check `baseURL` matches domain; enable secure cookies in prod |
|
||||
| OAuth callback errors | Verify redirect URIs in provider dashboard |
|
||||
| Type errors after adding plugin | Re-run CLI generate/migrate |
|
||||
| Issue | Fix |
|
||||
| ------------------------------- | ------------------------------------------------------------- |
|
||||
| "Secret not set" | Add `BETTER_AUTH_SECRET` env var |
|
||||
| "Invalid Origin" | Add domain to `trustedOrigins` |
|
||||
| Cookies not setting | Check `baseURL` matches domain; enable secure cookies in prod |
|
||||
| OAuth callback errors | Verify redirect URIs in provider dashboard |
|
||||
| Type errors after adding plugin | Re-run CLI generate/migrate |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -18,19 +18,19 @@ description: Configure email verification, implement password reset flows, set p
|
||||
Configure `emailVerification.sendVerificationEmail` to verify user email addresses.
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { sendEmail } from "./email"; // your email sending function
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { sendEmail } from './email'; // your email sending function
|
||||
|
||||
export const auth = betterAuth({
|
||||
emailVerification: {
|
||||
sendVerificationEmail: async ({ user, url, token }, request) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "Verify your email address",
|
||||
text: `Click the link to verify your email: ${url}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
emailVerification: {
|
||||
sendVerificationEmail: async ({ user, url, token }, request) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: 'Verify your email address',
|
||||
text: `Click the link to verify your email: ${url}`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -42,9 +42,9 @@ For stricter security, enable `emailAndPassword.requireEmailVerification` to blo
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
requireEmailVerification: true,
|
||||
},
|
||||
emailAndPassword: {
|
||||
requireEmailVerification: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -60,7 +60,7 @@ Always use absolute URLs (including the origin) for callback URLs in sign-up and
|
||||
|
||||
```ts
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
callbackURL: "https://example.com/callback", // absolute URL with origin
|
||||
callbackURL: 'https://example.com/callback' // absolute URL with origin
|
||||
});
|
||||
```
|
||||
|
||||
@@ -69,26 +69,26 @@ const { data, error } = await authClient.signUp.email({
|
||||
Provide `sendResetPassword` in the email and password config to enable password resets.
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { sendEmail } from "./email"; // your email sending function
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { sendEmail } from './email'; // your email sending function
|
||||
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
// Custom email sending function to send reset-password email
|
||||
sendResetPassword: async ({ user, url, token }, request) => {
|
||||
void sendEmail({
|
||||
to: user.email,
|
||||
subject: "Reset your password",
|
||||
text: `Click the link to reset your password: ${url}`,
|
||||
});
|
||||
},
|
||||
// Optional event hook
|
||||
onPasswordReset: async ({ user }, request) => {
|
||||
// your logic here
|
||||
console.log(`Password for user ${user.email} has been reset.`);
|
||||
},
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
// Custom email sending function to send reset-password email
|
||||
sendResetPassword: async ({ user, url, token }, request) => {
|
||||
void sendEmail({
|
||||
to: user.email,
|
||||
subject: 'Reset your password',
|
||||
text: `Click the link to reset your password: ${url}`
|
||||
});
|
||||
},
|
||||
// Optional event hook
|
||||
onPasswordReset: async ({ user }, request) => {
|
||||
// your logic here
|
||||
console.log(`Password for user ${user.email} has been reset.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -100,14 +100,14 @@ On serverless platforms, configure a background task handler:
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
advanced: {
|
||||
backgroundTasks: {
|
||||
handler: (promise) => {
|
||||
// Use platform-specific methods like waitUntil
|
||||
waitUntil(promise);
|
||||
},
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
backgroundTasks: {
|
||||
handler: (promise) => {
|
||||
// Use platform-specific methods like waitUntil
|
||||
waitUntil(promise);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -117,10 +117,10 @@ Tokens expire after 1 hour by default. Configure with `resetPasswordTokenExpires
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
resetPasswordTokenExpiresIn: 60 * 30, // 30 minutes
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
resetPasswordTokenExpiresIn: 60 * 30 // 30 minutes
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -132,10 +132,10 @@ Enable `revokeSessionsOnPasswordReset` to invalidate all existing sessions on pa
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
revokeSessionsOnPasswordReset: true,
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
revokeSessionsOnPasswordReset: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -145,11 +145,11 @@ Password length limits (configurable):
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
minPasswordLength: 12,
|
||||
maxPasswordLength: 256,
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
minPasswordLength: 12,
|
||||
maxPasswordLength: 256
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -159,10 +159,10 @@ Call `requestPasswordReset` to send the reset link. Triggers the `sendResetPassw
|
||||
|
||||
```ts
|
||||
const data = await auth.api.requestPasswordReset({
|
||||
body: {
|
||||
email: "john.doe@example.com", // required
|
||||
redirectTo: "https://example.com/reset-password",
|
||||
},
|
||||
body: {
|
||||
email: 'john.doe@example.com', // required
|
||||
redirectTo: 'https://example.com/reset-password'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -170,8 +170,8 @@ Or authClient:
|
||||
|
||||
```ts
|
||||
const { data, error } = await authClient.requestPasswordReset({
|
||||
email: "john.doe@example.com", // required
|
||||
redirectTo: "https://example.com/reset-password",
|
||||
email: 'john.doe@example.com', // required
|
||||
redirectTo: 'https://example.com/reset-password'
|
||||
});
|
||||
```
|
||||
|
||||
@@ -186,26 +186,25 @@ Default: `scrypt` (Node.js native, no external dependencies).
|
||||
To use Argon2id or another algorithm, provide custom `hash` and `verify` functions:
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { hash, verify, type Options } from "@node-rs/argon2";
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { hash, verify, type Options } from '@node-rs/argon2';
|
||||
|
||||
const argon2Options: Options = {
|
||||
memoryCost: 65536, // 64 MiB
|
||||
timeCost: 3, // 3 iterations
|
||||
parallelism: 4, // 4 parallel lanes
|
||||
outputLen: 32, // 32 byte output
|
||||
algorithm: 2, // Argon2id variant
|
||||
memoryCost: 65536, // 64 MiB
|
||||
timeCost: 3, // 3 iterations
|
||||
parallelism: 4, // 4 parallel lanes
|
||||
outputLen: 32, // 32 byte output
|
||||
algorithm: 2 // Argon2id variant
|
||||
};
|
||||
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
password: {
|
||||
hash: (password) => hash(password, argon2Options),
|
||||
verify: ({ password, hash: storedHash }) =>
|
||||
verify(storedHash, password, argon2Options),
|
||||
},
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
password: {
|
||||
hash: (password) => hash(password, argon2Options),
|
||||
verify: ({ password, hash: storedHash }) => verify(storedHash, password, argon2Options)
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -11,28 +11,28 @@ description: Configure multi-tenant organizations, manage members and invitation
|
||||
4. Verify: check that organization, member, invitation tables exist in your database
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { organization } from "better-auth/plugins";
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { organization } from 'better-auth/plugins';
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
organization({
|
||||
allowUserToCreateOrganization: true,
|
||||
organizationLimit: 5, // Max orgs per user
|
||||
membershipLimit: 100, // Max members per org
|
||||
}),
|
||||
],
|
||||
plugins: [
|
||||
organization({
|
||||
allowUserToCreateOrganization: true,
|
||||
organizationLimit: 5, // Max orgs per user
|
||||
membershipLimit: 100 // Max members per org
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Client-Side Setup
|
||||
|
||||
```ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { organizationClient } from "better-auth/client/plugins";
|
||||
import { createAuthClient } from 'better-auth/client';
|
||||
import { organizationClient } from 'better-auth/client/plugins';
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [organizationClient()],
|
||||
plugins: [organizationClient()]
|
||||
});
|
||||
```
|
||||
|
||||
@@ -42,12 +42,12 @@ The creator is automatically assigned the `owner` role.
|
||||
|
||||
```ts
|
||||
const createOrg = async () => {
|
||||
const { data, error } = await authClient.organization.create({
|
||||
name: "My Company",
|
||||
slug: "my-company",
|
||||
logo: "https://example.com/logo.png",
|
||||
metadata: { plan: "pro" },
|
||||
});
|
||||
const { data, error } = await authClient.organization.create({
|
||||
name: 'My Company',
|
||||
slug: 'my-company',
|
||||
logo: 'https://example.com/logo.png',
|
||||
metadata: { plan: 'pro' }
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
@@ -57,13 +57,13 @@ Restrict who can create organizations based on user attributes:
|
||||
|
||||
```ts
|
||||
organization({
|
||||
allowUserToCreateOrganization: async (user) => {
|
||||
return user.emailVerified === true;
|
||||
},
|
||||
organizationLimit: async (user) => {
|
||||
// Premium users get more organizations
|
||||
return user.plan === "premium" ? 20 : 3;
|
||||
},
|
||||
allowUserToCreateOrganization: async (user) => {
|
||||
return user.emailVerified === true;
|
||||
},
|
||||
organizationLimit: async (user) => {
|
||||
// Premium users get more organizations
|
||||
return user.plan === 'premium' ? 20 : 3;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -73,26 +73,25 @@ Administrators can create organizations for other users (server-side only):
|
||||
|
||||
```ts
|
||||
await auth.api.createOrganization({
|
||||
body: {
|
||||
name: "Client Organization",
|
||||
slug: "client-org",
|
||||
userId: "user-id-who-will-be-owner", // `userId` is required
|
||||
},
|
||||
body: {
|
||||
name: 'Client Organization',
|
||||
slug: 'client-org',
|
||||
userId: 'user-id-who-will-be-owner' // `userId` is required
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Note**: The `userId` parameter cannot be used alongside session headers.
|
||||
|
||||
|
||||
## Active Organizations
|
||||
|
||||
Stored in the session and scopes subsequent API calls. Set after user selects one.
|
||||
|
||||
```ts
|
||||
const setActive = async (organizationId: string) => {
|
||||
const { data, error } = await authClient.organization.setActive({
|
||||
organizationId,
|
||||
});
|
||||
const { data, error } = await authClient.organization.setActive({
|
||||
organizationId
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
@@ -106,11 +105,11 @@ Use `getFullOrganization()` to retrieve the active org with all members, invitat
|
||||
|
||||
```ts
|
||||
await auth.api.addMember({
|
||||
body: {
|
||||
userId: "user-id",
|
||||
role: "member",
|
||||
organizationId: "org-id",
|
||||
},
|
||||
body: {
|
||||
userId: 'user-id',
|
||||
role: 'member',
|
||||
organizationId: 'org-id'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -120,11 +119,11 @@ For client-side member additions, use the invitation system instead.
|
||||
|
||||
```ts
|
||||
await auth.api.addMember({
|
||||
body: {
|
||||
userId: "user-id",
|
||||
role: ["admin", "moderator"],
|
||||
organizationId: "org-id",
|
||||
},
|
||||
body: {
|
||||
userId: 'user-id',
|
||||
role: ['admin', 'moderator'],
|
||||
organizationId: 'org-id'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -140,12 +139,12 @@ Use `updateMemberRole({ memberId, role })`.
|
||||
|
||||
```ts
|
||||
organization({
|
||||
membershipLimit: async (user, organization) => {
|
||||
if (organization.metadata?.plan === "enterprise") {
|
||||
return 1000;
|
||||
}
|
||||
return 50;
|
||||
},
|
||||
membershipLimit: async (user, organization) => {
|
||||
if (organization.metadata?.plan === 'enterprise') {
|
||||
return 1000;
|
||||
}
|
||||
return 50;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -154,29 +153,29 @@ organization({
|
||||
### Setting Up Invitation Emails
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { organization } from "better-auth/plugins";
|
||||
import { sendEmail } from "./email";
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { organization } from 'better-auth/plugins';
|
||||
import { sendEmail } from './email';
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
organization({
|
||||
sendInvitationEmail: async (data) => {
|
||||
const { email, organization, inviter, invitation } = data;
|
||||
plugins: [
|
||||
organization({
|
||||
sendInvitationEmail: async (data) => {
|
||||
const { email, organization, inviter, invitation } = data;
|
||||
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: `Join ${organization.name}`,
|
||||
html: `
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: `Join ${organization.name}`,
|
||||
html: `
|
||||
<p>${inviter.user.name} invited you to join ${organization.name}</p>
|
||||
<a href="https://yourapp.com/accept-invite?id=${invitation.id}">
|
||||
Accept Invitation
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
`
|
||||
});
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
@@ -184,8 +183,8 @@ export const auth = betterAuth({
|
||||
|
||||
```ts
|
||||
await authClient.organization.inviteMember({
|
||||
email: "newuser@example.com",
|
||||
role: "member",
|
||||
email: 'newuser@example.com',
|
||||
role: 'member'
|
||||
});
|
||||
```
|
||||
|
||||
@@ -193,9 +192,9 @@ await authClient.organization.inviteMember({
|
||||
|
||||
```ts
|
||||
const { data } = await authClient.organization.getInvitationURL({
|
||||
email: "newuser@example.com",
|
||||
role: "member",
|
||||
callbackURL: "https://yourapp.com/dashboard",
|
||||
email: 'newuser@example.com',
|
||||
role: 'member',
|
||||
callbackURL: 'https://yourapp.com/dashboard'
|
||||
});
|
||||
|
||||
// Share data.url via any channel
|
||||
@@ -207,9 +206,9 @@ This endpoint does not call `sendInvitationEmail` — handle delivery yourself.
|
||||
|
||||
```ts
|
||||
organization({
|
||||
invitationExpiresIn: 60 * 60 * 24 * 7, // 7 days (default: 48 hours)
|
||||
invitationLimit: 100, // Max pending invitations per org
|
||||
cancelPendingInvitationsOnReInvite: true, // Cancel old invites when re-inviting
|
||||
invitationExpiresIn: 60 * 60 * 24 * 7, // 7 days (default: 48 hours)
|
||||
invitationLimit: 100, // Max pending invitations per org
|
||||
cancelPendingInvitationsOnReInvite: true // Cancel old invites when re-inviting
|
||||
});
|
||||
```
|
||||
|
||||
@@ -221,11 +220,11 @@ Default roles: `owner` (full access), `admin` (manage members/invitations/settin
|
||||
|
||||
```ts
|
||||
const { data } = await authClient.organization.hasPermission({
|
||||
permission: "member:write",
|
||||
permission: 'member:write'
|
||||
});
|
||||
|
||||
if (data?.hasPermission) {
|
||||
// User can manage members
|
||||
// User can manage members
|
||||
}
|
||||
```
|
||||
|
||||
@@ -236,16 +235,16 @@ Use `checkRolePermission({ role, permissions })` for client-side UI rendering (s
|
||||
### Enabling Teams
|
||||
|
||||
```ts
|
||||
import { organization } from "better-auth/plugins";
|
||||
import { organization } from 'better-auth/plugins';
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
organization({
|
||||
teams: {
|
||||
enabled: true
|
||||
}
|
||||
}),
|
||||
],
|
||||
plugins: [
|
||||
organization({
|
||||
teams: {
|
||||
enabled: true
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
@@ -253,7 +252,7 @@ export const auth = betterAuth({
|
||||
|
||||
```ts
|
||||
const { data } = await authClient.organization.createTeam({
|
||||
name: "Engineering",
|
||||
name: 'Engineering'
|
||||
});
|
||||
```
|
||||
|
||||
@@ -267,11 +266,11 @@ Set active team with `setActiveTeam({ teamId })`.
|
||||
|
||||
```ts
|
||||
organization({
|
||||
teams: {
|
||||
maximumTeams: 20, // Max teams per org
|
||||
maximumMembersPerTeam: 50, // Max members per team
|
||||
allowRemovingAllTeams: false, // Prevent removing last team
|
||||
}
|
||||
teams: {
|
||||
maximumTeams: 20, // Max teams per org
|
||||
maximumMembersPerTeam: 50, // Max members per team
|
||||
allowRemovingAllTeams: false // Prevent removing last team
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -280,17 +279,17 @@ organization({
|
||||
### Enabling Dynamic Access Control
|
||||
|
||||
```ts
|
||||
import { organization } from "better-auth/plugins";
|
||||
import { dynamicAccessControl } from "@better-auth/organization/addons";
|
||||
import { organization } from 'better-auth/plugins';
|
||||
import { dynamicAccessControl } from '@better-auth/organization/addons';
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
organization({
|
||||
dynamicAccessControl: {
|
||||
enabled: true
|
||||
}
|
||||
}),
|
||||
],
|
||||
plugins: [
|
||||
organization({
|
||||
dynamicAccessControl: {
|
||||
enabled: true
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
@@ -298,11 +297,11 @@ export const auth = betterAuth({
|
||||
|
||||
```ts
|
||||
await authClient.organization.createRole({
|
||||
role: "moderator",
|
||||
permission: {
|
||||
member: ["read"],
|
||||
invitation: ["read"],
|
||||
},
|
||||
role: 'moderator',
|
||||
permission: {
|
||||
member: ['read'],
|
||||
invitation: ['read']
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -314,37 +313,37 @@ Execute custom logic at various points in the organization lifecycle:
|
||||
|
||||
```ts
|
||||
organization({
|
||||
hooks: {
|
||||
organization: {
|
||||
beforeCreate: async ({ data, user }) => {
|
||||
// Validate or modify data before creation
|
||||
return {
|
||||
data: {
|
||||
...data,
|
||||
metadata: { ...data.metadata, createdBy: user.id },
|
||||
},
|
||||
};
|
||||
},
|
||||
afterCreate: async ({ organization, member }) => {
|
||||
// Post-creation logic (e.g., send welcome email, create default resources)
|
||||
await createDefaultResources(organization.id);
|
||||
},
|
||||
beforeDelete: async ({ organization }) => {
|
||||
// Cleanup before deletion
|
||||
await archiveOrganizationData(organization.id);
|
||||
},
|
||||
},
|
||||
member: {
|
||||
afterCreate: async ({ member, organization }) => {
|
||||
await notifyAdmins(organization.id, `New member joined`);
|
||||
},
|
||||
},
|
||||
invitation: {
|
||||
afterCreate: async ({ invitation, organization, inviter }) => {
|
||||
await logInvitation(invitation);
|
||||
},
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
organization: {
|
||||
beforeCreate: async ({ data, user }) => {
|
||||
// Validate or modify data before creation
|
||||
return {
|
||||
data: {
|
||||
...data,
|
||||
metadata: { ...data.metadata, createdBy: user.id }
|
||||
}
|
||||
};
|
||||
},
|
||||
afterCreate: async ({ organization, member }) => {
|
||||
// Post-creation logic (e.g., send welcome email, create default resources)
|
||||
await createDefaultResources(organization.id);
|
||||
},
|
||||
beforeDelete: async ({ organization }) => {
|
||||
// Cleanup before deletion
|
||||
await archiveOrganizationData(organization.id);
|
||||
}
|
||||
},
|
||||
member: {
|
||||
afterCreate: async ({ member, organization }) => {
|
||||
await notifyAdmins(organization.id, `New member joined`);
|
||||
}
|
||||
},
|
||||
invitation: {
|
||||
afterCreate: async ({ invitation, organization, inviter }) => {
|
||||
await logInvitation(invitation);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -354,32 +353,32 @@ Customize table names, field names, and add additional fields:
|
||||
|
||||
```ts
|
||||
organization({
|
||||
schema: {
|
||||
organization: {
|
||||
modelName: "workspace", // Rename table
|
||||
fields: {
|
||||
name: "workspaceName", // Rename fields
|
||||
},
|
||||
additionalFields: {
|
||||
billingId: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
member: {
|
||||
additionalFields: {
|
||||
department: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
organization: {
|
||||
modelName: 'workspace', // Rename table
|
||||
fields: {
|
||||
name: 'workspaceName' // Rename fields
|
||||
},
|
||||
additionalFields: {
|
||||
billingId: {
|
||||
type: 'string',
|
||||
required: false
|
||||
}
|
||||
}
|
||||
},
|
||||
member: {
|
||||
additionalFields: {
|
||||
department: {
|
||||
type: 'string',
|
||||
required: false
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -396,8 +395,8 @@ Always ensure ownership transfer before removing the current owner:
|
||||
```ts
|
||||
// Transfer ownership first
|
||||
await authClient.organization.updateMemberRole({
|
||||
memberId: "new-owner-member-id",
|
||||
role: "owner",
|
||||
memberId: 'new-owner-member-id',
|
||||
role: 'owner'
|
||||
});
|
||||
|
||||
// Then the previous owner can be demoted or removed
|
||||
@@ -409,7 +408,7 @@ Deleting an organization removes all associated data (members, invitations, team
|
||||
|
||||
```ts
|
||||
organization({
|
||||
disableOrganizationDeletion: true, // Disable via config
|
||||
disableOrganizationDeletion: true // Disable via config
|
||||
});
|
||||
```
|
||||
|
||||
@@ -417,15 +416,15 @@ Or implement soft delete via hooks:
|
||||
|
||||
```ts
|
||||
organization({
|
||||
hooks: {
|
||||
organization: {
|
||||
beforeDelete: async ({ organization }) => {
|
||||
// Archive instead of delete
|
||||
await archiveOrganization(organization.id);
|
||||
throw new Error("Organization archived, not deleted");
|
||||
},
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
organization: {
|
||||
beforeDelete: async ({ organization }) => {
|
||||
// Archive instead of delete
|
||||
await archiveOrganization(organization.id);
|
||||
throw new Error('Organization archived, not deleted');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -438,42 +437,42 @@ organization({
|
||||
## Complete Configuration Example
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { organization } from "better-auth/plugins";
|
||||
import { sendEmail } from "./email";
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { organization } from 'better-auth/plugins';
|
||||
import { sendEmail } from './email';
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
organization({
|
||||
// Organization limits
|
||||
allowUserToCreateOrganization: true,
|
||||
organizationLimit: 10,
|
||||
membershipLimit: 100,
|
||||
creatorRole: "owner",
|
||||
plugins: [
|
||||
organization({
|
||||
// Organization limits
|
||||
allowUserToCreateOrganization: true,
|
||||
organizationLimit: 10,
|
||||
membershipLimit: 100,
|
||||
creatorRole: 'owner',
|
||||
|
||||
// Slugs
|
||||
defaultOrganizationIdField: "slug",
|
||||
// Slugs
|
||||
defaultOrganizationIdField: 'slug',
|
||||
|
||||
// Invitations
|
||||
invitationExpiresIn: 60 * 60 * 24 * 7, // 7 days
|
||||
invitationLimit: 50,
|
||||
sendInvitationEmail: async (data) => {
|
||||
await sendEmail({
|
||||
to: data.email,
|
||||
subject: `Join ${data.organization.name}`,
|
||||
html: `<a href="https://app.com/invite/${data.invitation.id}">Accept</a>`,
|
||||
});
|
||||
},
|
||||
// Invitations
|
||||
invitationExpiresIn: 60 * 60 * 24 * 7, // 7 days
|
||||
invitationLimit: 50,
|
||||
sendInvitationEmail: async (data) => {
|
||||
await sendEmail({
|
||||
to: data.email,
|
||||
subject: `Join ${data.organization.name}`,
|
||||
html: `<a href="https://app.com/invite/${data.invitation.id}">Accept</a>`
|
||||
});
|
||||
},
|
||||
|
||||
// Hooks
|
||||
hooks: {
|
||||
organization: {
|
||||
afterCreate: async ({ organization }) => {
|
||||
console.log(`Organization ${organization.name} created`);
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
// Hooks
|
||||
hooks: {
|
||||
organization: {
|
||||
afterCreate: async ({ organization }) => {
|
||||
console.log(`Organization ${organization.name} created`);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
@@ -11,33 +11,33 @@ description: Configure TOTP authenticator apps, send OTP codes via email/SMS, ma
|
||||
4. Verify: check that `twoFactorSecret` column exists on user table
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { twoFactor } from "better-auth/plugins";
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { twoFactor } from 'better-auth/plugins';
|
||||
|
||||
export const auth = betterAuth({
|
||||
appName: "My App",
|
||||
plugins: [
|
||||
twoFactor({
|
||||
issuer: "My App",
|
||||
}),
|
||||
],
|
||||
appName: 'My App',
|
||||
plugins: [
|
||||
twoFactor({
|
||||
issuer: 'My App'
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Client-Side Setup
|
||||
|
||||
```ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { twoFactorClient } from "better-auth/client/plugins";
|
||||
import { createAuthClient } from 'better-auth/client';
|
||||
import { twoFactorClient } from 'better-auth/client/plugins';
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
twoFactorClient({
|
||||
onTwoFactorRedirect() {
|
||||
window.location.href = "/2fa";
|
||||
},
|
||||
}),
|
||||
],
|
||||
plugins: [
|
||||
twoFactorClient({
|
||||
onTwoFactorRedirect() {
|
||||
window.location.href = '/2fa';
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
@@ -47,14 +47,14 @@ Requires password verification. Returns TOTP URI (for QR code) and backup codes.
|
||||
|
||||
```ts
|
||||
const enable2FA = async (password: string) => {
|
||||
const { data, error } = await authClient.twoFactor.enable({
|
||||
password,
|
||||
});
|
||||
const { data, error } = await authClient.twoFactor.enable({
|
||||
password
|
||||
});
|
||||
|
||||
if (data) {
|
||||
// data.totpURI — generate a QR code from this
|
||||
// data.backupCodes — display to user
|
||||
}
|
||||
if (data) {
|
||||
// data.totpURI — generate a QR code from this
|
||||
// data.backupCodes — display to user
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
@@ -65,10 +65,10 @@ const enable2FA = async (password: string) => {
|
||||
### Displaying the QR Code
|
||||
|
||||
```tsx
|
||||
import QRCode from "react-qr-code";
|
||||
import QRCode from 'react-qr-code';
|
||||
|
||||
const TotpSetup = ({ totpURI }: { totpURI: string }) => {
|
||||
return <QRCode value={totpURI} />;
|
||||
return <QRCode value={totpURI} />;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -78,10 +78,10 @@ Accepts codes from one period before/after current time:
|
||||
|
||||
```ts
|
||||
const verifyTotp = async (code: string) => {
|
||||
const { data, error } = await authClient.twoFactor.verifyTotp({
|
||||
code,
|
||||
trustDevice: true,
|
||||
});
|
||||
const { data, error } = await authClient.twoFactor.verifyTotp({
|
||||
code,
|
||||
trustDevice: true
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
@@ -89,10 +89,10 @@ const verifyTotp = async (code: string) => {
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
totpOptions: {
|
||||
digits: 6, // 6 or 8 digits (default: 6)
|
||||
period: 30, // Code validity period in seconds (default: 30)
|
||||
},
|
||||
totpOptions: {
|
||||
digits: 6, // 6 or 8 digits (default: 6)
|
||||
period: 30 // Code validity period in seconds (default: 30)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -101,27 +101,27 @@ twoFactor({
|
||||
### Configuring OTP Delivery
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { twoFactor } from "better-auth/plugins";
|
||||
import { sendEmail } from "./email";
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { twoFactor } from 'better-auth/plugins';
|
||||
import { sendEmail } from './email';
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
sendOTP: async ({ user, otp }, ctx) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "Your verification code",
|
||||
text: `Your code is: ${otp}`,
|
||||
});
|
||||
},
|
||||
period: 5, // Code validity in minutes (default: 3)
|
||||
digits: 6, // Number of digits (default: 6)
|
||||
allowedAttempts: 5, // Max verification attempts (default: 5)
|
||||
},
|
||||
}),
|
||||
],
|
||||
plugins: [
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
sendOTP: async ({ user, otp }, ctx) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: 'Your verification code',
|
||||
text: `Your code is: ${otp}`
|
||||
});
|
||||
},
|
||||
period: 5, // Code validity in minutes (default: 3)
|
||||
digits: 6, // Number of digits (default: 6)
|
||||
allowedAttempts: 5 // Max verification attempts (default: 5)
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
@@ -135,9 +135,9 @@ Configure how OTP codes are stored in the database:
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
storeOTP: "encrypted", // Options: "plain", "encrypted", "hashed"
|
||||
},
|
||||
otpOptions: {
|
||||
storeOTP: 'encrypted' // Options: "plain", "encrypted", "hashed"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -145,12 +145,12 @@ For custom encryption:
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
storeOTP: {
|
||||
encrypt: async (token) => myEncrypt(token),
|
||||
decrypt: async (token) => myDecrypt(token),
|
||||
},
|
||||
},
|
||||
otpOptions: {
|
||||
storeOTP: {
|
||||
encrypt: async (token) => myEncrypt(token),
|
||||
decrypt: async (token) => myDecrypt(token)
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -162,16 +162,16 @@ Generated automatically when 2FA is enabled. Each code is single-use.
|
||||
|
||||
```tsx
|
||||
const BackupCodes = ({ codes }: { codes: string[] }) => {
|
||||
return (
|
||||
<div>
|
||||
<p>Save these codes in a secure location:</p>
|
||||
<ul>
|
||||
{codes.map((code, i) => (
|
||||
<li key={i}>{code}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<p>Save these codes in a secure location:</p>
|
||||
<ul>
|
||||
{codes.map((code, i) => (
|
||||
<li key={i}>{code}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -181,10 +181,10 @@ Invalidates all previous codes:
|
||||
|
||||
```ts
|
||||
const regenerateBackupCodes = async (password: string) => {
|
||||
const { data, error } = await authClient.twoFactor.generateBackupCodes({
|
||||
password,
|
||||
});
|
||||
// data.backupCodes contains the new codes
|
||||
const { data, error } = await authClient.twoFactor.generateBackupCodes({
|
||||
password
|
||||
});
|
||||
// data.backupCodes contains the new codes
|
||||
};
|
||||
```
|
||||
|
||||
@@ -192,10 +192,10 @@ const regenerateBackupCodes = async (password: string) => {
|
||||
|
||||
```ts
|
||||
const verifyBackupCode = async (code: string) => {
|
||||
const { data, error } = await authClient.twoFactor.verifyBackupCode({
|
||||
code,
|
||||
trustDevice: true,
|
||||
});
|
||||
const { data, error } = await authClient.twoFactor.verifyBackupCode({
|
||||
code,
|
||||
trustDevice: true
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
@@ -203,11 +203,11 @@ const verifyBackupCode = async (code: string) => {
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
backupCodeOptions: {
|
||||
amount: 10, // Number of codes to generate (default: 10)
|
||||
length: 10, // Length of each code (default: 10)
|
||||
storeBackupCodes: "encrypted", // Options: "plain", "encrypted"
|
||||
},
|
||||
backupCodeOptions: {
|
||||
amount: 10, // Number of codes to generate (default: 10)
|
||||
length: 10, // Length of each code (default: 10)
|
||||
storeBackupCodes: 'encrypted' // Options: "plain", "encrypted"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -225,16 +225,16 @@ Response includes `twoFactorRedirect: true` when 2FA is required:
|
||||
|
||||
```ts
|
||||
const signIn = async (email: string, password: string) => {
|
||||
const { data, error } = await authClient.signIn.email(
|
||||
{ email, password },
|
||||
{
|
||||
onSuccess(context) {
|
||||
if (context.data.twoFactorRedirect) {
|
||||
window.location.href = "/2fa";
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
const { data, error } = await authClient.signIn.email(
|
||||
{ email, password },
|
||||
{
|
||||
onSuccess(context) {
|
||||
if (context.data.twoFactorRedirect) {
|
||||
window.location.href = '/2fa';
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -252,7 +252,7 @@ Flow: credentials → session removed → temporary 2FA cookie (10 min default)
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
twoFactorCookieMaxAge: 600, // 10 minutes in seconds (default)
|
||||
twoFactorCookieMaxAge: 600 // 10 minutes in seconds (default)
|
||||
});
|
||||
```
|
||||
|
||||
@@ -262,9 +262,9 @@ Built-in: 3 requests per 10 seconds for all 2FA endpoints. OTP has additional at
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
allowedAttempts: 5, // Max attempts per OTP code (default: 5)
|
||||
},
|
||||
otpOptions: {
|
||||
allowedAttempts: 5 // Max attempts per OTP code (default: 5)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -280,52 +280,52 @@ Requires password confirmation. Revokes trusted device records:
|
||||
|
||||
```ts
|
||||
const disable2FA = async (password: string) => {
|
||||
const { data, error } = await authClient.twoFactor.disable({
|
||||
password,
|
||||
});
|
||||
const { data, error } = await authClient.twoFactor.disable({
|
||||
password
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Complete Configuration Example
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { twoFactor } from "better-auth/plugins";
|
||||
import { sendEmail } from "./email";
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { twoFactor } from 'better-auth/plugins';
|
||||
import { sendEmail } from './email';
|
||||
|
||||
export const auth = betterAuth({
|
||||
appName: "My App",
|
||||
plugins: [
|
||||
twoFactor({
|
||||
// TOTP settings
|
||||
issuer: "My App",
|
||||
totpOptions: {
|
||||
digits: 6,
|
||||
period: 30,
|
||||
},
|
||||
// OTP settings
|
||||
otpOptions: {
|
||||
sendOTP: async ({ user, otp }) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "Your verification code",
|
||||
text: `Your code is: ${otp}`,
|
||||
});
|
||||
},
|
||||
period: 5,
|
||||
allowedAttempts: 5,
|
||||
storeOTP: "encrypted",
|
||||
},
|
||||
// Backup code settings
|
||||
backupCodeOptions: {
|
||||
amount: 10,
|
||||
length: 10,
|
||||
storeBackupCodes: "encrypted",
|
||||
},
|
||||
// Session settings
|
||||
twoFactorCookieMaxAge: 600, // 10 minutes
|
||||
trustDeviceMaxAge: 30 * 24 * 60 * 60, // 30 days
|
||||
}),
|
||||
],
|
||||
appName: 'My App',
|
||||
plugins: [
|
||||
twoFactor({
|
||||
// TOTP settings
|
||||
issuer: 'My App',
|
||||
totpOptions: {
|
||||
digits: 6,
|
||||
period: 30
|
||||
},
|
||||
// OTP settings
|
||||
otpOptions: {
|
||||
sendOTP: async ({ user, otp }) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: 'Your verification code',
|
||||
text: `Your code is: ${otp}`
|
||||
});
|
||||
},
|
||||
period: 5,
|
||||
allowedAttempts: 5,
|
||||
storeOTP: 'encrypted'
|
||||
},
|
||||
// Backup code settings
|
||||
backupCodeOptions: {
|
||||
amount: 10,
|
||||
length: 10,
|
||||
storeBackupCodes: 'encrypted'
|
||||
},
|
||||
// Session settings
|
||||
twoFactorCookieMaxAge: 600, // 10 minutes
|
||||
trustDeviceMaxAge: 30 * 24 * 60 * 60 // 30 days
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user