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:
NeilClack
2026-07-07 13:36:23 -05:00
parent 38347f5f24
commit 38d1752f5b
98 changed files with 3408 additions and 1758 deletions
@@ -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}`);
}
}
}
}
}
});
```
+44 -38
View File
@@ -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
})
]
});
```