Requires Drizzle ORM or Kysely for D1 (no direct adapter); use drizzleAdapter() or new Kysely({ dialect: new D1Dialect() }) with nodejs_compat flag in wrangler.toml
Provides 80+ auto-generated REST endpoints covering authentication, sessions, 2FA, organizations, admin operations, and social OAuthβzero endpoint code needed
Confirm successful installation by checking the skill directory location:
.cursor/skills/better-auth
Restart Cursor to activate better-auth. Access via /better-auth in your agent's command palette.
β
Security Notice
We perform automated surface-level scans (Gen AI Scanner, Socket, Snyk) during installation. These checks detect common vulnerabilities but do not guarantee complete security. Always review skill source code and verify the publisher's reputation before production use.
Skills execute code in your environment. Always review source, verify the publisher, and test in isolation before production.
import{ betterAuth }from"better-auth";import{ Kysely, CamelCasePlugin }from"kysely";import{ D1Dialect }from"kysely-d1";typeEnv={DB: D1Database;BETTER_AUTH_SECRET:string;// ... other env vars};exportfunctioncreateAuth(env: Env){returnbetterAuth({ secret: env.BETTER_AUTH_SECRET,// Kysely with D1Dialect database:{ db:newKysely({ dialect:newD1Dialect({ database: env.DB,}), plugins:[// CRITICAL: Required if using Drizzle schema with snake_casenewCamelCasePlugin(),],}), 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:
// β WRONG - DB binding not available outside requestconst db =drizzle(env.DB,{ schema })// env.DB doesn't exist hereexportconst auth =betterAuth({ database:drizzleAdapter(db,{ provider:"sqlite"})})// β CORRECT - Create auth instance per-requestexportdefault{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.
import{ betterAuth }from"better-auth";import{ drizzleAdapter }from"better-auth/adapters/drizzle";import{ reactStartCookies }from"better-auth/react-start";exportconst 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:
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).