api-auth-better-auth-drizzle-hono
Better Auth patterns, sessions, OAuth
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-auth-better-auth-drizzle-hono/skills/api-auth-better-auth-drizzle-hono
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Authentication with Better Auth
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 the auth route 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 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)
</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, 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
- Projects requiring database-stored or stateless sessions
When NOT to use:
- Need managed authentication with zero maintenance (consider hosted auth solutions)
- Simple static sites without user accounts
- Projects where serverless cold starts are critical (though stateless mode helps)
Key patterns covered:
- Server configuration (auth.ts) with plugins
- Session middleware and type-safe route protection
- Email/password authentication flows
- OAuth providers (GitHub, Google, Generic OAuth)
- Two-factor authentication (TOTP)
- Organization and multi-tenancy
- Session strategies: database, cookie cache, stateless
- Database adapter integration
- Client-side useSession hook
- Performance: experimental joins, cookie caching, stateless sessions
Detailed Resources:
- examples/core.md - Sign up, sign in, client setup, database adapter
- examples/oauth.md - GitHub, Google, Generic OAuth providers
- examples/two-factor.md - TOTP setup, enable, verify
- examples/organizations.md - Multi-tenancy, invitations
- examples/sessions.md - Session config, cookie caching, stateless
- reference.md - Decision frameworks, anti-patterns, version notes
<red_flags>
RED FLAGS
- Hardcoded secrets (clientId/clientSecret in source) - must use environment variables
- CORS configured after auth routes - preflight requests will fail
- Missing
BETTER_AUTH_SECRETenv var - sessions will not work - No schema generation after adding plugins - database errors at runtime
- Untyped session middleware - loses TypeScript safety,
c.userbecomesany - Using
auth.migrate()with Drizzle adapter - only works with Kysely, usegenerate+ Drizzle Kit - Missing
c.req.rawwhen callingauth.handler()- must pass the raw Web Standard Request
Gotchas & Edge Cases:
- Google only issues refresh tokens on first consent - use
accessType: "offline"andprompt: "consent" - GitHub OAuth apps don't issue refresh tokens (access tokens are long-lived)
- Stateless sessions cannot be revoked individually - increment
versionto invalidate all - Cookie cache revocation is delayed until
maxAgeexpires on other devices - Session cookies need
SameSite=None+Securefor cross-domain deployments authClient.forgotPasswordwas renamed toauthClient.requestPasswordResetin v1.4InferUser/InferSessionremoved in v1.5 - use genericUserandSessiontypes frombetter-auth
See reference.md for anti-patterns with code examples, decision frameworks, and version notes.
</red_flags>
<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 the auth route 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 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)
Failure to follow these rules will cause authentication failures, security vulnerabilities, or runtime errors.
</critical_reminders>
Files (skills)
-
examples
-
core.md 8.3 KB
# Better Auth - Core Examples > Essential patterns for authentication. See [SKILL.md](../SKILL.md) for core concepts and [reference.md](../reference.md) for decision frameworks. **Additional Examples:** - [oauth.md](oauth.md) - Social providers, Generic OAuth, OAuth Provider plugin - [two-factor.md](two-factor.md) - TOTP setup and verification - [organizations.md](organizations.md) - Multi-tenancy and invitations - [sessions.md](sessions.md) - Session configuration, cookie caching, stateless --- ## Client-Side Sign Up ```typescript // hooks/use-sign-up.ts import { useState } from "react"; import { authClient } from "@/lib/auth-client"; interface SignUpData { name: string; email: string; password: string; } export function useSignUp() { const [isPending, setIsPending] = useState(false); const [error, setError] = useState<string | null>(null); const signUp = async (data: SignUpData) => { setIsPending(true); setError(null); try { const result = await authClient.signUp.email({ name: data.name, email: data.email, password: data.password, callbackURL: "/dashboard", }); if (result.error) { setError(result.error.message); return { success: false }; } return { success: true }; } catch (err) { setError("An unexpected error occurred"); return { success: false }; } finally { setIsPending(false); } }; return { signUp, isPending, error }; } ``` **Why good:** Handles loading and error states, callbackURL redirects after signup, error from Better Auth surfaced to UI --- ## Client-Side Sign In ```typescript // hooks/use-sign-in.ts import { useState } from "react"; import { authClient } from "@/lib/auth-client"; interface SignInData { email: string; password: string; rememberMe?: boolean; } export function useSignIn() { const [isPending, setIsPending] = useState(false); const [error, setError] = useState<string | null>(null); const [requires2FA, setRequires2FA] = useState(false); const signIn = async (data: SignInData) => { setIsPending(true); setError(null); try { const result = await authClient.signIn.email({ email: data.email, password: data.password, rememberMe: data.rememberMe ?? false, callbackURL: "/dashboard", }); // Handle 2FA requirement if (result.data?.twoFactorRedirect) { setRequires2FA(true); return { success: false, requires2FA: true }; } if (result.error) { setError(result.error.message); return { success: false }; } return { success: true }; } catch (err) { setError("An unexpected error occurred"); return { success: false }; } finally { setIsPending(false); } }; return { signIn, isPending, error, requires2FA }; } ``` **Why good:** rememberMe extends session duration, twoFactorRedirect flag enables 2FA flow, structured return type for component handling --- ## Drizzle Database Adapter ### Database Setup ```typescript // lib/db.ts import { drizzle } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; import * as schema from "./schema"; const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); export const db = drizzle(pool, { schema }); ``` ### Auth Configuration ```typescript // lib/auth.ts import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; // or "@better-auth/drizzle-adapter" (v1.5+) import { db } from "@/lib/db"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", // or "sqlite" or "mysql" // Optional: use your existing schema table names // schema: { // user: schema.users, // session: schema.sessions, // }, }), // Enable experimental joins for 2-3x faster queries experimental: { joins: true, }, }); ``` ### Schema Generation Commands ```bash # For Drizzle adapter, use this 3-step workflow: # Step 1: Generate Better Auth schema npx auth@latest generate # Step 2: Generate Drizzle migration file npx drizzle-kit generate # Step 3: Apply migration to database npx drizzle-kit migrate # NOTE: Better Auth's own `migrate` command only works with Kysely adapter # For Drizzle, always use the 3-step workflow above ``` **Why good:** drizzleAdapter integrates with existing Drizzle setup, experimental joins improve performance, schema customization for existing tables --- ## Client Configuration Better Auth provides framework-specific clients: `better-auth/react`, `better-auth/vue`, `better-auth/svelte`, `better-auth/solid`, and `better-auth/client` (vanilla). All share the same API -- only the import path differs. Examples below use the React client. ### Basic Client ```typescript // lib/auth-client.ts import { createAuthClient } from "better-auth/react"; // or /vue, /svelte, /solid, /client export const authClient = createAuthClient({ baseURL: process.env.APP_URL || "http://localhost:3000", }); ``` ### With Plugins ```typescript // lib/auth-client.ts import { createAuthClient } from "better-auth/react"; import { twoFactorClient } from "better-auth/client/plugins"; import { organizationClient } from "better-auth/client/plugins"; export const authClient = createAuthClient({ baseURL: process.env.APP_URL || "http://localhost:3000", plugins: [ twoFactorClient({ twoFactorPage: "/auth/two-factor", }), organizationClient(), ], }); ``` ### useSession Hook ```typescript // components/user-menu.tsx import { authClient } from "@/lib/auth-client"; export function UserMenu() { // Reactive session - updates on auth state changes const { data: session, isPending, error } = authClient.useSession(); if (isPending) { return <div>Loading...</div>; } if (!session?.user) { return <a href="/auth/sign-in">Sign In</a>; } return ( <div> <span>{session.user.email}</span> <button onClick={() => authClient.signOut()} type="button" > Sign Out </button> </div> ); } ``` **Why good:** useSession is reactive and updates on auth changes, includes `refetch` method for manual refresh, signOut handles cookie cleanup --- ## Password Reset Use `authClient.requestPasswordReset` (renamed from `forgotPassword` in v1.4). ```typescript // hooks/use-password-reset.ts import { useState } from "react"; import { authClient } from "@/lib/auth-client"; export function usePasswordReset() { const [isPending, setIsPending] = useState(false); const [error, setError] = useState<string | null>(null); const [success, setSuccess] = useState(false); const requestReset = async (email: string) => { setIsPending(true); setError(null); try { const result = await authClient.requestPasswordReset({ email, redirectTo: "/auth/reset-password", }); if (result.error) { setError(result.error.message); return; } setSuccess(true); } catch (err) { setError("Failed to send reset email"); } finally { setIsPending(false); } }; return { requestReset, isPending, error, success }; } ``` --- ## Server-Side Password Verification Use `auth.api.verifyPassword` to confirm identity before sensitive operations (e.g., email change, account deletion). ```typescript // routes/settings.ts import { auth } from "@/lib/auth"; const HTTP_STATUS_UNAUTHORIZED = 401; app.post("/settings/change-email", async (c) => { const session = c.get("session"); const { password, newEmail } = await c.req.json(); const isValid = await auth.api.verifyPassword({ body: { email: session.user.email, password }, }); if (!isValid) { return c.json({ error: "Invalid password" }, HTTP_STATUS_UNAUTHORIZED); } // Proceed with email change... return c.json({ success: true }, 200); }); ``` --- ## Bundle Size Optimization Use `better-auth/minimal` to reduce bundle size when using ORM adapters (excludes Kysely). ```typescript // lib/auth.ts - Use minimal entry point for smaller bundles import { betterAuth } from "better-auth/minimal"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "@/lib/db"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg" }), }); ``` **When to use:** Projects using Drizzle, Prisma, or MongoDB adapters (not direct database connections) -
oauth.md 5.2 KB
# Better Auth - OAuth Examples > OAuth provider patterns for social login, Generic OAuth, and OAuth Provider plugin. See [SKILL.md](../SKILL.md) for core concepts. **Additional Examples:** - [core.md](core.md) - Sign up, sign in, client setup, database adapter - [two-factor.md](two-factor.md) - TOTP setup and verification - [organizations.md](organizations.md) - Multi-tenancy and invitations - [sessions.md](sessions.md) - Session configuration, cookie caching, stateless --- ## Server Configuration ```typescript // lib/auth.ts import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "@/lib/db"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg" }), socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }, google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, // Always get refresh token (Google only issues on first consent) accessType: "offline", prompt: "consent", }, }, }); ``` **Why good:** Environment variables protect secrets, accessType: "offline" ensures refresh tokens from Google, prompt: "consent" forces token refresh --- ## Environment Variables ```bash # .env.local GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret # Required for Better Auth BETTER_AUTH_SECRET=your_32_character_random_string BETTER_AUTH_URL=http://localhost:3000 ``` --- ## Client-Side OAuth Sign In ```typescript // components/oauth-buttons.tsx import { authClient } from "@/lib/auth-client"; export function OAuthButtons() { const handleGitHubSignIn = async () => { await authClient.signIn.social({ provider: "github", callbackURL: "/dashboard", }); }; const handleGoogleSignIn = async () => { await authClient.signIn.social({ provider: "google", callbackURL: "/dashboard", }); }; return ( <div> <button onClick={handleGitHubSignIn} type="button"> Continue with GitHub </button> <button onClick={handleGoogleSignIn} type="button"> Continue with Google </button> </div> ); } ``` **Why good:** callbackURL handles post-auth redirect, social provider string is type-safe from Better Auth types --- ## Bad Example - Hardcoded Secrets ```typescript // BAD Example - Hardcoded secrets import { betterAuth } from "better-auth"; export const auth = betterAuth({ socialProviders: { github: { clientId: "abc123", // BAD: Hardcoded in source clientSecret: "secret456", // BAD: Commits to git }, }, }); ``` **Why bad:** Hardcoded secrets committed to version control, exposed in build logs, impossible to rotate without code change --- ## Generic OAuth Plugin Use any OAuth 2.0 or OIDC provider with the Generic OAuth plugin. Supports discovery URL for auto-configuration. ```typescript // lib/auth.ts import { betterAuth } from "better-auth"; import { genericOAuth } from "better-auth/plugins"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "@/lib/db"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg" }), plugins: [ genericOAuth({ config: [ { providerId: "custom-idp", discoveryUrl: "https://idp.example.com/.well-known/openid-configuration", clientId: process.env.CUSTOM_IDP_CLIENT_ID!, clientSecret: process.env.CUSTOM_IDP_CLIENT_SECRET!, scopes: ["openid", "email", "profile"], pkce: true, mapProfileToUser: (profile) => ({ name: profile.name, email: profile.email, image: profile.picture, }), }, // Pre-configured helpers: auth0(), keycloak(), okta(), microsoftEntraId(), slack() ], }), ], }); ``` Client-side usage with Generic OAuth: ```typescript await authClient.signIn.oauth2({ providerId: "custom-idp", callbackURL: "/dashboard", }); ``` **Why good:** Works with any OAuth2/OIDC provider, PKCE for enhanced security, pre-configured helpers for common providers --- ## OAuth Provider Plugin (Act as OAuth Server) Use `oAuthProvider()` to let your app act as an OAuth 2.1 provider for other services. Replaces deprecated `oidcProvider`. ```typescript import { oAuthProvider } from "better-auth/plugins"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg" }), plugins: [ oAuthProvider({ clients: [ { clientId: "first-party-app", clientSecret: process.env.FIRST_PARTY_SECRET!, redirectUris: ["https://app.example.com/callback"], skipConsent: true, // Trusted clients skip consent screen }, { clientId: "third-party", clientSecret: process.env.THIRD_PARTY_SECRET!, redirectUris: ["https://partner.example.com/oauth/callback"], skipConsent: false, }, ], }), ], }); ``` **Why good:** OAuth 2.1 compliant, trusted client support for first-party apps, consent screen control -
organizations.md 3.9 KB
# Better Auth - Organization Multi-Tenancy Examples > Organization and multi-tenancy patterns for team management. See [SKILL.md](../SKILL.md) for core concepts. **Additional Examples:** - [core.md](core.md) - Sign up, sign in, client setup, database adapter - [oauth.md](oauth.md) - Social providers, Generic OAuth - [two-factor.md](two-factor.md) - TOTP setup and verification - [sessions.md](sessions.md) - Session configuration, cookie caching, stateless --- ## Server Configuration ```typescript // lib/auth.ts import { betterAuth } from "better-auth"; import { organization } from "better-auth/plugins"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "@/lib/db"; import { sendEmail } from "@/lib/email"; const ORG_LIMIT_PER_USER = 5; const INVITATION_EXPIRY_SECONDS = 48 * 60 * 60; // 48 hours export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg" }), plugins: [ organization({ organizationLimit: ORG_LIMIT_PER_USER, invitationExpiresIn: INVITATION_EXPIRY_SECONDS, sendInvitationEmail: async ({ email, invitationId, organization }) => { const inviteUrl = `${process.env.APP_URL}/accept-invite?id=${invitationId}`; await sendEmail({ to: email, subject: `Join ${organization.name}`, html: `<a href="${inviteUrl}">Accept invitation</a>`, }); }, }), ], }); ``` --- ## Client Configuration ```typescript // lib/auth-client.ts import { createAuthClient } from "better-auth/react"; import { organizationClient } from "better-auth/client/plugins"; export const authClient = createAuthClient({ baseURL: process.env.APP_URL || "http://localhost:3000", plugins: [organizationClient()], }); ``` --- ## Create Organization ```typescript // hooks/use-create-org.ts import { useState } from "react"; import { authClient } from "@/lib/auth-client"; interface CreateOrgData { name: string; slug: string; } export function useCreateOrg() { const [isPending, setIsPending] = useState(false); const [error, setError] = useState<string | null>(null); const createOrg = async (data: CreateOrgData) => { setIsPending(true); setError(null); try { const result = await authClient.organization.create({ name: data.name, slug: data.slug, }); if (result.error) { setError(result.error.message); return { success: false }; } // Set as active organization await authClient.organization.setActive({ organizationId: result.data.id, }); return { success: true, organization: result.data }; } catch (err) { setError("Failed to create organization"); return { success: false }; } finally { setIsPending(false); } }; return { createOrg, isPending, error }; } ``` --- ## Invite Members ```typescript // hooks/use-invite-member.ts import { useState } from "react"; import { authClient } from "@/lib/auth-client"; type Role = "owner" | "admin" | "member"; interface InviteData { email: string; role: Role; organizationId: string; } export function useInviteMember() { const [isPending, setIsPending] = useState(false); const [error, setError] = useState<string | null>(null); const inviteMember = async (data: InviteData) => { setIsPending(true); setError(null); try { const result = await authClient.organization.inviteMember({ email: data.email, role: data.role, organizationId: data.organizationId, }); if (result.error) { setError(result.error.message); return { success: false }; } return { success: true }; } catch (err) { setError("Failed to send invitation"); return { success: false }; } finally { setIsPending(false); } }; return { inviteMember, isPending, error }; } ``` **Why good:** organizationLimit prevents abuse, invitation emails customizable, setActive switches org context for session -
sessions.md 4.3 KB
# Better Auth - Session Management Examples > Session configuration and management patterns. See [SKILL.md](../SKILL.md) for core concepts. **Additional Examples:** - [core.md](core.md) - Sign up, sign in, client setup, database adapter - [oauth.md](oauth.md) - Social providers, Generic OAuth - [two-factor.md](two-factor.md) - TOTP setup and verification - [organizations.md](organizations.md) - Multi-tenancy and invitations --- ## Server Configuration ```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; // 7 days const SESSION_UPDATE_AGE_SECONDS = 60 * 60 * 24; // Refresh daily const CACHE_MAX_AGE_SECONDS = 5 * 60; // 5 minutes const FRESH_AGE_SECONDS = 60 * 5; // 5 minutes for sensitive operations export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg" }), session: { expiresIn: SESSION_EXPIRES_IN_SECONDS, updateAge: SESSION_UPDATE_AGE_SECONDS, freshAge: FRESH_AGE_SECONDS, // Cookie caching reduces database hits cookieCache: { enabled: true, maxAge: CACHE_MAX_AGE_SECONDS, strategy: "compact", // or "jwt" or "jwe" }, }, }); ``` **Why good:** cookieCache reduces DB queries (verify signature instead), freshAge requires recent auth for sensitive ops, named constants make policy auditable --- ## Revoke Sessions ```typescript // hooks/use-sessions.ts import { authClient } from "@/lib/auth-client"; export async function listSessions() { const result = await authClient.session.listSessions(); return result.data ?? []; } export async function revokeSession(token: string) { await authClient.session.revokeSession({ token }); } export async function revokeOtherSessions() { await authClient.session.revokeOtherSessions(); } ``` **Why good:** listSessions enables "active sessions" UI, revokeOtherSessions useful after password change --- ## Cookie Cache Strategies Three encoding strategies for session cookies: ```typescript // lib/auth.ts const CACHE_MAX_AGE_SECONDS = 5 * 60; // 5 minutes export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg" }), session: { cookieCache: { enabled: true, maxAge: CACHE_MAX_AGE_SECONDS, // Choose based on requirements: strategy: "compact", // Default: smallest, fastest // strategy: "jwt", // Readable, verifiable by third parties // strategy: "jwe", // Encrypted, hides session data }, }, }); ``` | Strategy | Size | Security | Use Case | | --------- | -------- | --------- | --------------------------------------- | | `compact` | Smallest | Signed | Internal apps, performance-critical | | `jwt` | Medium | Signed | API consumers, third-party verification | | `jwe` | Largest | Encrypted | Sensitive data, hide from client | **Gotcha:** Revoked sessions persist in cache until `maxAge` expires on other devices. --- ## Session Versioning for Mass Invalidation For stateless sessions, increment version to invalidate all sessions: ```typescript // lib/auth.ts export const auth = betterAuth({ session: { cookieCache: { enabled: true, maxAge: CACHE_MAX_AGE_SECONDS, strategy: "jwe", // Increment to invalidate ALL stateless sessions version: 2, // Was 1 }, }, }); ``` **Why good:** Mass invalidation without database, useful for security incidents --- ## Full Stateless Mode (No Database) Omit the `database` option entirely for fully stateless sessions stored in encrypted cookies. ```typescript // lib/auth.ts - No database dependency import { betterAuth } from "better-auth"; const CACHE_MAX_AGE_SECONDS = 60 * 5; export const auth = betterAuth({ // No database option = stateless mode emailAndPassword: { enabled: true }, session: { cookieCache: { enabled: true, maxAge: CACHE_MAX_AGE_SECONDS, strategy: "jwe", // Encrypted for security refreshCache: true, // Auto-refresh before expiry }, }, }); ``` **Why good:** No database dependency, works with edge functions, auto-refresh prevents expiration during active sessions **Trade-off:** Individual sessions cannot be revoked - increment `version` to invalidate all sessions at once -
two-factor.md 3.8 KB
# Better Auth - Two-Factor Authentication Examples > TOTP-based two-factor authentication patterns. See [SKILL.md](../SKILL.md) for core concepts. **Additional Examples:** - [core.md](core.md) - Sign up, sign in, client setup, database adapter - [oauth.md](oauth.md) - Social providers, Generic OAuth - [organizations.md](organizations.md) - Multi-tenancy and invitations - [sessions.md](sessions.md) - Session configuration, cookie caching, stateless --- ## Server Configuration ```typescript // lib/auth.ts import { betterAuth } from "better-auth"; import { twoFactor } from "better-auth/plugins"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "@/lib/db"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg" }), emailAndPassword: { enabled: true }, plugins: [ twoFactor({ issuer: "MyApp", // Shown in authenticator app // Optional: skip verification on enable (not recommended) // skipVerificationOnEnable: false, }), ], }); ``` After adding the plugin, run: ```bash # Step 1: Generate Better Auth schema npx auth@latest generate # Step 2: Generate Drizzle migration npx drizzle-kit generate # Step 3: Apply migration npx drizzle-kit migrate ``` --- ## Client Configuration ```typescript // lib/auth-client.ts import { createAuthClient } from "better-auth/react"; import { twoFactorClient } from "better-auth/client/plugins"; export const authClient = createAuthClient({ baseURL: process.env.APP_URL || "http://localhost:3000", plugins: [ twoFactorClient({ twoFactorPage: "/auth/two-factor", // Redirect for 2FA verification }), ], }); ``` --- ## Enable 2FA Flow ```typescript // components/enable-2fa.tsx import { useState } from "react"; import { authClient } from "@/lib/auth-client"; export function Enable2FA() { const [totpUri, setTotpUri] = useState<string | null>(null); const [backupCodes, setBackupCodes] = useState<string[]>([]); const [error, setError] = useState<string | null>(null); const handleEnable = async (password: string) => { try { const result = await authClient.twoFactor.enable({ password, }); if (result.error) { setError(result.error.message); return; } // Get TOTP URI for QR code generation setTotpUri(result.data?.totpURI ?? null); setBackupCodes(result.data?.backupCodes ?? []); } catch (err) { setError("Failed to enable 2FA"); } }; return ( <div> {/* Render QR code from totpUri */} {/* Display backup codes for user to save */} </div> ); } ``` --- ## Verify 2FA on Sign In ```typescript // components/verify-2fa.tsx import { useState } from "react"; import { authClient } from "@/lib/auth-client"; export function Verify2FA() { const [code, setCode] = useState(""); const [error, setError] = useState<string | null>(null); const handleVerify = async () => { try { const result = await authClient.twoFactor.verifyTOTP({ code, trustDevice: true, // Skip 2FA on this device for future logins }); if (result.error) { setError("Invalid code. Please try again."); return; } // Redirect to dashboard on success window.location.href = "/dashboard"; } catch (err) { setError("Verification failed"); } }; return ( <div> <input type="text" value={code} onChange={(e) => setCode(e.target.value)} placeholder="Enter 6-digit code" maxLength={6} /> <button onClick={handleVerify} type="button"> Verify </button> {error && <p>{error}</p>} </div> ); } ``` **Why good:** trustDevice reduces friction for trusted devices, backup codes stored for recovery, TOTP secrets encrypted in database -
v1.4-features.md 404 B
# Better Auth - v1.4+ Features (Relocated) > Content from this file has been merged into the appropriate topic files: - Stateless sessions, cookie cache strategies -> [sessions.md](sessions.md) - Generic OAuth, OAuth Provider plugin -> [oauth.md](oauth.md) - Password reset, verifyPassword, bundle optimization -> [core.md](core.md) - Experimental joins -> [core.md](core.md) (Drizzle adapter section)
-
-
reference.md 6.3 KB
# Authentication Reference > Decision frameworks, anti-patterns, and version notes for Better Auth. See [SKILL.md](SKILL.md) for core concepts and red flags, and [examples/](examples/) for code examples. --- <decision_framework> ## Decision Framework ### Session Storage Strategy ``` Need to revoke individual sessions? +-- YES -> Database sessions (default) | +-- Need reduced DB load? | +-- YES -> Enable cookieCache with maxAge | +-- NO -> Default database sessions +-- NO -> Stateless sessions +-- Full stateless (no database)? +-- YES -> Omit database option entirely +-- NO -> cookieCache only +-- Need session data in JWT? +-- YES -> strategy: "jwt" +-- NO -> strategy: "compact" (smallest) +-- Need encrypted session data? +-- YES -> strategy: "jwe" (largest, most secure) ``` ### Authentication Method Selection ``` User authentication method? +-- Email/password only? | +-- YES -> emailAndPassword: { enabled: true } +-- OAuth providers? | +-- YES -> Add to socialProviders | +-- Need refresh tokens from Google? | +-- YES -> accessType: "offline", prompt: "consent" +-- Need 2FA? | +-- YES -> Add twoFactor() plugin +-- Multi-tenant SaaS? +-- YES -> Add organization() plugin ``` ### Plugin Selection ``` Which plugins do you need? +-- Two-factor auth? -> twoFactor() +-- Organizations/teams? -> organization() +-- Custom session data? -> customSession() +-- Passkeys/WebAuthn? -> @better-auth/passkey (separate package!) +-- Magic links? -> magicLink() +-- API keys? -> @better-auth/api-key (separate package) +-- Generic OAuth provider? -> genericOAuth() +-- Act as OAuth provider? -> oAuthProvider() (replaces deprecated oidcProvider) +-- Anonymous users? -> anonymous() +-- Admin impersonation? -> admin() (must explicitly enable) +-- Enterprise SSO? -> SCIM support ``` </decision_framework> --- <integration> ## Integration Notes Better Auth is framework-agnostic - it works with any framework that provides a Web Standard `Request` object. Mount `auth.handler(request)` on your catch-all auth route. - **Database adapter**: `drizzleAdapter(db, { provider })` connects to your existing Drizzle setup - **Client**: `createAuthClient()` provides reactive `useSession` hook with `refetch` method - **Schema generation**: CLI generates ORM-specific schema files; run your ORM migration tool after **Replaces:** - Other auth libraries - choose one auth solution per project - Custom JWT session handling - Better Auth manages sessions end-to-end - OIDC Provider plugin (deprecated) - use `oAuthProvider()` instead </integration> --- <anti_patterns> ## Anti-Patterns ### Hardcoded Secrets ```typescript // ANTI-PATTERN: Secrets in code export const auth = betterAuth({ socialProviders: { github: { clientId: "abc123", // Commits to version control! clientSecret: "secret456", // Exposed in build logs! }, }, }); ``` **Why it's wrong:** Secrets committed to git, visible in build logs, impossible to rotate without code change. **What to do instead:** Use environment variables for all secrets. --- ### Missing CORS Configuration ```typescript // ANTI-PATTERN: Auth routes before CORS app.on(["POST", "GET"], "/auth/*", (c) => { return auth.handler(c.req.raw); }); // CORS after auth - preflight requests fail! app.use( "/auth/*", cors({ /* ... */ }), ); ``` **Why it's wrong:** CORS middleware must run before route handlers to handle OPTIONS preflight requests. **What to do instead:** Register CORS middleware before auth routes. --- ### No Type Safety for Session ```typescript // ANTI-PATTERN: Untyped session access app.use("*", async (c, next) => { const session = await auth.api.getSession({ headers: c.req.raw.headers }); c.user = session?.user; // No type - c.user is any await next(); }); ``` **Why it's wrong:** No TypeScript safety, c.user is any, no autocomplete. **What to do instead:** Use `createMiddleware<{ Variables: AuthVariables }>` with `auth.$Infer.Session`. --- ### Magic Numbers for Session Config ```typescript // ANTI-PATTERN: Magic numbers export const auth = betterAuth({ session: { expiresIn: 604800, // What is this? updateAge: 86400, // Days? Hours? freshAge: 300, // No idea }, }); ``` **Why it's wrong:** Numbers scattered in code, meaning unclear, policy changes require hunting. **What to do instead:** Use named constants like `SESSION_EXPIRES_IN_SECONDS = 60 * 60 * 24 * 7`. --- ### Forgetting Schema Generation ```typescript // ANTI-PATTERN: Adding plugins without schema update import { twoFactor, organization } from "better-auth/plugins"; export const auth = betterAuth({ plugins: [twoFactor(), organization()], // Error: Missing tables for plugins! }); ``` **Why it's wrong:** Plugins require database tables that don't exist yet. **What to do instead (Drizzle adapter):** Run the 3-step workflow: 1. `npx auth@latest generate` (generate Better Auth schema) 2. `npx drizzle-kit generate` (generate migration file) 3. `npx drizzle-kit migrate` (apply migration) **Note:** Better Auth's `migrate` command only works with Kysely adapter. For Drizzle, always use `generate` + Drizzle Kit. </anti_patterns> --- <version_notes> ## Version Notes **Better Auth v1.5 Breaking Changes (from v1.4):** - CLI changed: `npx @better-auth/cli` replaced by `npx auth@latest` - Database adapters extracted to separate packages (e.g. `@better-auth/drizzle-adapter`) - old import paths still work via re-exports - API key plugin extracted to `@better-auth/api-key` (separate install) - `InferUser` and `InferSession` types removed - use generic `User` and `Session` types from `better-auth` - All previously deprecated APIs removed (e.g. `createAdapter` -> `createAdapterFactory`) - `$ERROR_CODES` field on plugins now expects `Record<string, RawError>` not `Record<string, string>` - After hooks execute post-transaction instead of during - `/forget-password/email-otp` endpoint removed - use standard password reset flow **Better Auth v1.4 Breaking Changes (still relevant):** - `authClient.forgotPassword` renamed to `authClient.requestPasswordReset` - Account info endpoint changed from POST to GET `/account-info` - Passkey plugin moved to separate package: `@better-auth/passkey` - Plugin callbacks receive `ctx` instead of `request` (access via `ctx.request`) - Admin impersonation disabled by default (v1.4.9+) - must explicitly enable </version_notes> -
SKILL.md 11.3 KB
--- name: api-auth-better-auth-drizzle-hono description: Better Auth patterns, sessions, OAuth --- # Authentication with Better Auth > **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 the auth route 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 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)** </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, 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 - Projects requiring database-stored or stateless sessions **When NOT to use:** - Need managed authentication with zero maintenance (consider hosted auth solutions) - Simple static sites without user accounts - Projects where serverless cold starts are critical (though stateless mode helps) **Key patterns covered:** - Server configuration (auth.ts) with plugins - Session middleware and type-safe route protection - Email/password authentication flows - OAuth providers (GitHub, Google, Generic OAuth) - Two-factor authentication (TOTP) - Organization and multi-tenancy - Session strategies: database, cookie cache, stateless - Database adapter integration - Client-side useSession hook - Performance: experimental joins, cookie caching, stateless sessions **Detailed Resources:** - [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, version notes --- <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. **Core principles:** 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 </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Server Configuration (auth.ts) 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; // 7 days const SESSION_UPDATE_AGE_SECONDS = 60 * 60 * 24; // Refresh daily export const auth = betterAuth({ 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.APP_URL || "http://localhost:3000"], }); ``` **Why good:** Named constants make session policy auditable, env vars for URLs, single exported instance ```typescript // BAD: Magic numbers, hardcoded secrets, default export const auth = betterAuth({ database: { url: "postgres://user:pass@localhost/db" }, session: { expiresIn: 604800 }, }); export default auth; ``` **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: Session Middleware with Type Safety Mount auth handler and create typed middleware for session access in routes. ```typescript // CRITICAL: CORS must be configured BEFORE auth routes app.use("/auth/*", cors({ origin: APP_URL, credentials: true })); app.on(["POST", "GET"], "/auth/*", (c) => auth.handler(c.req.raw)); ``` ```typescript // 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, 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(); }, ); ``` **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 // 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(); }); ``` **Why bad:** No AuthVariables type = any access, direct property assignment bypasses typed Variables See [examples/core.md](examples/core.md) for protected route patterns. --- ### Pattern 3: Schema Generation After Plugins Every plugin adds database tables. Run the CLI after adding or modifying plugins: ```bash # Step 1: Generate Better Auth schema (outputs ORM-specific files) npx auth@latest generate # Step 2: Generate migration with your ORM tool npx drizzle-kit generate # Step 3: Apply migration npx drizzle-kit migrate ``` Always run all 3 steps. The Better Auth `migrate` command only works with the Kysely adapter - for Drizzle, use `generate` + Drizzle Kit. --- ### Pattern 4: Email/Password with Verification Configure email/password auth with verification and password reset callbacks. ```typescript 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 password", html: `<a href="${url}">Reset</a>`, }); }, }, emailVerification: { sendVerificationEmail: async ({ user, url }) => { await sendEmail({ to: user.email, subject: "Verify email", html: `<a href="${url}">Verify</a>`, }); }, }, }); ``` **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" } }, }); ``` 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> --- <red_flags> ## RED FLAGS - Hardcoded secrets (clientId/clientSecret in source) - must use environment variables - CORS configured after auth routes - preflight requests will fail - Missing `BETTER_AUTH_SECRET` env var - sessions will not work - No schema generation after adding plugins - database errors at runtime - Untyped session middleware - loses TypeScript safety, `c.user` becomes `any` - Using `auth.migrate()` with Drizzle adapter - only works with Kysely, use `generate` + Drizzle Kit - Missing `c.req.raw` when calling `auth.handler()` - must pass the raw Web Standard Request **Gotchas & Edge Cases:** - Google only issues refresh tokens on first consent - use `accessType: "offline"` and `prompt: "consent"` - GitHub OAuth apps don't issue refresh tokens (access tokens are long-lived) - Stateless sessions cannot be revoked individually - increment `version` to invalidate all - Cookie cache revocation is delayed until `maxAge` expires on other devices - Session cookies need `SameSite=None` + `Secure` for cross-domain deployments - `authClient.forgotPassword` was renamed to `authClient.requestPasswordReset` in v1.4 - `InferUser`/`InferSession` removed in v1.5 - use generic `User` and `Session` types from `better-auth` See [reference.md](reference.md) for anti-patterns with code examples, decision frameworks, and version notes. </red_flags> --- <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 the auth route 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 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)** **Failure to follow these rules will cause authentication failures, security vulnerabilities, or runtime errors.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.