diff --git a/better-auth/best-practices/SKILL.md b/better-auth/best-practices/SKILL.md index 3e6a4e1..ff0f0cc 100644 --- a/better-auth/best-practices/SKILL.md +++ b/better-auth/best-practices/SKILL.md @@ -15,7 +15,7 @@ description: Configure Better Auth server and client, set up database adapters, 2. Set env vars: `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` 3. Create `auth.ts` with database + config 4. Create route handler for your framework -5. Run `npx @better-auth/cli@latest migrate` +5. Run `npx auth migrate` (or `npx @better-auth/cli@latest migrate`) 6. Verify: call `GET /api/auth/ok` — should return `{ status: "ok" }` --- @@ -32,11 +32,19 @@ Only define `baseURL`/`secret` in config if env vars are NOT set. 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 -**Re-run after adding/changing plugins.** +The new standalone CLI (`npx auth`) replaces the old `@better-auth/cli` package (now deprecated): + +- `npx auth init` - Interactive setup wizard (config, database adapter, framework integration) +- `npx auth migrate` - Apply schema (built-in adapter) +- `npx auth generate` - Generate schema for Prisma/Drizzle +- `npx auth generate --adapter prisma` - Generate schema for a specific adapter without a config file +- `npx auth generate --adapter drizzle` - Same, for Drizzle +- `npx auth upgrade` - Upgrade Better Auth to the latest version + +The old `@better-auth/cli` commands still work as aliases during the deprecation period. + +**Re-run migrate/generate after adding/changing plugins.** --- @@ -45,7 +53,7 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c | Option | Notes | |--------|-------| | `appName` | Optional display name | -| `baseURL` | Only if `BETTER_AUTH_URL` not set | +| `baseURL` | Only if `BETTER_AUTH_URL` not set. Supports dynamic config object: `{ allowedHosts, fallback, protocol }` for Vercel preview deployments and multi-domain setups. | | `basePath` | Default `/api/auth`. Set `/` for root. | | `secret` | Only if `BETTER_AUTH_SECRET` not set | | `database` | Required for most features. See adapters docs. | @@ -59,9 +67,11 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c ## Database -**Direct connections:** Pass `pg.Pool`, `mysql2` pool, `better-sqlite3`, or `bun:sqlite` instance. +**Direct connections:** Pass `pg.Pool`, `mysql2` pool, `better-sqlite3`, `bun:sqlite`, or a Cloudflare D1 binding. -**ORM adapters:** Import from `better-auth/adapters/drizzle`, `better-auth/adapters/prisma`, `better-auth/adapters/mongodb`. +**ORM adapters:** Import from `better-auth/adapters/drizzle`, `better-auth/adapters/prisma`, `better-auth/adapters/mongodb` (re-exported from the main package), or directly from the extracted packages for smaller bundles: `@better-auth/drizzle-adapter`, `@better-auth/prisma-adapter`, `@better-auth/kysely-adapter`, `@better-auth/mongo-adapter`. + +**Cloudflare D1:** Pass the D1 binding directly — auto-detected, no adapter setup required. Note: D1 does not support interactive transactions; Better Auth uses `batch()` for atomicity. **Critical:** Better Auth uses adapter model names, NOT underlying table names. If Prisma model is `User` mapping to table `users`, use `modelName: "user"` (Prisma reference), not `"users"`. @@ -109,18 +119,34 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c - `disableOriginCheck` - ⚠️ Security risk - `crossSubDomainCookies.enabled` - Share cookies across subdomains - `ipAddress.ipAddressHeaders` - Custom IP headers for proxies +- `ipAddress.ipv6Subnet` - Rate limit IPv6 by subnet prefix (default: 64) - `database.generateId` - Custom ID generation or `"serial"`/`"uuid"`/`false` -**Rate limiting:** `rateLimit.enabled`, `rateLimit.window`, `rateLimit.max`, `rateLimit.storage` ("memory" | "database" | "secondary-storage"). +**Rate limiting:** `rateLimit.enabled`, `rateLimit.window`, `rateLimit.max`, `rateLimit.storage` ("memory" | "database" | "secondary-storage"). Default sensitive-endpoint limits are 3 req/10s for sign-in/sign-up and 3 req/60s for password-reset/OTP. + +**Secret key rotation** — rotate `BETTER_AUTH_SECRET` without invalidating existing data by providing a `secrets` array: + +```ts +export const auth = betterAuth({ + secrets: [ + { version: 2, value: "new-secret-key" }, // first = active for new encryptions + { version: 1, value: "old-secret-key" }, // kept for decryption + ], +}); +``` + +Or via environment variable: `BETTER_AUTH_SECRETS="2:new-secret,1:old-secret"` --- ## Hooks -**Endpoint hooks:** `hooks.before` / `hooks.after` - Array of `{ matcher, handler }`. Use `createAuthMiddleware`. Access `ctx.path`, `ctx.context.returned` (after), `ctx.context.session`. +**Endpoint hooks:** `hooks.before` / `hooks.after` — Pass a single `createAuthMiddleware` handler or an array of `{ matcher, handler }` objects. Both global and plugin hooks use the same `AuthMiddleware` type. Access `ctx.path`, `ctx.context.returned` (after), `ctx.context.session`. **Database hooks:** `databaseHooks.user.create.before/after`, same for `session`, `account`. Useful for adding default values or post-creation actions. +**Important (1.5):** `after` database hooks (`create.after`, `update.after`, `delete.after`) now run **after the transaction commits**, not inside it. If you need atomic database writes from a hook, use the adapter directly within the main operation. + **Hook context (`ctx.context`):** `session`, `secret`, `authCookies`, `password.hash()`/`verify()`, `adapter`, `internalAdapter`, `generateId()`, `tables`, `baseURL`. --- @@ -133,10 +159,20 @@ 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`. +**Popular plugins (bundled):** `twoFactor`, `organization`, `passkey`, `magicLink`, `emailOtp`, `username`, `phoneNumber`, `admin`, `bearer`, `jwt`, `multiSession`, `openAPI`, `genericOAuth`, `testUtils`. + +**Extracted to their own packages (install separately):** +- `apiKey` → `@better-auth/api-key` (**removed from** `better-auth/plugins` in 1.5) +- OAuth 2.1 provider → `@better-auth/oauth-provider` (replaces deprecated `oidcProvider`) +- `electron` → `@better-auth/electron` +- `i18n` → `@better-auth/i18n` + +**Breaking (1.5):** `apiKey` is no longer exported from `better-auth/plugins`. The `userId` field on `ApiKey` is renamed to `referenceId`. Client plugins go in `createAuthClient({ plugins: [...] })`. +**Session update:** `authClient.updateSession({ ...fields })` updates custom additional session fields without re-authentication. + --- ## Client @@ -162,7 +198,10 @@ For separate client/server projects: `createAuthClient()`. 3. **Secondary storage** - Sessions go there by default, not DB 4. **Cookie cache** - Custom session fields NOT cached, always re-fetched 5. **Stateless mode** - No DB = session in cookie only, logout on cache expiry -6. **Change email flow** - Sends to current email first, then new email +6. **Change email flow** - Sends confirmation to old email, then sends verification to new email (`sendChangeEmailConfirmation` was renamed from `sendChangeEmailVerification`) +7. **After hooks** - Database `after` hooks run post-transaction; don't rely on them for atomic DB writes +8. **apiKey plugin** - Moved to `@better-auth/api-key` package; `userId` field renamed to `referenceId` +9. **getMigrations import** - Must now be imported from `better-auth/db/migration`, not `better-auth` --- diff --git a/better-auth/create-auth/SKILL.md b/better-auth/create-auth/SKILL.md index 515e6dd..2a79f0a 100644 --- a/better-auth/create-auth/SKILL.md +++ b/better-auth/create-auth/SKILL.md @@ -92,7 +92,7 @@ After collecting answers, present a concise implementation plan as a markdown ch - **UI:** Custom forms ### Steps -1. Install `better-auth` and `@better-auth/cli` +1. Install `better-auth` and run `npx auth init` 2. Create `lib/auth.ts` with server config 3. Create `lib/auth-client.ts` with React client 4. Set up route handler at `app/api/auth/[...all]/route.ts` @@ -160,6 +160,12 @@ At the end of implementation, guide users thoroughly on remaining next steps (e. | `@better-auth/stripe` | Stripe payments | | `@better-auth/scim` | SCIM user provisioning | | `@better-auth/expo` | React Native/Expo | +| `@better-auth/api-key` | API key auth (extracted from core in 1.5) | +| `@better-auth/oauth-provider` | OAuth 2.1 / OIDC provider (replaces `oidcProvider`) | +| `@better-auth/electron` | Electron desktop auth | +| `@better-auth/i18n` | Internationalized error messages | +| `@better-auth/drizzle-adapter` | Drizzle adapter (standalone, smaller bundle) | +| `@better-auth/prisma-adapter` | Prisma adapter (standalone, smaller bundle) | --- @@ -234,9 +240,10 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE | 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` | +| Built-in Kysely | `npx auth migrate` (applies directly) | +| Prisma | `npx auth generate --output prisma/schema.prisma` then `npx prisma migrate dev` | +| Drizzle | `npx auth generate --output src/db/auth-schema.ts` then `npx drizzle-kit push` | +| Any adapter (no config) | `npx auth generate --adapter prisma` or `--adapter drizzle` | **Re-run after adding plugins.** @@ -249,9 +256,10 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE | 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` | +| Cloudflare D1 | Pass the D1 binding directly — auto-detected | +| Prisma | `prismaAdapter(prisma, { provider: "postgresql" })` from `better-auth/adapters/prisma` or `@better-auth/prisma-adapter` | +| Drizzle | `drizzleAdapter(db, { provider: "pg" })` from `better-auth/adapters/drizzle` or `@better-auth/drizzle-adapter` | +| MongoDB | `mongodbAdapter(db)` from `better-auth/adapters/mongodb` or `@better-auth/mongo-adapter` | --- @@ -266,6 +274,11 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE | `openAPI` | `better-auth/plugins` | - | API docs | | `passkey` | `@better-auth/passkey` | `passkeyClient` | WebAuthn | | `sso` | `@better-auth/sso` | - | Enterprise SSO | +| `apiKey` | `@better-auth/api-key` | `apiKeyClient` | API keys (standalone package) | +| `oauthProvider` | `@better-auth/oauth-provider` | - | OAuth 2.1 / OIDC server | +| `electron` | `@better-auth/electron` | `electronClient` | Electron desktop auth | +| `i18n` | `@better-auth/i18n` | - | Translated error messages | +| `testUtils` | `better-auth/plugins` | - | Testing utilities | **Plugin pattern:** Server plugin + client plugin + run migrations. @@ -317,5 +330,5 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE - [Docs](https://better-auth.com/docs) - [Examples](https://github.com/better-auth/examples) - [Plugins](https://better-auth.com/docs/concepts/plugins) -- [CLI](https://better-auth.com/docs/concepts/cli) +- [CLI](https://better-auth.com/docs/concepts/cli) — Use `npx auth` (new standalone CLI replacing `@better-auth/cli`) - [Migration Guides](https://better-auth.com/docs/guides) diff --git a/better-auth/emailAndPassword/SKILL.md b/better-auth/emailAndPassword/SKILL.md index 537c010..9a51a3a 100644 --- a/better-auth/emailAndPassword/SKILL.md +++ b/better-auth/emailAndPassword/SKILL.md @@ -8,7 +8,7 @@ description: Configure email verification, implement password reset flows, set p 1. Enable email/password: `emailAndPassword: { enabled: true }` 2. Configure `emailVerification.sendVerificationEmail` 3. Add `sendResetPassword` for password reset flows -4. Run `npx @better-auth/cli@latest migrate` +4. Run `npx auth migrate` (deprecated alias: `npx @better-auth/cli@latest migrate`) 5. Verify: attempt sign-up and confirm verification email triggers --- @@ -48,7 +48,21 @@ export const auth = betterAuth({ }); ``` -**Note**: This requires `sendVerificationEmail` to be configured and only applies to email/password sign-ins. +**Note**: This requires `sendVerificationEmail` to be configured and only applies to email/password sign-ins. When enabled, sign-up no longer reveals whether an email address is already registered (enumeration prevention, 1.5). + +## Server-Side Password Verification + +`verifyPassword` is a **server-only** endpoint (1.5). Use it to verify a user's current password without signing in, e.g. before a sensitive operation: + +```ts +// Server-side only — there is no authClient.verifyPassword() +const result = await auth.api.verifyPassword({ + body: { password: "current-password" }, + headers: request.headers, // forwards the session cookie +}); +``` + +The endpoint returns `{ valid: true }` on success or an error response if the password is incorrect or no session is present. ## Client Side Validation @@ -92,6 +106,8 @@ export const auth = betterAuth({ }); ``` +**Change email flow (1.5 rename):** The callback for sending confirmation to the current address is `sendChangeEmailConfirmation` (was `sendChangeEmailVerification` — now removed). The `/change-email` endpoint always returns `{ status: true }` regardless of whether the email exists, preventing user enumeration. + ### Security Considerations Built-in protections: background email sending (timing attack prevention), dummy operations on invalid requests, constant response messages regardless of user existence. diff --git a/better-auth/organization/SKILL.md b/better-auth/organization/SKILL.md index 0e84a84..e21c084 100644 --- a/better-auth/organization/SKILL.md +++ b/better-auth/organization/SKILL.md @@ -7,7 +7,7 @@ description: Configure multi-tenant organizations, manage members and invitation 1. Add `organization()` plugin to server config 2. Add `organizationClient()` plugin to client config -3. Run `npx @better-auth/cli migrate` +3. Run `npx auth migrate` (deprecated alias: `npx @better-auth/cli migrate`) 4. Verify: check that organization, member, invitation tables exist in your database ```ts @@ -299,14 +299,14 @@ export const auth = betterAuth({ ```ts await authClient.organization.createRole({ role: "moderator", - permission: { + permissions: { // Note: "permissions" (plural) — "permission" was renamed in 1.5 member: ["read"], invitation: ["read"], }, }); ``` -Use `updateRole({ roleId, permission })` and `deleteRole({ roleId })`. Pre-defined roles (owner, admin, member) cannot be deleted. Roles assigned to members cannot be deleted until reassigned. +Use `updateRole({ roleId, permissions })` and `deleteRole({ roleId })`. Pre-defined roles (owner, admin, member) cannot be deleted. Roles assigned to members cannot be deleted until reassigned. ## Lifecycle Hooks @@ -433,6 +433,7 @@ organization({ - Invitations expire after 48 hours by default - Only the invited email address can accept an invitation +- Expired invitations are automatically rejected on acceptance attempts (1.5) - Pending invitations can be cancelled by organization admins ## Complete Configuration Example diff --git a/better-auth/twoFactor/SKILL.md b/better-auth/twoFactor/SKILL.md index cf9c30b..8a47803 100644 --- a/better-auth/twoFactor/SKILL.md +++ b/better-auth/twoFactor/SKILL.md @@ -7,7 +7,7 @@ description: Configure TOTP authenticator apps, send OTP codes via email/SMS, ma 1. Add `twoFactor()` plugin to server config with `issuer` 2. Add `twoFactorClient()` plugin to client config -3. Run `npx @better-auth/cli migrate` +3. Run `npx auth migrate` (deprecated alias: `npx @better-auth/cli migrate`) 4. Verify: check that `twoFactorSecret` column exists on user table ```ts @@ -270,7 +270,7 @@ twoFactor({ ### Encryption at Rest -TOTP secrets: encrypted with auth secret. Backup codes: encrypted by default. OTP: configurable (`"plain"`, `"encrypted"`, `"hashed"`). Uses constant-time comparison for verification. +TOTP secrets: encrypted with auth secret. Backup codes: encrypted by default. OTP: configurable (`"plain"`, `"encrypted"`, `"hashed"`). Uses constant-time comparison for verification. OTP codes are atomically invalidated on use to prevent race-condition reuse attacks (1.5). 2FA can only be enabled for credential (email/password) accounts. diff --git a/security/SKILL.MD b/security/SKILL.MD index 5abc5a8..2950285 100644 --- a/security/SKILL.MD +++ b/security/SKILL.MD @@ -27,6 +27,23 @@ Better Auth looks for secrets in this order: - Generate: `openssl rand -base64 32` - Never commit secrets to version control +### Non-Destructive Secret Key Rotation + +Rotate `BETTER_AUTH_SECRET` without invalidating existing sessions, tokens, or encrypted data. The first entry is always used for new encryptions; all entries are tried for decryption. + +```ts +export const auth = betterAuth({ + secrets: [ + { version: 2, value: process.env.NEW_SECRET }, // current — used for all new encryptions + { version: 1, value: process.env.OLD_SECRET }, // previous — kept for decryption only + ], +}); +``` + +Or via environment variable: `BETTER_AUTH_SECRETS="2:new-secret-key,1:old-secret-key"` + +Remove an old key from the array only after all data encrypted with it has been re-encrypted or expired. + ## Rate Limiting Enabled in production by default. Applies to all endpoints. Plugins can override per-endpoint. @@ -45,6 +62,11 @@ export const auth = betterAuth({ }); ``` +**Hardened defaults (1.5):** +- Sign-in / sign-up: 3 requests per 10 seconds +- Password reset / OTP: 3 requests per 60 seconds +- Rejected requests are **not** counted against the limit + ### Storage Options Options: `"memory"` (resets on restart, avoid on serverless), `"database"` (persistent), `"secondary-storage"` (Redis, default when available). @@ -55,6 +77,18 @@ rateLimit: { } ``` +### IPv6 Support + +Rate limit by IPv6 subnet prefix to prevent abuse from /128 addresses: + +```ts +advanced: { + ipAddress: { + ipv6Subnet: 64, // Rate limit by /64 subnet (default: 64) + }, +} +``` + ### Custom Storage Implement your own rate limit storage: @@ -74,7 +108,7 @@ rateLimit: { ### Per-Endpoint Rules -Sensitive endpoints default to 3 requests per 10 seconds (`/sign-in`, `/sign-up`, `/change-password`, `/change-email`). Override: +Sensitive endpoints default to 3 requests per 10 seconds (`/sign-in`, `/sign-up`, `/change-password`, `/change-email`), and 3 per 60 seconds for password reset and OTP. Override: ```ts rateLimit: { @@ -261,6 +295,8 @@ Set `ipv6Subnet` (128, 64, 48, 32; default 64) to group IPv6 addresses. Enable ` ## Database Hooks for Security Auditing +**Important (1.5):** Database `after` hooks (`create.after`, `update.after`, `delete.after`) now run **after the transaction commits**, not during it. This means external side effects (emails, API calls) will not roll back the transaction if they fail — but you cannot rely on them for additional atomic DB writes. + ```ts import { betterAuth } from "better-auth"; @@ -332,6 +368,34 @@ export const auth = betterAuth({ Ensures operations like sending emails don't affect response timing. +## Verification Token Storage + +Verification tokens (email verification, password reset, etc.) can be stored in secondary storage (e.g., Redis) instead of — or in addition to — the database: + +```ts +export const auth = betterAuth({ + secondaryStorage: { /* your Redis config */ }, + verification: { + storeIdentifier: "hashed", // Hash identifiers for extra security + storeInDatabase: false, // Secondary storage only + }, +}); +``` + +Per-type overrides: + +```ts +verification: { + storeIdentifier: { + default: "plain", + overrides: { + "email-verification": "hashed", + "password-reset": "hashed", + }, + }, +}, +``` + ## Account Enumeration Prevention Built-in: consistent response messages, dummy operations on invalid requests, background email sending. Return generic error messages ("Invalid credentials") rather than specific ones ("User not found"). @@ -421,9 +485,10 @@ export const auth = betterAuth({ Before deploying to production: - [ ] **Secret**: Use a strong, unique secret (32+ characters, high entropy) +- [ ] **Secret Rotation**: Use `secrets` array for zero-downtime key rotation when needed - [ ] **HTTPS**: Ensure `baseURL` uses HTTPS - [ ] **Trusted Origins**: Configure all valid origins (frontend, mobile apps) -- [ ] **Rate Limiting**: Keep enabled with appropriate limits +- [ ] **Rate Limiting**: Keep enabled with appropriate limits (hardened defaults in 1.5) - [ ] **CSRF Protection**: Keep enabled (`disableCSRFCheck: false`) - [ ] **Secure Cookies**: Enabled automatically with HTTPS - [ ] **OAuth Tokens**: Consider `encryptOAuthTokens: true` if storing tokens