home / skills / epicenterhq / epicenter / better-auth-best-practices

better-auth-best-practices skill

/.agents/skills/better-auth-best-practices

This skill helps you implement best-practice Better Auth integration in your TypeScript apps, simplifying secure authentication setup and plugin usage.

npx playbooks add skill epicenterhq/epicenter --skill better-auth-best-practices

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
7.5 KB
---
name: better-auth-best-practices
description: Skill for integrating Better Auth - the comprehensive TypeScript authentication framework.
---

# Better Auth Integration Guide

**Always consult [better-auth.com/docs](https://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
- `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32`
- `BETTER_AUTH_URL` - Base URL (e.g., `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
- `npx @better-auth/cli@latest migrate` - Apply schema (built-in adapter)
- `npx @better-auth/cli@latest generate` - Generate schema for Prisma/Drizzle
- `npx @better-auth/cli mcp --cursor` - Add MCP to AI tools

**Re-run after adding/changing plugins.**

---

## Core Config Options

| Option | Notes |
|--------|-------|
| `appName` | Optional display name |
| `baseURL` | Only if `BETTER_AUTH_URL` not set |
| `basePath` | Default `/api/auth`. Set `/` for root. |
| `secret` | Only if `BETTER_AUTH_SECRET` not set |
| `database` | Required for most features. See adapters docs. |
| `secondaryStorage` | Redis/KV for sessions & rate limits |
| `emailAndPassword` | `{ enabled: true }` to activate |
| `socialProviders` | `{ google: { clientId, clientSecret }, ... }` |
| `plugins` | Array of plugins |
| `trustedOrigins` | 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:**
1. If `secondaryStorage` defined → sessions go there (not DB)
2. Set `session.storeSessionInDatabase: true` to also persist to DB
3. No database + `cookieCache` → fully stateless mode

**Cookie cache strategies:**
- `compact` (default) - Base64url + HMAC. Smallest.
- `jwt` - Standard JWT. Readable but signed.
- `jwe` - Encrypted. Maximum security.

**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

- `emailVerification.sendVerificationEmail` - Must be defined for verification to work
- `emailVerification.sendOnSignUp` / `sendOnSignIn` - Auto-send triggers
- `emailAndPassword.sendResetPassword` - Password reset email handler

---

## Security

**In `advanced`:**
- `useSecureCookies` - Force HTTPS cookies
- `disableCSRFCheck` - ⚠️ Security risk
- `disableOriginCheck` - ⚠️ Security risk  
- `crossSubDomainCookies.enabled` - Share cookies across subdomains
- `ipAddress.ipAddressHeaders` - Custom IP headers for proxies
- `database.generateId` - Custom ID generation or `"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 `bearer()` plugin.** It makes Better Auth work for native apps without changing server-side session logic:

- `bearer()` adds `set-auth-token` response header to all auth responses
- Sign-in/sign-up endpoints already return `{ token: session.token }` in the response body
- The client extracts the token from the body or header, stores it locally
- On subsequent requests: `Authorization: Bearer <token>` → bearer() converts to cookie internally → Better Auth validates normally

**Client config for native apps:**
```typescript
const authClient = createAuthClient({
  baseURL: 'https://hub.example.com',
  fetchOptions: {
    auth: {
      type: 'Bearer',
      token: () => localStorage.getItem('session-token') || '',
    },
  },
});
```

**Token extraction on sign-in:**
```typescript
const { data } = await authClient.signIn.email({
  email: '[email protected]',
  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

1. **Model vs table name** - Config uses ORM model name, not DB table name
2. **Plugin schema** - Re-run CLI after adding plugins
3. **Secondary storage** - Sessions go there by default, not DB
4. **Cookie cache** - Custom session fields NOT cached, always re-fetched
5. **Stateless mode** - No DB = session in cookie only, logout on cache expiry
6. **Change email flow** - Sends to current email first, then new email

---

## Resources

- [Docs](https://better-auth.com/docs)
- [Options Reference](https://better-auth.com/docs/reference/options)
- [LLMs.txt](https://better-auth.com/llms.txt)
- [GitHub](https://github.com/better-auth/better-auth)
- [Init Options Source](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts)

Overview

This skill integrates Better Auth — a TypeScript-first, framework-agnostic authentication framework — into your app. It provides configuration guidance, environment requirements, adapter and plugin wiring, and native-app recommendations for Svelte, SvelteKit, Tauri, and other TypeScript stacks. Use it to set up email/password, OAuth, passkeys, magic links, and bearer-based native auth flows quickly and securely.

How this skill works

The skill inspects project structure for an auth.ts entrypoint, validates required environment variables (BETTER_AUTH_SECRET, BETTER_AUTH_URL) and suggests where to place config when env vars are absent. It verifies adapter and database settings, recommends session and cookie-cache strategies, and ensures plugins are imported from tree-shakeable paths. For native apps it recommends the bearer() plugin and shows how to extract and use tokens in Tauri/Electron/Expo.

When to use it

  • You need a TypeScript-first auth system for Svelte, SvelteKit, or other frontends.
  • You must support multiple flows: email/password, OAuth, magic links, passkeys, or 2FA.
  • You want native-desktop or mobile support where cookies are unreliable (Tauri/Electron/Expo).
  • You need clear adapter wiring for Prisma/Drizzle/MongoDB or direct DB pools.
  • You want to add plugins (two-factor, organization, apiKey, bearer) with correct imports.

Best practices

  • Always set BETTER_AUTH_SECRET (min 32 chars) and BETTER_AUTH_URL in environment for production.
  • Keep auth.ts in ./src, ./lib, ./utils, or project root; use --config for custom locations.
  • Use a secondaryStorage (Redis/KV) for session storage and rate limiting to avoid DB load.
  • Import plugins from their direct paths (e.g. better-auth/plugins/two-factor) to enable tree-shaking.
  • Use bearer() for native apps and store opaque tokens client-side; do not confuse this with JWT-based flows.
  • After changing plugins or models, re-run the CLI migrate/generate commands to update schemas.

Example use cases

  • SvelteKit web app with Prisma adapter, email/password sign-up, and social OAuth providers.
  • Tauri desktop app using bearer() to persist opaque HMAC session tokens instead of relying on cookies.
  • Mobile app where sign-in returns set-auth-token header; client stores token and sends Authorization: Bearer <token>.
  • Multi-tenant org feature using the organization plugin and database hooks for automatic defaults.
  • Serverless deployment using cookieCache compact mode for stateless sessions and short session expiry.

FAQ

Do I have to store secrets in code?

No. Prefer BETTER_AUTH_SECRET and BETTER_AUTH_URL in environment variables; only set secret/baseURL in config if env vars are not available.

How do I support native apps where cookies fail?

Install and enable the bearer() plugin. Extract the session token from the response body or set-auth-token header and send it as Authorization: Bearer on subsequent requests; bearer() converts it to the expected cookie server-side.