better-auth
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.
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
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.