Claude
Skills
Sign in
Back

better-auth

Included with Lifetime
$97 forever

Self-hosted auth for TypeScript/Cloudflare Workers with social auth, 2FA, passkeys, organizations, RBAC, and 15+ plugins. Requires Drizzle ORM or Kysely for D1 (no direct adapter). Self-hosted alternative to Clerk/Auth.js. Use when: self-hosting auth on D1, building OAuth provider, multi-tenant SaaS, or troubleshooting D1 adapter errors, session caching, rate limits, Expo crashes, additionalFields bugs.

Web Devscriptsassets

What this skill does


# better-auth - D1 Adapter & Error Prevention Guide

**Package**: [email protected] (Jan 21, 2026)
**Breaking Changes**: ESM-only (v1.4.0), Admin impersonation prevention default (v1.4.6), 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.10 (Dec 31, 2025)

**Major Features:**
- **OAuth 2.1 Provider plugin** - Build your own OAuth provider (replaces MCP plugin)
- **Patreon OAuth provider** - Social sign-in with Patreon
- **Kick OAuth provider** - With refresh token support
- **Vercel OAuth provider** - Sign in with Vercel
- **Global `backgroundTasks` config** - Deferred actions for better performance
- **Form data support** - Email authentication with fetch metadata fallback
- **Stripe enhancements** - Flexible subscription lifecycle, `disableRedirect` option

**Admin Plugin Updates:**
- ⚠️ **Breaking**: Impersonation of admins disabled by default (v1.4.6)
- Support role with permission-based user updates
- Role type inference improvements

**Security Fixes:**
- SAML XML parser hardening with configurable size constraints
- SAML assertion timestamp validation with per-provider clock skew
- SSO domain-verified provider trust
- Deprecated algorithm rejection
- Line nonce enforcement

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

---

## 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.

**⚠️ Cloudflare Workers Note**: D1 database bindings are only available inside the request handler (the `fetch()` function). You cannot initialize better-auth outside the request context. Use a factory function pattern:

```typescript
// ❌ WRONG - DB binding not available outside request
const db = drizzle(env.DB, { schema }) // env.DB doesn't exist here
export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite" }) })

// ✅ CORRECT - Create auth instance per-request
export default {
  fetch(request, env, ctx) {
    const db = drizzle(env.DB, { schema })
    const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite" }) })
    return auth.handler(request)
  }
}
```

**Community Validation**: Multiple production implementations confirm this pattern (Medium, AnswerOverflow, official Hono examples).

---

## 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**.

**Session Nullability Pattern**: When using `useSession()` in TanStack Start, the session object always exists, but `session.user` and `session.session` are `null` when not logged in:

```typescript
const { data: session } = authClient.useSession()

// When NOT logged in:
console.log(session) // { user: null, session: null }
console.log(!!session) // true (unexpected!)

// Correct check:
if (session?.user) {
  // User is logged in
}
```

**Always check `session?.user` or `session?.session`, not just `session`**. This is expected behavior (session object container always exists).

**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.4+)

Better Auth provides plugins for advanced authentication features:

| Plugin | Import | Description | Docs |
|--------|--------|-------------|------|
| **OAuth 2.1 Provider** | `better-auth/plugins` | Build OAuth 2.1 provider with PKCE, JWT tokens, consent flows (replaces MCP & OIDC plugins) | [📚](https://www.better-auth.com/docs/plugins/oauth-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 with flexible lifecycle handling | [📚](https://www.better-auth.com/docs/plugins/stripe) |
| **MCP** | `better-auth/plugins` | ⚠️ **Deprecated** - Use OAuth 2.1 Provider instead | [📚](https://www.better-auth.com/docs/plugins/mcp) |
| **Expo** | `better-auth/expo` | React Native/Expo with `webBrowserOptions` and last-login-method tracking | [📚](https://www.better-auth.com/docs/integrations/expo) |

### OAuth 2.1 Provider Plugin (New in v1.4.9)

Build your own OAuth provider for MCP servers, third-party apps, or API access:

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

export const auth = betterAuth({
  plugins: [
    jwt(), // Required for token signing
    oauthProvider({
      // Token expiration (seconds)
      accessTokenExpiresIn: 3600,      // 1 h

Related in Web Dev