101 added, 234 removed. Audit A to A.
---
name: api-auth-better-auth-drizzle-hono
description: Better Auth patterns, sessions, OAuth
---
# Authentication with Better Auth
- > **Quick Guide:** Use Better Auth (v1.4+) for type-safe, self-hosted authentication in TypeScript apps. It provides email/password, OAuth, 2FA, sessions, stateless auth, and organization multi-tenancy out of the box. Integrates seamlessly with Hono and Drizzle ORM. Auth.js is now maintained by the Better Auth team.
+ > **Quick Guide:** Use Better Auth (v1.5+) for type-safe, self-hosted authentication in TypeScript apps. It provides email/password, OAuth, 2FA, sessions, stateless auth, and organization multi-tenancy. Plugin architecture enables progressive complexity. Mount auth handler before session-dependent middleware, configure CORS first for cross-origin deployments, and always run schema generation after adding plugins.
---
<critical_requirements>
## CRITICAL: Before Using This Skill
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST mount Better Auth handler on `/api/auth/*` BEFORE any other middleware that depends on session)**
**(You MUST configure CORS middleware BEFORE auth routes when client and server are on different origins)**
**(You MUST use environment variables for ALL secrets (clientId, clientSecret, BETTER_AUTH_SECRET) - NEVER hardcode)**
- **(You MUST run `npx @better-auth/cli generate` then `npx drizzle-kit generate` and `npx drizzle-kit migrate` after adding plugins - `migrate` only works with Kysely adapter)**
+ **(You MUST run `npx auth@latest generate` then your ORM migration tool after adding plugins)**
**(You MUST use `auth.$Infer.Session` types for type-safe session access in middleware)**
- **(You MUST use `authClient.requestPasswordReset` - `authClient.forgotPassword` was renamed in v1.4)**
-
</critical_requirements>
---
- **Auto-detection:** Better Auth, betterAuth, createAuthClient, auth.handler, auth.api.getSession, socialProviders, twoFactor plugin, organization plugin, drizzleAdapter, session management, OAuth providers, stateless sessions, cookieCache, genericOAuth, passkey, SCIM
+ **Auto-detection:** Better Auth, betterAuth, createAuthClient, auth.handler, auth.api.getSession, socialProviders, twoFactor plugin, organization plugin, drizzleAdapter, session management, OAuth providers, stateless sessions, cookieCache, genericOAuth, oAuthProvider, passkey, SCIM
**When to use:**
- Building self-hosted authentication (no vendor lock-in)
- Need email/password + OAuth + 2FA in one solution
- Multi-tenant SaaS with organization/team management
- - Type-safe session management with Hono
- - Projects requiring database-stored sessions
+ - Type-safe session management
+ - Projects requiring database-stored or stateless sessions
**When NOT to use:**
- - Serverless with strict cold start requirements (consider Clerk/Auth0)
- - Need managed authentication with zero setup (consider Clerk)
+ - Need managed authentication with zero maintenance (consider hosted auth solutions)
- Simple static sites without user accounts
- - Mobile-only apps (consider Firebase Auth)
-
- **Note:** Auth.js/NextAuth is now maintained by the Better Auth team - consider Better Auth for new projects.
+ - Projects where serverless cold starts are critical (though stateless mode helps)
**Key patterns covered:**
- Server configuration (auth.ts) with plugins
- - Hono integration with session middleware
+ - Session middleware and type-safe route protection
- Email/password authentication flows
- - OAuth providers (GitHub, Google, etc.) and Generic OAuth plugin
+ - OAuth providers (GitHub, Google, Generic OAuth)
- Two-factor authentication (TOTP)
- Organization and multi-tenancy
- - Session management, cookie caching, and stateless sessions
- - Drizzle ORM database adapter
- - Client-side React integration with useSession
- - Performance optimization with cookieCache and experimental joins
- - Passkey authentication (separate package: @better-auth/passkey)
+ - Session strategies: database, cookie cache, stateless
+ - Database adapter integration
+ - Client-side useSession hook
+ - Performance: experimental joins, cookie caching, stateless sessions
**Detailed Resources:**
- - For code examples, see [examples/](examples/) folder:
- - [core.md](examples/core.md) - Sign up, sign in, client setup, Drizzle adapter
- - [oauth.md](examples/oauth.md) - GitHub, Google OAuth providers, Generic OAuth plugin
- - [two-factor.md](examples/two-factor.md) - TOTP setup, enable, verify
- - [organizations.md](examples/organizations.md) - Multi-tenancy, invitations
- - [sessions.md](examples/sessions.md) - Session config, cookie caching, stateless sessions
- - [v1.4-features.md](examples/v1.4-features.md) - Stateless auth, performance, Generic OAuth
- - For decision frameworks, anti-patterns, and red flags, see [reference.md](reference.md)
+ - [examples/core.md](examples/core.md) - Sign up, sign in, client setup, database adapter
+ - [examples/oauth.md](examples/oauth.md) - GitHub, Google, Generic OAuth providers
+ - [examples/two-factor.md](examples/two-factor.md) - TOTP setup, enable, verify
+ - [examples/organizations.md](examples/organizations.md) - Multi-tenancy, invitations
+ - [examples/sessions.md](examples/sessions.md) - Session config, cookie caching, stateless
+ - [reference.md](reference.md) - Decision frameworks, anti-patterns, red flags
---
<philosophy>
## Philosophy
Better Auth follows a **TypeScript-first, self-hosted** approach to authentication. Your user data stays in your database, with no vendor lock-in. The plugin architecture enables progressive complexity - start simple and add features as needed.
- **Note on Auth.js:** Auth.js (formerly NextAuth.js) is now maintained and overseen by the Better Auth team. Better Auth is the recommended path forward for new TypeScript projects.
-
**Core principles:**
- 1. **Type safety throughout** - Session types flow from server to client
- 2. **Database as source of truth** - Sessions stored in your DB, not JWTs only (with optional stateless mode in v1.4+)
- 3. **Plugin-based extensibility** - Add 2FA, organizations, passkeys, SCIM when needed
- 4. **Framework-agnostic** - Works with Hono, Next.js, SvelteKit, Remix, etc.
+ 1. **Type safety throughout** - Session types flow from server to client via `auth.$Infer.Session`
+ 2. **Database as source of truth** - Sessions stored in your DB (with optional stateless mode)
+ 3. **Plugin-based extensibility** - Add 2FA, organizations, passkeys, SCIM, OAuth provider when needed
+ 4. **Framework-agnostic** - Works with any TypeScript web framework
5. **Performance-focused** - Experimental joins (2-3x faster), cookie caching, stateless sessions
- **When to use Better Auth:**
-
- - Self-hosted authentication with full control
- - Multi-tenant SaaS with organizations/teams
- - Need 2FA, passkeys, or enterprise SSO (SCIM)
- - TypeScript projects requiring type-safe auth
- - High-performance auth with database optimization
-
- **When NOT to use:**
-
- - Need managed auth with zero maintenance
- - Serverless with aggressive cold start budgets (though stateless mode helps)
- - Cloudflare Workers without `nodejs_compat` flag (v1.4+ requirement)
-
</philosophy>
---
<patterns>
## Core Patterns
### Pattern 1: Server Configuration (auth.ts)
- Create the auth instance with database adapter and configuration. This is the single source of truth for authentication.
-
- #### Constants
-
- ```typescript
- // lib/auth.ts
- const SESSION_EXPIRES_IN_SECONDS = 60 * 60 * 24 * 7; // 7 days
- const SESSION_UPDATE_AGE_SECONDS = 60 * 60 * 24; // 1 day (refresh daily)
- const PASSWORD_MIN_LENGTH = 8;
- const PASSWORD_MAX_LENGTH = 128;
- ```
-
- #### Basic Setup
+ Create the auth instance with database adapter. Single source of truth for all authentication config.
```typescript
// lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
-
import { db } from "@/lib/db";
- const SESSION_EXPIRES_IN_SECONDS = 60 * 60 * 24 * 7;
- const SESSION_UPDATE_AGE_SECONDS = 60 * 60 * 24;
+ const SESSION_EXPIRES_IN_SECONDS = 60 * 60 * 24 * 7; // 7 days
+ const SESSION_UPDATE_AGE_SECONDS = 60 * 60 * 24; // Refresh daily
export const auth = betterAuth({
- database: drizzleAdapter(db, {
- provider: "pg", // or "sqlite" or "mysql"
- }),
+ database: drizzleAdapter(db, { provider: "pg" }),
emailAndPassword: {
enabled: true,
minPasswordLength: 8,
maxPasswordLength: 128,
},
session: {
expiresIn: SESSION_EXPIRES_IN_SECONDS,
updateAge: SESSION_UPDATE_AGE_SECONDS,
},
- trustedOrigins: [process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"],
+ trustedOrigins: [process.env.APP_URL || "http://localhost:3000"],
});
-
- // Named export (project convention)
- export { auth };
```
- **Why good:** Single auth instance exported for reuse, drizzleAdapter connects to existing DB, named constants make session policy clear, environment variables for URLs
+ **Why good:** Named constants make session policy auditable, env vars for URLs, single exported instance
```typescript
- // BAD Example - Anti-patterns
- import { betterAuth } from "better-auth";
-
+ // BAD: Magic numbers, hardcoded secrets, default export
const auth = betterAuth({
- database: {
- url: "postgres://user:pass@localhost:5432/db", // BAD: Hardcoded credentials
- },
- session: {
- expiresIn: 604800, // BAD: Magic number (what is this?)
- },
- trustedOrigins: ["http://localhost:3000"], // BAD: Hardcoded URL
+ database: { url: "postgres://user:pass@localhost/db" },
+ session: { expiresIn: 604800 },
});
-
- export default auth; // BAD: Default export
+ export default auth;
```
- **Why bad:** Hardcoded credentials leak in source control, magic numbers obscure session policy, hardcoded URLs break in production, default export prevents tree-shaking
+ **Why bad:** Hardcoded credentials leak in source control, magic numbers obscure policy, default export
- ---
+ See [examples/core.md](examples/core.md) for full setup with email verification and Drizzle adapter configuration.
- ### Pattern 2: Hono Integration with Session Middleware
+ ---
- Mount Better Auth handler and create middleware for session access in routes.
+ ### Pattern 2: Session Middleware with Type Safety
- #### Handler Setup
+ Mount auth handler and create typed middleware for session access in routes.
```typescript
- // app/api/[[...route]]/route.ts
- import { Hono } from "hono";
- import { cors } from "hono/cors";
- import { handle } from "hono/vercel";
-
- import { auth } from "@/lib/auth";
-
- const CORS_MAX_AGE_SECONDS = 86400;
-
- const app = new Hono().basePath("/api");
-
// CRITICAL: CORS must be configured BEFORE auth routes
- app.use(
- "/auth/*",
- cors({
- origin: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
- allowHeaders: ["Content-Type", "Authorization"],
- allowMethods: ["POST", "GET", "OPTIONS"],
- credentials: true,
- maxAge: CORS_MAX_AGE_SECONDS,
- }),
- );
-
- // Mount Better Auth handler on /api/auth/*
- app.on(["POST", "GET"], "/auth/*", (c) => {
- return auth.handler(c.req.raw);
- });
-
- // Named exports for Next.js
- export const GET = handle(app);
- export const POST = handle(app);
- export { app };
+ app.use("/auth/*", cors({ origin: APP_URL, credentials: true }));
+ app.on(["POST", "GET"], "/auth/*", (c) => auth.handler(c.req.raw));
```
- **Why good:** CORS before auth prevents preflight failures, `c.req.raw` provides Web Standard Request that Better Auth expects, named exports follow convention
-
- #### Session Middleware
-
```typescript
- // middleware/auth-middleware.ts
- import type { Context, Next } from "hono";
- import { createMiddleware } from "hono/factory";
-
- import { auth } from "@/lib/auth";
-
+ // middleware/auth-middleware.ts - Type-safe session access
type AuthVariables = {
user: typeof auth.$Infer.Session.user | null;
session: typeof auth.$Infer.Session.session | null;
};
export const authMiddleware = createMiddleware<{ Variables: AuthVariables }>(
- async (c: Context, next: Next) => {
- const session = await auth.api.getSession({
- headers: c.req.raw.headers,
- });
-
+ async (c, next) => {
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
c.set("user", session?.user ?? null);
c.set("session", session?.session ?? null);
-
await next();
},
);
-
- // Named export
- export { authMiddleware };
```
- **Why good:** Type-safe Variables with `auth.$Infer.Session` ensures c.get("user") is correctly typed, null fallback prevents undefined access
-
- #### Protected Routes
+ **Why good:** `auth.$Infer.Session` ensures `c.get("user")` is correctly typed, CORS before auth prevents preflight failures, `c.req.raw` provides the Web Standard Request that Better Auth expects
```typescript
- // routes/protected.ts
- import { OpenAPIHono, createRoute } from "@hono/zod-openapi";
-
- import { authMiddleware } from "@/middleware/auth-middleware";
- import { ErrorResponseSchema, UserSchema } from "@/schemas";
-
- const HTTP_STATUS_UNAUTHORIZED = 401;
-
- const app = new OpenAPIHono();
-
- // Apply auth middleware globally or per-route
- app.use("*", authMiddleware);
-
- const getMeRoute = createRoute({
- method: "get",
- path: "/me",
- operationId: "getCurrentUser",
- tags: ["Auth"],
- responses: {
- 200: {
- description: "Current user",
- content: { "application/json": { schema: UserSchema } },
- },
- 401: {
- description: "Unauthorized",
- content: { "application/json": { schema: ErrorResponseSchema } },
- },
- },
+ // BAD: No type annotation - c.user is any, bypasses type system
+ app.use("*", async (c, next) => {
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
+ c.user = session?.user; // any - no autocomplete
+ await next();
});
-
- app.openapi(getMeRoute, async (c) => {
- const user = c.get("user");
+ ```
- if (!user) {
- return c.json(
- { error: "unauthorized", message: "Authentication required" },
- HTTP_STATUS_UNAUTHORIZED,
- );
- }
+ **Why bad:** No AuthVariables type = any access, direct property assignment bypasses typed Variables
- return c.json({ user }, 200);
- });
+ See [examples/core.md](examples/core.md) for protected route patterns.
- export { app as protectedRoutes };
- ```
+ ---
- **Why good:** authMiddleware sets typed user/session, null check returns proper 401, OpenAPI route documents auth requirement
+ ### Pattern 3: Schema Generation After Plugins
- ```typescript
- // BAD Example - No type safety
- app.use("*", async (c, next) => {
- const session = await auth.api.getSession({
- headers: c.req.raw.headers,
- });
+ Every plugin adds database tables. Run the CLI after adding or modifying plugins:
- // BAD: No type annotation - c.user is any
- c.user = session?.user;
+ ```bash
+ # Step 1: Generate Better Auth schema (outputs ORM-specific files)
+ npx auth@latest generate
- await next();
- });
+ # Step 2: Generate migration with your ORM tool
+ npx drizzle-kit generate
- app.get("/me", async (c) => {
- // BAD: c.user is any - no autocomplete, no type checking
- if (!c.user) {
- return c.json({ error: "Unauthorized" }, 401); // BAD: Magic number
- }
- return c.json(c.user);
- });
+ # Step 3: Apply migration
+ npx drizzle-kit migrate
```
- **Why bad:** No AuthVariables type = any access, magic 401 requires hunting for status meaning, direct c.user assignment bypasses Hono's type system
+ Always run all 3 steps. The Better Auth `migrate` command only works with the Kysely adapter - for Drizzle, use `generate` + Drizzle Kit.
---
- ### Pattern 3: Email/Password Authentication
-
- Configure email/password auth with proper password requirements and error handling.
+ ### Pattern 4: Email/Password with Verification
- #### Server Configuration
+ Configure email/password auth with verification and password reset callbacks.
```typescript
- // lib/auth.ts
- import { betterAuth } from "better-auth";
- import { drizzleAdapter } from "better-auth/adapters/drizzle";
-
- import { db } from "@/lib/db";
- import { sendEmail } from "@/lib/email";
-
- const PASSWORD_RESET_EXPIRY_SECONDS = 3600; // 1 hour
-
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg" }),
emailAndPassword: {
enabled: true,
minPasswordLength: 8,
maxPasswordLength: 128,
requireEmailVerification: true,
sendResetPassword: async ({ user, url }) => {
await sendEmail({
to: user.email,
- subject: "Reset your password",
- html: `<a href="${url}">Click here to reset your password</a>`,
+ subject: "Reset password",
+ html: `<a href="${url}">Reset</a>`,
});
},
},
emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
await sendEmail({
to: user.email,
- subject: "Verify your email",
- html: `<a href="${url}">Click here to verify your email</a>`,
+ subject: "Verify email",
+ html: `<a href="${url}">Verify</a>`,
});
},
},
});
+ ```
- export { auth };
+ **Why good:** Email verification prevents fake signups, password requirements enforced server-side
+
+ See [examples/core.md](examples/core.md) for client-side sign up/in hooks with error handling.
+
+ ---
+
+ ### Pattern 5: Session Strategies
+
+ Three session approaches with different trade-offs:
+
+ | Strategy | DB Required | Revocable | Best For |
+ | ------------------ | ----------- | ----------------- | --------------- |
+ | Database (default) | Yes | Yes | Most apps |
+ | Cookie cache + DB | Yes | Yes (delayed) | Reduce DB load |
+ | Stateless | No | No (version-only) | Edge/serverless |
+
+ ```typescript
+ // Cookie cache: reduces DB hits by caching session in signed cookie
+ session: {
+ cookieCache: { enabled: true, maxAge: CACHE_SECONDS, strategy: "compact" },
+ }
+
+ // Stateless: omit database option entirely
+ const auth = betterAuth({
+ // No database = fully stateless
+ session: { cookieCache: { enabled: true, strategy: "jwe" } },
+ });
```
- **Why good:** Email verification prevents fake signups, custom email sender integrates with your email service, password requirements enforced server-side
+ Cookie cache strategies: `compact` (smallest, internal), `jwt` (standard, third-party verifiable), `jwe` (encrypted, hides data).
+ See [examples/sessions.md](examples/sessions.md) for full configuration and revocation patterns.
+
</patterns>
---
<critical_reminders>
## CRITICAL REMINDERS
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST mount Better Auth handler on `/api/auth/*` BEFORE any other middleware that depends on session)**
**(You MUST configure CORS middleware BEFORE auth routes when client and server are on different origins)**
**(You MUST use environment variables for ALL secrets (clientId, clientSecret, BETTER_AUTH_SECRET) - NEVER hardcode)**
- **(You MUST run `npx @better-auth/cli generate` then `npx drizzle-kit generate` and `npx drizzle-kit migrate` after adding plugins - `migrate` only works with Kysely adapter)**
+ **(You MUST run `npx auth@latest generate` then your ORM migration tool after adding plugins)**
**(You MUST use `auth.$Infer.Session` types for type-safe session access in middleware)**
- **(You MUST use `authClient.requestPasswordReset` - `authClient.forgotPassword` was renamed in v1.4)**
-
**Failure to follow these rules will cause authentication failures, security vulnerabilities, or runtime errors.**
</critical_reminders>
-
- ---
-
- ## Sources
-
- - [Better Auth Official Documentation](https://www.better-auth.com/)
- - [Better Auth GitHub Repository](https://github.com/better-auth/better-auth)
- - [Hono Integration Guide](https://www.better-auth.com/docs/integrations/hono)
- - [Two-Factor Authentication Plugin](https://www.better-auth.com/docs/plugins/2fa)
- - [Organization Plugin](https://www.better-auth.com/docs/plugins/organization)
- - [Drizzle Adapter Documentation](https://www.better-auth.com/docs/adapters/drizzle)