better-auth

Build authentication systems for TypeScript/Cloudflare Workers with social auth, 2FA, passkeys, organizations, and RBAC. Self-hosted alternative to Clerk/Auth.js. IMPORTANT: Requires Drizzle ORM or Kysely for D1 - no direct D1 adapter. v1.4.0 (Nov 2025) adds stateless sessions, ESM-only (breaking), JWT key rotation, SCIM provisioning. v1.3 adds SSO/SAML, multi-team support. Use when: self-hosting auth on Cloudflare D1, migrating from Clerk, implementing multi-tenant SaaS, or troubleshooting D1 adapter errors, session serialization, OAuth flows, TanStack Start cookie issues, nanostore session invalidation.

31 stars

Best use case

better-auth is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Build authentication systems for TypeScript/Cloudflare Workers with social auth, 2FA, passkeys, organizations, and RBAC. Self-hosted alternative to Clerk/Auth.js. IMPORTANT: Requires Drizzle ORM or Kysely for D1 - no direct D1 adapter. v1.4.0 (Nov 2025) adds stateless sessions, ESM-only (breaking), JWT key rotation, SCIM provisioning. v1.3 adds SSO/SAML, multi-team support. Use when: self-hosting auth on Cloudflare D1, migrating from Clerk, implementing multi-tenant SaaS, or troubleshooting D1 adapter errors, session serialization, OAuth flows, TanStack Start cookie issues, nanostore session invalidation.

Teams using better-auth should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/better-auth/SKILL.md --create-dirs "https://raw.githubusercontent.com/ovachiever/droid-tings/main/skills/better-auth/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/better-auth/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How better-auth Compares

Feature / Agentbetter-authStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Build authentication systems for TypeScript/Cloudflare Workers with social auth, 2FA, passkeys, organizations, and RBAC. Self-hosted alternative to Clerk/Auth.js. IMPORTANT: Requires Drizzle ORM or Kysely for D1 - no direct D1 adapter. v1.4.0 (Nov 2025) adds stateless sessions, ESM-only (breaking), JWT key rotation, SCIM provisioning. v1.3 adds SSO/SAML, multi-team support. Use when: self-hosting auth on Cloudflare D1, migrating from Clerk, implementing multi-tenant SaaS, or troubleshooting D1 adapter errors, session serialization, OAuth flows, TanStack Start cookie issues, nanostore session invalidation.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# better-auth - D1 Adapter & Error Prevention Guide

**Package**: better-auth@1.4.0 (Nov 22, 2025)
**Breaking Changes**: ESM-only (v1.4.0), Multi-team table changes (v1.3), D1 requires Drizzle/Kysely (no direct adapter)

---

## ⚠️ CRITICAL: D1 Adapter Requirement

better-auth **DOES NOT** have `d1Adapter()`. You **MUST** use:
- **Drizzle ORM** (recommended): `drizzleAdapter(db, { provider: "sqlite" })`
- **Kysely**: `new Kysely({ dialect: new D1Dialect({ database: env.DB }) })`

See Issue #1 below for details.

---

## What's New in v1.4.0 (Nov 22, 2025)

**Major Features:**
- **Stateless session management** - Sessions without database storage
- **ESM-only package** ⚠️ Breaking: CommonJS no longer supported
- **JWT key rotation** - Automatic key rotation for enhanced security
- **SCIM provisioning** - Enterprise user provisioning protocol
- **@standard-schema/spec** - Replaces ZodType for validation
- **CaptchaFox integration** - Built-in CAPTCHA support
- Automatic server-side IP detection
- Cookie-based account data storage
- Multiple passkey origins support
- RP-Initiated Logout endpoint (OIDC)

📚 **Docs**: https://www.better-auth.com/changelogs

---

## What's New in v1.3 (July 2025)

**Major Features:**
- **SSO with SAML 2.0** - Enterprise single sign-on (moved to separate `@better-auth/sso` package)
- **Multi-team support** ⚠️ Breaking: `teamId` removed from member table, new `teamMembers` table required
- **Additional fields** - Custom fields for organization/member/invitation models
- Performance improvements and bug fixes

📚 **Docs**: https://www.better-auth.com/blog/1-3

---

## Alternative: Kysely Adapter Pattern

If you prefer Kysely over Drizzle:

**File**: `src/auth.ts`

```typescript
import { betterAuth } from "better-auth";
import { Kysely, CamelCasePlugin } from "kysely";
import { D1Dialect } from "kysely-d1";

type Env = {
  DB: D1Database;
  BETTER_AUTH_SECRET: string;
  // ... other env vars
};

export function createAuth(env: Env) {
  return betterAuth({
    secret: env.BETTER_AUTH_SECRET,

    // Kysely with D1Dialect
    database: {
      db: new Kysely({
        dialect: new D1Dialect({
          database: env.DB,
        }),
        plugins: [
          // CRITICAL: Required if using Drizzle schema with snake_case
          new CamelCasePlugin(),
        ],
      }),
      type: "sqlite",
    },

    emailAndPassword: {
      enabled: true,
    },

    // ... other config
  });
}
```

**Why CamelCasePlugin?**

If your Drizzle schema uses `snake_case` column names (e.g., `email_verified`), but better-auth expects `camelCase` (e.g., `emailVerified`), the `CamelCasePlugin` automatically converts between the two.

---

## Framework Integrations

### TanStack Start

**⚠️ CRITICAL**: TanStack Start requires the `reactStartCookies` plugin to handle cookie setting properly.

```typescript
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { reactStartCookies } from "better-auth/react-start";

export const auth = betterAuth({
  database: drizzleAdapter(db, { provider: "sqlite" }),
  plugins: [
    twoFactor(),
    organization(),
    reactStartCookies(), // ⚠️ MUST be LAST plugin
  ],
});
```

**Why it's needed**: TanStack Start uses a special cookie handling system. Without this plugin, auth functions like `signInEmail()` and `signUpEmail()` won't set cookies properly, causing authentication to fail.

**Important**: The `reactStartCookies` plugin **must be the last plugin in the array**.

**API Route Setup** (`/src/routes/api/auth/$.ts`):
```typescript
import { auth } from '@/lib/auth'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/api/auth/$')({
  server: {
    handlers: {
      GET: ({ request }) => auth.handler(request),
      POST: ({ request }) => auth.handler(request),
    },
  },
})
```

📚 **Official Docs**: https://www.better-auth.com/docs/integrations/tanstack

---

## Available Plugins (v1.3+)

Better Auth provides plugins for advanced authentication features:

| Plugin | Import | Description | Docs |
|--------|--------|-------------|------|
| **OIDC Provider** | `better-auth/plugins` | Build your own OpenID Connect provider (become an OAuth provider for other apps) | [📚](https://www.better-auth.com/docs/plugins/oidc-provider) |
| **SSO** | `better-auth/plugins` | Enterprise Single Sign-On with OIDC, OAuth2, and SAML 2.0 support | [📚](https://www.better-auth.com/docs/plugins/sso) |
| **Stripe** | `better-auth/plugins` | Payment and subscription management (stable as of v1.3+) | [📚](https://www.better-auth.com/docs/plugins/stripe) |
| **MCP** | `better-auth/plugins` | Act as OAuth provider for Model Context Protocol (MCP) clients | [📚](https://www.better-auth.com/docs/plugins/mcp) |
| **Expo** | `better-auth/expo` | React Native/Expo integration with secure cookie management | [📚](https://www.better-auth.com/docs/integrations/expo) |

---

## API Reference

### Overview: What You Get For Free

When you call `auth.handler()`, better-auth automatically exposes **80+ production-ready REST endpoints** at `/api/auth/*`. Every endpoint is also available as a **server-side method** via `auth.api.*` for programmatic use.

This dual-layer API system means:
- **Clients** (React, Vue, mobile apps) call HTTP endpoints directly
- **Server-side code** (middleware, background jobs) uses `auth.api.*` methods
- **Zero boilerplate** - no need to write auth endpoints manually

**Time savings**: Building this from scratch = ~220 hours. With better-auth = ~4-8 hours. **97% reduction.**

---

### Auto-Generated HTTP Endpoints

All endpoints are automatically exposed at `/api/auth/*` when using `auth.handler()`.

#### Core Authentication Endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/sign-up/email` | POST | Register with email/password |
| `/sign-in/email` | POST | Authenticate with email/password |
| `/sign-out` | POST | Logout user |
| `/change-password` | POST | Update password (requires current password) |
| `/forget-password` | POST | Initiate password reset flow |
| `/reset-password` | POST | Complete password reset with token |
| `/send-verification-email` | POST | Send email verification link |
| `/verify-email` | GET | Verify email with token (`?token=<token>`) |
| `/get-session` | GET | Retrieve current session |
| `/list-sessions` | GET | Get all active user sessions |
| `/revoke-session` | POST | End specific session |
| `/revoke-other-sessions` | POST | End all sessions except current |
| `/revoke-sessions` | POST | End all user sessions |
| `/update-user` | POST | Modify user profile (name, image) |
| `/change-email` | POST | Update email address |
| `/set-password` | POST | Add password to OAuth-only account |
| `/delete-user` | POST | Remove user account |
| `/list-accounts` | GET | Get linked authentication providers |
| `/link-social` | POST | Connect OAuth provider to account |
| `/unlink-account` | POST | Disconnect provider |

#### Social OAuth Endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/sign-in/social` | POST | Initiate OAuth flow (provider specified in body) |
| `/callback/:provider` | GET | OAuth callback handler (e.g., `/callback/google`) |
| `/get-access-token` | GET | Retrieve provider access token |

**Example OAuth flow**:
```typescript
// Client initiates
await authClient.signIn.social({
  provider: "google",
  callbackURL: "/dashboard",
});

// better-auth handles redirect to Google
// Google redirects back to /api/auth/callback/google
// better-auth creates session automatically
```

---

#### Plugin Endpoints

##### Two-Factor Authentication (2FA Plugin)

```typescript
import { twoFactor } from "better-auth/plugins";
```

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/two-factor/enable` | POST | Activate 2FA for user |
| `/two-factor/disable` | POST | Deactivate 2FA |
| `/two-factor/get-totp-uri` | GET | Get QR code URI for authenticator app |
| `/two-factor/verify-totp` | POST | Validate TOTP code from authenticator |
| `/two-factor/send-otp` | POST | Send OTP via email |
| `/two-factor/verify-otp` | POST | Validate email OTP |
| `/two-factor/generate-backup-codes` | POST | Create recovery codes |
| `/two-factor/verify-backup-code` | POST | Use backup code for login |
| `/two-factor/view-backup-codes` | GET | View current backup codes |

📚 **Docs**: https://www.better-auth.com/docs/plugins/2fa

##### Organization Plugin (Multi-Tenant SaaS)

```typescript
import { organization } from "better-auth/plugins";
```

**Organizations** (10 endpoints):

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/organization/create` | POST | Create organization |
| `/organization/list` | GET | List user's organizations |
| `/organization/get-full` | GET | Get complete org details |
| `/organization/update` | PUT | Modify organization |
| `/organization/delete` | DELETE | Remove organization |
| `/organization/check-slug` | GET | Verify slug availability |
| `/organization/set-active` | POST | Set active organization context |

**Members** (8 endpoints):

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/organization/list-members` | GET | Get organization members |
| `/organization/add-member` | POST | Add member directly |
| `/organization/remove-member` | DELETE | Remove member |
| `/organization/update-member-role` | PUT | Change member role |
| `/organization/get-active-member` | GET | Get current member info |
| `/organization/leave` | POST | Leave organization |

**Invitations** (7 endpoints):

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/organization/invite-member` | POST | Send invitation email |
| `/organization/accept-invitation` | POST | Accept invite |
| `/organization/reject-invitation` | POST | Reject invite |
| `/organization/cancel-invitation` | POST | Cancel pending invite |
| `/organization/get-invitation` | GET | Get invitation details |
| `/organization/list-invitations` | GET | List org invitations |
| `/organization/list-user-invitations` | GET | List user's pending invites |

**Teams** (8 endpoints):

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/organization/create-team` | POST | Create team within org |
| `/organization/list-teams` | GET | List organization teams |
| `/organization/update-team` | PUT | Modify team |
| `/organization/remove-team` | DELETE | Remove team |
| `/organization/set-active-team` | POST | Set active team context |
| `/organization/list-team-members` | GET | List team members |
| `/organization/add-team-member` | POST | Add member to team |
| `/organization/remove-team-member` | DELETE | Remove team member |

**Permissions & Roles** (6 endpoints):

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/organization/has-permission` | POST | Check if user has permission |
| `/organization/create-role` | POST | Create custom role |
| `/organization/delete-role` | DELETE | Delete custom role |
| `/organization/list-roles` | GET | List all roles |
| `/organization/get-role` | GET | Get role details |
| `/organization/update-role` | PUT | Modify role permissions |

📚 **Docs**: https://www.better-auth.com/docs/plugins/organization

##### Admin Plugin

```typescript
import { admin } from "better-auth/plugins";
```

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/admin/create-user` | POST | Create user as admin |
| `/admin/list-users` | GET | List all users (with filters/pagination) |
| `/admin/set-role` | POST | Assign user role |
| `/admin/set-user-password` | POST | Change user password |
| `/admin/update-user` | PUT | Modify user details |
| `/admin/remove-user` | DELETE | Delete user account |
| `/admin/ban-user` | POST | Ban user account |
| `/admin/unban-user` | POST | Unban user |
| `/admin/list-user-sessions` | GET | Get user's active sessions |
| `/admin/revoke-user-session` | DELETE | End specific user session |
| `/admin/revoke-user-sessions` | DELETE | End all user sessions |
| `/admin/impersonate-user` | POST | Start impersonating user |
| `/admin/stop-impersonating` | POST | End impersonation session |

📚 **Docs**: https://www.better-auth.com/docs/plugins/admin

##### Other Plugin Endpoints

**Passkey Plugin** (5 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/passkey):
- `/passkey/add`, `/sign-in/passkey`, `/passkey/list`, `/passkey/delete`, `/passkey/update`

**Magic Link Plugin** (2 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/magic-link):
- `/sign-in/magic-link`, `/magic-link/verify`

**Username Plugin** (2 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/username):
- `/sign-in/username`, `/username/is-available`

**Phone Number Plugin** (5 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/phone-number):
- `/sign-in/phone-number`, `/phone-number/send-otp`, `/phone-number/verify`, `/phone-number/request-password-reset`, `/phone-number/reset-password`

**Email OTP Plugin** (6 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/email-otp):
- `/email-otp/send-verification-otp`, `/email-otp/check-verification-otp`, `/sign-in/email-otp`, `/email-otp/verify-email`, `/forget-password/email-otp`, `/email-otp/reset-password`

**Anonymous Plugin** (1 endpoint) - [Docs](https://www.better-auth.com/docs/plugins/anonymous):
- `/sign-in/anonymous`

**JWT Plugin** (2 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/jwt):
- `/token` (get JWT), `/jwks` (public key for verification)

**OpenAPI Plugin** (2 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/open-api):
- `/reference` (interactive API docs with Scalar UI)
- `/generate-openapi-schema` (get OpenAPI spec as JSON)

---

### Server-Side API Methods (`auth.api.*`)

Every HTTP endpoint has a corresponding server-side method. Use these for:
- **Server-side middleware** (protecting routes)
- **Background jobs** (user cleanup, notifications)
- **Admin operations** (bulk user management)
- **Custom auth flows** (programmatic session creation)

#### Core API Methods

```typescript
// Authentication
await auth.api.signUpEmail({
  body: { email, password, name },
  headers: request.headers,
});

await auth.api.signInEmail({
  body: { email, password, rememberMe: true },
  headers: request.headers,
});

await auth.api.signOut({ headers: request.headers });

// Session Management
const session = await auth.api.getSession({ headers: request.headers });

await auth.api.listSessions({ headers: request.headers });

await auth.api.revokeSession({
  body: { token: "session_token_here" },
  headers: request.headers,
});

// User Management
await auth.api.updateUser({
  body: { name: "New Name", image: "https://..." },
  headers: request.headers,
});

await auth.api.changeEmail({
  body: { newEmail: "newemail@example.com" },
  headers: request.headers,
});

await auth.api.deleteUser({
  body: { password: "current_password" },
  headers: request.headers,
});

// Account Linking
await auth.api.linkSocialAccount({
  body: { provider: "google" },
  headers: request.headers,
});

await auth.api.unlinkAccount({
  body: { providerId: "google", accountId: "google_123" },
  headers: request.headers,
});
```

#### Plugin API Methods

**2FA Plugin**:
```typescript
// Enable 2FA
const { totpUri, backupCodes } = await auth.api.enableTwoFactor({
  body: { issuer: "MyApp" },
  headers: request.headers,
});

// Verify TOTP code
await auth.api.verifyTOTP({
  body: { code: "123456", trustDevice: true },
  headers: request.headers,
});

// Generate backup codes
const { backupCodes } = await auth.api.generateBackupCodes({
  headers: request.headers,
});
```

**Organization Plugin**:
```typescript
// Create organization
const org = await auth.api.createOrganization({
  body: { name: "Acme Corp", slug: "acme" },
  headers: request.headers,
});

// Add member
await auth.api.addMember({
  body: {
    userId: "user_123",
    role: "admin",
    organizationId: org.id,
  },
  headers: request.headers,
});

// Check permissions
const hasPermission = await auth.api.hasPermission({
  body: {
    organizationId: org.id,
    permission: "users:delete",
  },
  headers: request.headers,
});
```

**Admin Plugin**:
```typescript
// List users with pagination
const users = await auth.api.listUsers({
  query: {
    search: "john",
    limit: 10,
    offset: 0,
    sortBy: "createdAt",
    sortOrder: "desc",
  },
  headers: request.headers,
});

// Ban user
await auth.api.banUser({
  body: {
    userId: "user_123",
    reason: "Violation of ToS",
    expiresAt: new Date("2025-12-31"),
  },
  headers: request.headers,
});

// Impersonate user (for admin support)
const impersonationSession = await auth.api.impersonateUser({
  body: {
    userId: "user_123",
    expiresIn: 3600, // 1 hour
  },
  headers: request.headers,
});
```

---

### When to Use Which

| Use Case | Use HTTP Endpoints | Use `auth.api.*` Methods |
|----------|-------------------|--------------------------|
| **Client-side auth** | ✅ Yes | ❌ No |
| **Server middleware** | ❌ No | ✅ Yes |
| **Background jobs** | ❌ No | ✅ Yes |
| **Admin dashboards** | ✅ Yes (from client) | ✅ Yes (from server) |
| **Custom auth flows** | ❌ No | ✅ Yes |
| **Mobile apps** | ✅ Yes | ❌ No |
| **API routes** | ✅ Yes (proxy to handler) | ✅ Yes (direct calls) |

**Example: Protected Route Middleware**

```typescript
import { Hono } from "hono";
import { createAuth } from "./auth";
import { createDatabase } from "./db";

const app = new Hono<{ Bindings: Env }>();

// Middleware using server-side API
app.use("/api/protected/*", async (c, next) => {
  const db = createDatabase(c.env.DB);
  const auth = createAuth(db, c.env);

  // Use server-side method
  const session = await auth.api.getSession({
    headers: c.req.raw.headers,
  });

  if (!session) {
    return c.json({ error: "Unauthorized" }, 401);
  }

  // Attach to context
  c.set("user", session.user);
  c.set("session", session.session);

  await next();
});

// Protected route
app.get("/api/protected/profile", async (c) => {
  const user = c.get("user");
  return c.json({ user });
});
```

---

### Discovering Available Endpoints

Use the **OpenAPI plugin** to see all endpoints in your configuration:

```typescript
import { betterAuth } from "better-auth";
import { openAPI } from "better-auth/plugins";

export const auth = betterAuth({
  database: /* ... */,
  plugins: [
    openAPI(), // Adds /api/auth/reference endpoint
  ],
});
```

**Interactive documentation**: Visit `http://localhost:8787/api/auth/reference`

This shows a **Scalar UI** with:
- ✅ All available endpoints grouped by feature
- ✅ Request/response schemas with types
- ✅ Try-it-out functionality (test endpoints in browser)
- ✅ Authentication requirements
- ✅ Code examples in multiple languages

**Programmatic access**:
```typescript
const schema = await auth.api.generateOpenAPISchema();
console.log(JSON.stringify(schema, null, 2));
// Returns full OpenAPI 3.0 spec
```

---

### Quantified Time Savings

**Building from scratch** (manual implementation):
- Core auth endpoints (sign-up, sign-in, OAuth, sessions): **40 hours**
- Email verification & password reset: **10 hours**
- 2FA system (TOTP, backup codes, email OTP): **20 hours**
- Organizations (teams, invitations, RBAC): **60 hours**
- Admin panel (user management, impersonation): **30 hours**
- Testing & debugging: **50 hours**
- Security hardening: **20 hours**

**Total manual effort**: **~220 hours** (5.5 weeks full-time)

**With better-auth**:
- Initial setup: **2-4 hours**
- Customization & styling: **2-4 hours**

**Total with better-auth**: **4-8 hours**

**Savings**: **~97% development time**

---

### Key Takeaway

better-auth provides **80+ production-ready endpoints** covering:
- ✅ Core authentication (20 endpoints)
- ✅ 2FA & passwordless (15 endpoints)
- ✅ Organizations & teams (35 endpoints)
- ✅ Admin & user management (15 endpoints)
- ✅ Social OAuth (auto-configured callbacks)
- ✅ OpenAPI documentation (interactive UI)

**You write zero endpoint code.** Just configure features and call `auth.handler()`.

---

## Known Issues & Solutions

### Issue 1: "d1Adapter is not exported" Error

**Problem**: Code shows `import { d1Adapter } from 'better-auth/adapters/d1'` but this doesn't exist.

**Symptoms**: TypeScript error or runtime error about missing export.

**Solution**: Use Drizzle or Kysely instead:

```typescript
// ❌ WRONG - This doesn't exist
import { d1Adapter } from 'better-auth/adapters/d1'
database: d1Adapter(env.DB)

// ✅ CORRECT - Use Drizzle
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { drizzle } from 'drizzle-orm/d1'
const db = drizzle(env.DB, { schema })
database: drizzleAdapter(db, { provider: "sqlite" })

// ✅ CORRECT - Use Kysely
import { Kysely } from 'kysely'
import { D1Dialect } from 'kysely-d1'
database: {
  db: new Kysely({ dialect: new D1Dialect({ database: env.DB }) }),
  type: "sqlite"
}
```

**Source**: Verified from 4 production repositories using better-auth + D1

---

### Issue 2: Schema Generation Fails

**Problem**: `npx better-auth migrate` doesn't create D1-compatible schema.

**Symptoms**: Migration SQL has wrong syntax or doesn't work with D1.

**Solution**: Use Drizzle Kit to generate migrations:

```bash
# Generate migration from Drizzle schema
npx drizzle-kit generate

# Apply to D1
wrangler d1 migrations apply my-app-db --remote
```

**Why**: Drizzle Kit generates SQLite-compatible SQL that works with D1.

---

### Issue 3: "CamelCase" vs "snake_case" Column Mismatch

**Problem**: Database has `email_verified` but better-auth expects `emailVerified`.

**Symptoms**: Session reads fail, user data missing fields.

**Solution**: Use `CamelCasePlugin` with Kysely or configure Drizzle properly:

**With Kysely**:
```typescript
import { CamelCasePlugin } from "kysely";

new Kysely({
  dialect: new D1Dialect({ database: env.DB }),
  plugins: [new CamelCasePlugin()], // Converts between naming conventions
})
```

**With Drizzle**: Define schema with camelCase from the start (as shown in examples).

---

### Issue 4: D1 Eventual Consistency

**Problem**: Session reads immediately after write return stale data.

**Symptoms**: User logs in but `getSession()` returns null on next request.

**Solution**: Use Cloudflare KV for session storage (strong consistency):

```typescript
import { betterAuth } from "better-auth";

export function createAuth(db: Database, env: Env) {
  return betterAuth({
    database: drizzleAdapter(db, { provider: "sqlite" }),
    session: {
      storage: {
        get: async (sessionId) => {
          const session = await env.SESSIONS_KV.get(sessionId);
          return session ? JSON.parse(session) : null;
        },
        set: async (sessionId, session, ttl) => {
          await env.SESSIONS_KV.put(sessionId, JSON.stringify(session), {
            expirationTtl: ttl,
          });
        },
        delete: async (sessionId) => {
          await env.SESSIONS_KV.delete(sessionId);
        },
      },
    },
  });
}
```

**Add to `wrangler.toml`**:
```toml
[[kv_namespaces]]
binding = "SESSIONS_KV"
id = "your-kv-namespace-id"
```

---

### Issue 5: CORS Errors for SPA Applications

**Problem**: CORS errors when auth API is on different origin than frontend.

**Symptoms**: `Access-Control-Allow-Origin` errors in browser console.

**Solution**: Configure CORS headers in Worker:

```typescript
import { cors } from "hono/cors";

app.use(
  "/api/auth/*",
  cors({
    origin: ["https://yourdomain.com", "http://localhost:3000"],
    credentials: true, // Allow cookies
    allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
  })
);
```

---

### Issue 6: OAuth Redirect URI Mismatch

**Problem**: Social sign-in fails with "redirect_uri_mismatch" error.

**Symptoms**: Google/GitHub OAuth returns error after user consent.

**Solution**: Ensure exact match in OAuth provider settings:

```
Provider setting: https://yourdomain.com/api/auth/callback/google
better-auth URL:  https://yourdomain.com/api/auth/callback/google

❌ Wrong: http vs https, trailing slash, subdomain mismatch
✅ Right: Exact character-for-character match
```

**Check better-auth callback URL**:
```typescript
// It's always: {baseURL}/api/auth/callback/{provider}
const callbackURL = `${env.BETTER_AUTH_URL}/api/auth/callback/google`;
console.log("Configure this URL in Google Console:", callbackURL);
```

---

### Issue 7: Missing Dependencies

**Problem**: TypeScript errors or runtime errors about missing packages.

**Symptoms**: `Cannot find module 'drizzle-orm'` or similar.

**Solution**: Install all required packages:

**For Drizzle approach**:
```bash
npm install better-auth drizzle-orm drizzle-kit @cloudflare/workers-types
```

**For Kysely approach**:
```bash
npm install better-auth kysely kysely-d1 @cloudflare/workers-types
```

---

### Issue 8: Email Verification Not Sending

**Problem**: Email verification links never arrive.

**Symptoms**: User signs up, but no email received.

**Solution**: Implement `sendVerificationEmail` handler:

```typescript
export const auth = betterAuth({
  database: /* ... */,
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },
  emailVerification: {
    sendVerificationEmail: async ({ user, url }) => {
      // Use your email service (SendGrid, Resend, etc.)
      await sendEmail({
        to: user.email,
        subject: "Verify your email",
        html: `
          <p>Click the link below to verify your email:</p>
          <a href="${url}">Verify Email</a>
        `,
      });
    },
    sendOnSignUp: true,
    autoSignInAfterVerification: true,
    expiresIn: 3600, // 1 hour
  },
});
```

**For Cloudflare**: Use Cloudflare Email Routing or external service (Resend, SendGrid).

---

### Issue 9: Session Expires Too Quickly

**Problem**: Session expires unexpectedly or never expires.

**Symptoms**: User logged out unexpectedly or session persists after logout.

**Solution**: Configure session expiration:

```typescript
export const auth = betterAuth({
  database: /* ... */,
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days (in seconds)
    updateAge: 60 * 60 * 24, // Update session every 24 hours
  },
});
```

---

### Issue 10: Social Provider Missing User Data

**Problem**: Social sign-in succeeds but missing user data (name, avatar).

**Symptoms**: `session.user.name` is null after Google/GitHub sign-in.

**Solution**: Request additional scopes:

```typescript
socialProviders: {
  google: {
    clientId: env.GOOGLE_CLIENT_ID,
    clientSecret: env.GOOGLE_CLIENT_SECRET,
    scope: ["openid", "email", "profile"], // Include 'profile' for name/image
  },
  github: {
    clientId: env.GITHUB_CLIENT_ID,
    clientSecret: env.GITHUB_CLIENT_SECRET,
    scope: ["user:email", "read:user"], // 'read:user' for full profile
  },
}
```

---

### Issue 11: TypeScript Errors with Drizzle Schema

**Problem**: TypeScript complains about schema types.

**Symptoms**: `Type 'DrizzleD1Database' is not assignable to...`

**Solution**: Export proper types from database:

```typescript
// src/db/index.ts
import { drizzle, type DrizzleD1Database } from "drizzle-orm/d1";
import * as schema from "./schema";

export type Database = DrizzleD1Database<typeof schema>;

export function createDatabase(d1: D1Database): Database {
  return drizzle(d1, { schema });
}
```

---

### Issue 12: Wrangler Dev Mode Not Working

**Problem**: `wrangler dev` fails with database errors.

**Symptoms**: "Database not found" or migration errors in local dev.

**Solution**: Apply migrations locally first:

```bash
# Apply migrations to local D1
wrangler d1 migrations apply my-app-db --local

# Then run dev server
wrangler dev
```

---

### Issue 13: User Data Updates Not Reflecting in UI (with TanStack Query)

**Problem**: After updating user data (e.g., avatar, name), changes don't appear in `useSession()` despite calling `queryClient.invalidateQueries()`.

**Symptoms**: Avatar image or user profile data appears stale after successful update. TanStack Query cache shows updated data, but better-auth session still shows old values.

**Root Cause**: better-auth uses **nanostores** for session state management, not TanStack Query. Calling `queryClient.invalidateQueries()` only invalidates React Query cache, not the better-auth nanostore.

**Solution**: Manually notify the nanostore after updating user data:

```typescript
// Update user data
const { data, error } = await authClient.updateUser({
  image: newAvatarUrl,
  name: newName
})

if (!error) {
  // Manually invalidate better-auth session state
  authClient.$store.notify('$sessionSignal')

  // Optional: Also invalidate React Query if using it for other data
  queryClient.invalidateQueries({ queryKey: ['user-profile'] })
}
```

**When to use**:
- Using better-auth + TanStack Query together
- Updating user profile fields (name, image, email)
- Any operation that modifies session user data client-side

**Alternative**: Call `refetch()` from `useSession()`, but `$store.notify()` is more direct:

```typescript
const { data: session, refetch } = authClient.useSession()
// After update
await refetch()
```

**Note**: `$store` is an undocumented internal API. This pattern is production-validated but may change in future better-auth versions.

**Source**: Community-discovered pattern, production use verified

---

## Migration Guides

### From Clerk

**Key differences**:
- Clerk: Third-party service → better-auth: Self-hosted
- Clerk: Proprietary → better-auth: Open source
- Clerk: Monthly cost → better-auth: Free

**Migration steps**:

1. **Export user data** from Clerk (CSV or API)
2. **Import into better-auth database**:
   ```typescript
   // migration script
   const clerkUsers = await fetchClerkUsers();

   for (const clerkUser of clerkUsers) {
     await db.insert(user).values({
       id: clerkUser.id,
       email: clerkUser.email,
       emailVerified: clerkUser.email_verified,
       name: clerkUser.first_name + " " + clerkUser.last_name,
       image: clerkUser.profile_image_url,
     });
   }
   ```
3. **Replace Clerk SDK** with better-auth client:
   ```typescript
   // Before (Clerk)
   import { useUser } from "@clerk/nextjs";
   const { user } = useUser();

   // After (better-auth)
   import { authClient } from "@/lib/auth-client";
   const { data: session } = authClient.useSession();
   const user = session?.user;
   ```
4. **Update middleware** for session verification
5. **Configure social providers** (same OAuth apps, different config)

---

### From Auth.js (NextAuth)

**Key differences**:
- Auth.js: Limited features → better-auth: Comprehensive (2FA, orgs, etc.)
- Auth.js: Callbacks-heavy → better-auth: Plugin-based
- Auth.js: Session handling varies → better-auth: Consistent

**Migration steps**:

1. **Database schema**: Auth.js and better-auth use similar schemas, but column names differ
2. **Replace configuration**:
   ```typescript
   // Before (Auth.js)
   import NextAuth from "next-auth";
   import GoogleProvider from "next-auth/providers/google";

   export default NextAuth({
     providers: [GoogleProvider({ /* ... */ })],
   });

   // After (better-auth)
   import { betterAuth } from "better-auth";

   export const auth = betterAuth({
     socialProviders: {
       google: { /* ... */ },
     },
   });
   ```
3. **Update client hooks**:
   ```typescript
   // Before
   import { useSession } from "next-auth/react";

   // After
   import { authClient } from "@/lib/auth-client";
   const { data: session } = authClient.useSession();
   ```

---

## Additional Resources

### Official Documentation

- **Homepage**: https://better-auth.com
- **Introduction**: https://www.better-auth.com/docs/introduction
- **Installation**: https://www.better-auth.com/docs/installation
- **Basic Usage**: https://www.better-auth.com/docs/basic-usage

### Core Concepts

- **Session Management**: https://www.better-auth.com/docs/concepts/session-management
- **Users & Accounts**: https://www.better-auth.com/docs/concepts/users-accounts
- **Client SDK**: https://www.better-auth.com/docs/concepts/client
- **Plugins System**: https://www.better-auth.com/docs/concepts/plugins

### Authentication Methods

- **Email & Password**: https://www.better-auth.com/docs/authentication/email-password
- **OAuth Providers**: https://www.better-auth.com/docs/concepts/oauth

### Plugin Documentation

**Core Plugins**:
- **2FA (Two-Factor)**: https://www.better-auth.com/docs/plugins/2fa
- **Organization**: https://www.better-auth.com/docs/plugins/organization
- **Admin**: https://www.better-auth.com/docs/plugins/admin
- **Multi-Session**: https://www.better-auth.com/docs/plugins/multi-session
- **API Key**: https://www.better-auth.com/docs/plugins/api-key
- **Generic OAuth**: https://www.better-auth.com/docs/plugins/generic-oauth

**Passwordless Plugins**:
- **Passkey**: https://www.better-auth.com/docs/plugins/passkey
- **Magic Link**: https://www.better-auth.com/docs/plugins/magic-link
- **Email OTP**: https://www.better-auth.com/docs/plugins/email-otp
- **Phone Number**: https://www.better-auth.com/docs/plugins/phone-number
- **Anonymous**: https://www.better-auth.com/docs/plugins/anonymous

**Advanced Plugins**:
- **Username**: https://www.better-auth.com/docs/plugins/username
- **JWT**: https://www.better-auth.com/docs/plugins/jwt
- **OpenAPI**: https://www.better-auth.com/docs/plugins/open-api
- **OIDC Provider**: https://www.better-auth.com/docs/plugins/oidc-provider
- **SSO**: https://www.better-auth.com/docs/plugins/sso
- **Stripe**: https://www.better-auth.com/docs/plugins/stripe
- **MCP**: https://www.better-auth.com/docs/plugins/mcp

### Framework Integrations

- **TanStack Start**: https://www.better-auth.com/docs/integrations/tanstack
- **Expo (React Native)**: https://www.better-auth.com/docs/integrations/expo

### Community & Support

- **GitHub**: https://github.com/better-auth/better-auth (22.4k ⭐)
- **Examples**: https://github.com/better-auth/better-auth/tree/main/examples
- **Discord**: https://discord.gg/better-auth
- **Changelog**: https://github.com/better-auth/better-auth/releases

### Related Documentation

- **Drizzle ORM**: https://orm.drizzle.team/docs/get-started-sqlite
- **Kysely**: https://kysely.dev/

---

## Production Examples

**Verified working D1 repositories** (all use Drizzle or Kysely):

1. **zpg6/better-auth-cloudflare** - Drizzle + D1 (includes CLI)
2. **zwily/example-react-router-cloudflare-d1-drizzle-better-auth** - Drizzle + D1
3. **foxlau/react-router-v7-better-auth** - Drizzle + D1
4. **matthewlynch/better-auth-react-router-cloudflare-d1** - Kysely + D1

**None** use a direct `d1Adapter` - all require Drizzle/Kysely.

---

## Version Compatibility

**Tested with**:
- `better-auth@1.3.34`
- `drizzle-orm@0.44.7`
- `drizzle-kit@0.31.6`
- `kysely@0.28.8`
- `kysely-d1@0.4.0`
- `@cloudflare/workers-types@latest`
- `hono@4.0.0`
- Node.js 18+, Bun 1.0+

**Breaking changes**: Check changelog when upgrading: https://github.com/better-auth/better-auth/releases

---

**Token Efficiency**:
- **Without skill**: ~28,000 tokens (D1 adapter errors, TanStack Start cookies, nanostore invalidation, OAuth flows, API discovery)
- **With skill**: ~5,600 tokens (focused on errors + breaking changes + API reference)
- **Savings**: ~80% (~22,400 tokens)

**Errors prevented**: 13 documented issues with exact solutions
**Key value**: D1 adapter requirement, v1.4.0/v1.3 breaking changes, TanStack Start fix, nanostore pattern, 80+ endpoint reference

---

**Last verified**: 2025-11-22 | **Skill version**: 3.0.0 | **Changes**: Added v1.4.0 (ESM-only, stateless sessions, SCIM) and v1.3 (SSO/SAML, multi-team) knowledge gaps. Removed tutorial/setup (~700 lines). Focused on error prevention + breaking changes + API reference.

Related Skills

clerk-auth

31
from ovachiever/droid-tings

Clerk auth with API version 2025-11-10 breaking changes (billing endpoints, payment_source→payment_method), Next.js v6 async auth(), PKCE for custom OAuth, credential stuffing defense. Use when: troubleshooting "Missing Clerk Secret Key", JWKS errors, authorizedParties CSRF, JWT size limits (1.2KB), 431 header errors (Vite dev mode), or testing with 424242 OTP.

auth-implementation-patterns

31
from ovachiever/droid-tings

Master authentication and authorization patterns including JWT, OAuth2, session management, and RBAC to build secure, scalable access control systems. Use when implementing auth systems, securing APIs, or debugging security issues.

zustand-state-management

31
from ovachiever/droid-tings

Build type-safe global state in React applications with Zustand. Supports TypeScript, persist middleware, devtools, slices pattern, and Next.js SSR. Use when setting up React state, migrating from Redux/Context API, implementing localStorage persistence, or troubleshooting Next.js hydration errors, TypeScript inference issues, or infinite render loops.

zinc-database

31
from ovachiever/droid-tings

Access ZINC (230M+ purchasable compounds). Search by ZINC ID/SMILES, similarity searches, 3D-ready structures for docking, analog discovery, for virtual screening and drug discovery.

zarr-python

31
from ovachiever/droid-tings

Chunked N-D arrays for cloud storage. Compressed arrays, parallel I/O, S3/GCS integration, NumPy/Dask/Xarray compatible, for large-scale scientific computing pipelines.

youtube-transcript

31
from ovachiever/droid-tings

Download YouTube video transcripts when user provides a YouTube URL or asks to download/get/fetch a transcript from YouTube. Also use when user wants to transcribe or get captions/subtitles from a YouTube video.

xlsx

31
from ovachiever/droid-tings

Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas

wordpress-plugin-core

31
from ovachiever/droid-tings

Build secure WordPress plugins with core patterns for hooks, database interactions, Settings API, custom post types, REST API, and AJAX. Covers three architecture patterns (Simple, OOP, PSR-4) and the Security Trinity. Use when creating plugins, implementing nonces/sanitization/escaping, working with $wpdb prepared statements, or troubleshooting SQL injection, XSS, CSRF vulnerabilities, or plugin activation errors.

whisper

31
from ovachiever/droid-tings

OpenAI's general-purpose speech recognition model. Supports 99 languages, transcription, translation to English, and language identification. Six model sizes from tiny (39M params) to large (1550M params). Use for speech-to-text, podcast transcription, or multilingual audio processing. Best for robust, multilingual ASR.

weights-and-biases

31
from ovachiever/droid-tings

Track ML experiments with automatic logging, visualize training in real-time, optimize hyperparameters with sweeps, and manage model registry with W&B - collaborative MLOps platform

webapp-testing

31
from ovachiever/droid-tings

Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.

serving-llms-vllm

31
from ovachiever/droid-tings

Serves LLMs with high throughput using vLLM's PagedAttention and continuous batching. Use when deploying production LLM APIs, optimizing inference latency/throughput, or serving models with limited GPU memory. Supports OpenAI-compatible endpoints, quantization (GPTQ/AWQ/FP8), and tensor parallelism.