better-auth authentication for Cloudflare D1 (primary), Next.js, Nuxt, Express, and 15+ frameworks via Drizzle ORM/Kysely. Covers 45+ OAuth providers, 2FA, passkeys, organizations, RBAC. Use for self-hosted auth or encountering D1 adapter, schema, session, CORS, OAuth errors.
Implements better-auth authentication for Cloudflare D1 using Drizzle ORM or Kysely. Use when setting up auth with D1, configuring OAuth providers, or fixing adapter/schema errors.
/plugin marketplace add secondsky/claude-skills/plugin install better-auth@claude-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/auth-flow-diagram.mdreferences/advanced-features.mdreferences/cloudflare-worker-drizzle.tsreferences/cloudflare-worker-kysely.tsreferences/configuration-guide.mdreferences/database-schema.tsreferences/databases/mongodb.mdreferences/databases/mysql.mdreferences/databases/postgresql.mdreferences/error-catalog.mdreferences/framework-comparison.mdreferences/frameworks/api-frameworks.mdreferences/frameworks/expo-mobile.mdreferences/frameworks/nextjs.mdreferences/frameworks/nuxt.mdreferences/frameworks/remix.mdreferences/frameworks/sveltekit.mdreferences/migration-guide-1.4.0.mdreferences/nextjs/README.mdreferences/nextjs/postgres-example.tsStatus: Production Ready
Last Updated: 2025-12-26
Package: better-auth@1.4.9 (ESM-only)
Dependencies: Drizzle ORM or Kysely (required for D1)
Option 1: Drizzle ORM (Recommended)
bun add better-auth drizzle-orm drizzle-kit
Option 2: Kysely
bun add better-auth kysely @noxharmonium/kysely-d1
better-auth v1.4.0+ is ESM-only. Ensure:
package.json:
{
"type": "module"
}
Upgrading from v1.3.x? Load references/migration-guide-1.4.0.md
better-auth DOES NOT have a direct d1Adapter(). You MUST use either:
drizzleAdapter()// ❌ WRONG - This doesn't exist
import { d1Adapter } from 'better-auth/adapters/d1'
// ✅ CORRECT - Use Drizzle
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { drizzle } from 'drizzle-orm/d1'
1. Create D1 Database:
wrangler d1 create my-app-db
2. Define Schema (src/db/schema.ts):
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const user = sqliteTable("user", {
id: text().primaryKey(),
name: text().notNull(),
email: text().notNull().unique(),
emailVerified: integer({ mode: "boolean" }).notNull().default(false),
image: text(),
});
export const session = sqliteTable("session", {
id: text().primaryKey(),
userId: text().notNull().references(() => user.id, { onDelete: "cascade" }),
token: text().notNull(),
expiresAt: integer({ mode: "timestamp" }).notNull(),
});
// See references/database-schema.ts for complete schema
3. Initialize Auth (src/auth.ts):
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { drizzle } from "drizzle-orm/d1";
import * as schema from "./db/schema";
export function createAuth(env: { DB: D1Database; BETTER_AUTH_SECRET: string }) {
const db = drizzle(env.DB, { schema });
return betterAuth({
baseURL: env.BETTER_AUTH_URL,
secret: env.BETTER_AUTH_SECRET,
database: drizzleAdapter(db, { provider: "sqlite" }),
emailAndPassword: { enabled: true },
});
}
4. Create Worker (src/index.ts):
import { Hono } from "hono";
import { createAuth } from "./auth";
const app = new Hono<{ Bindings: Env }>();
app.all("/api/auth/*", async (c) => {
const auth = createAuth(c.env);
return auth.handler(c.req.raw);
});
export default app;
5. Deploy:
bunx drizzle-kit generate
wrangler d1 migrations apply my-app-db --remote
wrangler deploy
✅ Use Drizzle/Kysely adapter (d1Adapter doesn't exist)
✅ Use Drizzle Kit for migrations (not better-auth migrate)
✅ Set BETTER_AUTH_SECRET via wrangler secret put
✅ Configure CORS with credentials: true
✅ Match OAuth callback URLs exactly (no trailing slash)
✅ Apply migrations to local D1 before wrangler dev
✅ Use camelCase column names in schema
❌ Use d1Adapter or better-auth migrate with D1
❌ Forget CORS credentials or mismatch OAuth URLs
❌ Use snake_case columns without CamelCasePlugin
❌ Skip local migrations or hardcode secrets
❌ Leave sendVerificationEmail unimplemented
ESM-only (no CommonJS):
// package.json required
{ "type": "module" }
API Renames:
forgetPassword → requestPasswordReset/account-info → GET /account-infoCallback Signatures:
// v1.3.x: request parameter
sendVerificationEmail: async ({ user, url, request }) => {}
// v1.4.0+: ctx parameter
sendVerificationEmail: async ({ user, url, ctx }) => {}
Load references/migration-guide-1.4.0.md when upgrading from <1.4.0
Key additions since v1.4.3:
backgroundTasks config to defer email sendingbetter-auth CLI with project scaffoldingLoad references/v1.4-features.md for detailed implementation guides.
Problem: Trying to use non-existent d1Adapter
Solution: Use drizzleAdapter or Kysely instead (see Quick Start above)
Problem: better-auth migrate doesn't work with D1
Solution: Use bunx drizzle-kit generate then wrangler d1 migrations apply
Problem: Database uses email_verified but better-auth expects emailVerified
Solution: Use camelCase in schema or add CamelCasePlugin to Kysely
Problem: Access-Control-Allow-Origin errors, cookies not sent
Solution: Configure CORS with credentials: true and correct origins
Problem: Social sign-in fails with "redirect_uri_mismatch"
Solution: Ensure exact match: https://yourdomain.com/api/auth/callback/google
Load references/error-catalog.md for all 15 errors with detailed solutions.
When: Basic authentication without social providers Quick Pattern:
// Client
await authClient.signIn.email({
email: "user@example.com",
password: "password123",
});
// Server - enable in config
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
}
Load: references/setup-guide.md → Step 5
When: Allow users to sign in with social accounts Supported: Google, GitHub, Microsoft, Apple, Discord, TikTok, Twitch, Spotify, LinkedIn, Slack, Reddit, Facebook, Twitter/X, Patreon, Vercel, Kick, and 30+ more. Quick Pattern:
// Client
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
// Server config
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
scope: ["openid", "email", "profile"],
},
}
Load: references/setup-guide.md → Step 5
When: Need to verify user is authenticated Quick Pattern:
app.get("/api/protected", async (c) => {
const auth = createAuth(c.env);
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
return c.json({ error: "Unauthorized" }, 401);
}
return c.json({ data: "protected", user: session.user });
});
Load: references/cloudflare-worker-drizzle.ts
When: Building SaaS with teams/organizations
Load: references/advanced-features.md → Organizations & Teams
When: Need extra security with 2FA/TOTP
Load: references/advanced-features.md → Two-Factor Authentication
Load references/setup-guide.md when:
Load references/error-catalog.md when:
Load references/advanced-features.md when:
Load references/cloudflare-worker-drizzle.ts when:
Load references/cloudflare-worker-kysely.ts when:
Load references/database-schema.ts when:
Load references/react-client-hooks.tsx when:
Load references/configuration-guide.md when:
Load references/framework-comparison.md when:
Load references/migration-guide-1.4.0.md when:
forgetPassword errors or ESM issuesLoad references/v1.4-features.md when:
Load references/nextjs/README.md when:
Load references/nextjs/postgres-example.ts when:
Load references/frameworks/nextjs.md when:
Load references/frameworks/nuxt.md when:
Load references/frameworks/remix.md when:
Load references/frameworks/sveltekit.md when:
Load references/frameworks/api-frameworks.md when:
Load references/frameworks/expo-mobile.md when:
Load references/databases/postgresql.md when:
Load references/databases/mongodb.md when:
Load references/databases/mysql.md when:
Load references/plugins/authentication.md when:
Load references/plugins/enterprise.md when:
Load references/plugins/api-tokens.md when:
Load references/plugins/payments.md when:
Quick Config (ESM-only in v1.4.0+):
export const auth = betterAuth({
baseURL: env.BETTER_AUTH_URL,
secret: env.BETTER_AUTH_SECRET,
database: drizzleAdapter(db, { provider: "sqlite" }),
});
Load references/configuration-guide.md for:
Create auth client (src/lib/auth-client.ts):
import { createAuthClient } from "better-auth/client";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_API_URL || "http://localhost:8787",
});
Use in React:
import { authClient } from "@/lib/auth-client";
export function UserProfile() {
const { data: session, isPending } = authClient.useSession();
if (isPending) return <div>Loading...</div>;
if (!session) return <div>Not authenticated</div>;
return (
<div>
<p>Welcome, {session.user.email}</p>
<button onClick={() => authClient.signOut()}>Sign Out</button>
</div>
);
}
Required:
better-auth@^1.4.9 - Core authentication framework (ESM-only)Choose ONE adapter:
drizzle-orm@^0.44.7 + drizzle-kit@^0.31.7 (recommended)kysely@^0.28.8 + @noxharmonium/kysely-d1@^0.4.0 (alternative)Optional:
@cloudflare/workers-types - TypeScript types for Workershono@^4.0.0 - Web framework for routing@better-auth/passkey - Passkey plugin (v1.4.0+, separate package)@better-auth/api-key - API key auth (v1.4.0+)This skill focuses on Cloudflare Workers + D1. better-auth also supports:
Frameworks (18 total): Next.js, Nuxt, Remix, SvelteKit, Astro, Express, NestJS, Fastify, Elysia, Expo, and more.
Databases (9 adapters): PostgreSQL, MongoDB, MySQL, Prisma, MS SQL, and others.
Additional Plugins: Anonymous auth, Email OTP, JWT, Multi-Session, OIDC Provider, payment integrations (Stripe, Polar).
For non-Cloudflare setups, load the appropriate framework or database reference file, or consult the official docs: https://better-auth.com/docs
Load references/framework-comparison.md for:
Verified working repositories (all use Drizzle or Kysely):
Note: Check each repo's better-auth version. Repos on v1.3.x need v1.4.0+ migration (see references/migration-guide-1.4.0.md). None use a direct d1Adapter - all require Drizzle/Kysely.
"type": "module" in package.json) - v1.4.0+ requiredQuestions? Issues?
references/error-catalog.md for all 15 errors and solutionsreferences/setup-guide.md for complete 8-step setupreferences/advanced-features.md for 2FA, organizations, and moreUse when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.