Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 51 additions & 12 deletions better-auth/best-practices/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" }`

---
Expand All @@ -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.**

---

Expand All @@ -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. |
Expand All @@ -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"`.

Expand Down Expand Up @@ -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`.

---
Expand All @@ -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
Expand All @@ -162,7 +198,10 @@ For separate client/server projects: `createAuthClient<typeof auth>()`.
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`

---

Expand Down
29 changes: 21 additions & 8 deletions better-auth/create-auth/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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) |

---

Expand Down Expand Up @@ -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.**

Expand All @@ -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` |

---

Expand All @@ -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.

Expand Down Expand Up @@ -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)
20 changes: 18 additions & 2 deletions better-auth/emailAndPassword/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions better-auth/organization/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions better-auth/twoFactor/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
Loading