Awesome-omni-skill better-auth-best-practices
Skill for integrating Better Auth - the comprehensive TypeScript authentication framework.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/better-auth-best-practices-epicenterhq" ~/.claude/skills/diegosouzapw-awesome-omni-skill-better-auth-best-practices-8da2b5 && rm -rf "$T"
skills/development/better-auth-best-practices-epicenterhq/SKILL.mdBetter Auth Integration Guide
Always consult better-auth.com/docs for code examples and latest API.
Better Auth is a TypeScript-first, framework-agnostic auth framework supporting email/password, OAuth, magic links, passkeys, and more via plugins.
Quick Reference
Environment Variables
- Encryption secret (min 32 chars). Generate:BETTER_AUTH_SECRETopenssl rand -base64 32
- Base URL (e.g.,BETTER_AUTH_URL
)https://example.com
Only define
baseURL/secret in config if env vars are NOT set.
File Location
CLI looks for
auth.ts in: ./, ./lib, ./utils, or under ./src. Use --config for custom path.
CLI Commands
- Apply schema (built-in adapter)npx @better-auth/cli@latest migrate
- Generate schema for Prisma/Drizzlenpx @better-auth/cli@latest generate
- Add MCP to AI toolsnpx @better-auth/cli mcp --cursor
Re-run after adding/changing plugins.
Core Config Options
| Option | Notes |
|---|---|
| Optional display name |
| Only if not set |
| Default . Set for root. |
| Only if not set |
| Required for most features. See adapters docs. |
| Redis/KV for sessions & rate limits |
| to activate |
| |
| Array of plugins |
| CSRF whitelist |
Database
Direct connections: Pass
pg.Pool, mysql2 pool, better-sqlite3, or bun:sqlite instance.
ORM adapters: Import from
better-auth/adapters/drizzle, better-auth/adapters/prisma, better-auth/adapters/mongodb.
Critical: Better Auth uses adapter model names, NOT underlying table names. If Prisma model is
User mapping to table users, use modelName: "user" (Prisma reference), not "users".
Session Management
Storage priority:
- If
defined → sessions go there (not DB)secondaryStorage - Set
to also persist to DBsession.storeSessionInDatabase: true - No database +
→ fully stateless modecookieCache
Cookie cache strategies:
(default) - Base64url + HMAC. Smallest.compact
- Standard JWT. Readable but signed.jwt
- Encrypted. Maximum security.jwe
Key options:
session.expiresIn (default 7 days), session.updateAge (refresh interval), session.cookieCache.maxAge, session.cookieCache.version (change to invalidate all sessions).
User & Account Config
User:
user.modelName, user.fields (column mapping), user.additionalFields, user.changeEmail.enabled (disabled by default), user.deleteUser.enabled (disabled by default).
Account:
account.modelName, account.accountLinking.enabled, account.storeAccountCookie (for stateless OAuth).
Required for registration:
email and name fields.
Email Flows
- Must be defined for verification to workemailVerification.sendVerificationEmail
/emailVerification.sendOnSignUp
- Auto-send triggerssendOnSignIn
- Password reset email handleremailAndPassword.sendResetPassword
Security
In
:advanced
- Force HTTPS cookiesuseSecureCookies
- ⚠️ Security riskdisableCSRFCheck
- ⚠️ Security riskdisableOriginCheck
- Share cookies across subdomainscrossSubDomainCookies.enabled
- Custom IP headers for proxiesipAddress.ipAddressHeaders
- Custom ID generation ordatabase.generateId
/"serial"
/"uuid"false
Rate limiting:
rateLimit.enabled, rateLimit.window, rateLimit.max, rateLimit.storage ("memory" | "database" | "secondary-storage").
Hooks
Endpoint hooks:
hooks.before / hooks.after - Array of { matcher, handler }. Use createAuthMiddleware. Access ctx.path, ctx.context.returned (after), ctx.context.session.
Database hooks:
databaseHooks.user.create.before/after, same for session, account. Useful for adding default values or post-creation actions.
Hook context (
): ctx.context
session, secret, authCookies, password.hash()/verify(), adapter, internalAdapter, generateId(), tables, baseURL.
Plugins
Import from dedicated paths for tree-shaking:
import { twoFactor } from "better-auth/plugins/two-factor"
NOT
from "better-auth/plugins".
Popular plugins:
twoFactor, organization, passkey, magicLink, emailOtp, username, phoneNumber, admin, apiKey, bearer, jwt, multiSession, sso, oauthProvider, oidcProvider, openAPI, genericOAuth.
Client plugins go in
createAuthClient({ plugins: [...] }).
Native App / Desktop Auth (Tauri, Electron, Expo)
Cookies are unreliable in non-browser contexts (Tauri webview rejects
Secure cookies from tauri://, split cookie jars between webview and Rust HTTP, debug/release inconsistencies).
Use the
plugin. It makes Better Auth work for native apps without changing server-side session logic:bearer()
addsbearer()
response header to all auth responsesset-auth-token- Sign-in/sign-up endpoints already return
in the response body{ token: session.token } - The client extracts the token from the body or header, stores it locally
- On subsequent requests:
→ bearer() converts to cookie internally → Better Auth validates normallyAuthorization: Bearer <token>
Client config for native apps:
const authClient = createAuthClient({ baseURL: 'https://hub.example.com', fetchOptions: { auth: { type: 'Bearer', token: () => localStorage.getItem('session-token') || '', }, }, });
Token extraction on sign-in:
const { data } = await authClient.signIn.email({ email: 'user@example.com', password: 'password', }, { onSuccess: (ctx) => { const token = ctx.response.headers.get('set-auth-token'); localStorage.setItem('session-token', token); }, });
Key point: bearer() handles opaque HMAC session tokens only. It does NOT validate JWTs. The
jwt() plugin is separate and for issuing tokens to external services, not for native app auth.
Client
Import from:
better-auth/client (vanilla), better-auth/react, better-auth/vue, better-auth/svelte, better-auth/solid.
Key methods:
signUp.email(), signIn.email(), signIn.social(), signOut(), useSession(), getSession(), revokeSession(), revokeSessions().
Type Safety
Infer types:
typeof auth.$Infer.Session, typeof auth.$Infer.Session.user.
For separate client/server projects:
createAuthClient<typeof auth>().
Common Gotchas
- Model vs table name - Config uses ORM model name, not DB table name
- Plugin schema - Re-run CLI after adding plugins
- Secondary storage - Sessions go there by default, not DB
- Cookie cache - Custom session fields NOT cached, always re-fetched
- Stateless mode - No DB = session in cookie only, logout on cache expiry
- Change email flow - Sends to current email first, then new email