
better-auth-best-practices
by kriegcloud
SKILL.md
name: Better Auth Best Practices description: A skill to integrate better-auth. And auth framework for TypeScript projects.
Best practices for building Better Auth integrations. Use when implementing authentication, authorization, session management, OAuth providers, or any Better Auth integration.
When designing an authentication integration, always prefer Better Auth's Documentation.
You should always default to the latest version of Better Auth unless the user specifies otherwise.
Better Auth is a TypeScript-first authentication framework that provides a secure, flexible, and developer-friendly approach to authentication. It supports multiple methods including email/password, OAuth providers, magic links, and passwordless authentication.
Core Principles
1. Packaging
Most imports come from the npm package better-auth (initializer), better-auth/plugins (plugins), and better-auth/adapters/* (adapters). Some plugins use a scoped package like @better-auth/stripe.
2. Environment Variables
Better Auth requires two environment variables:
BETTER_AUTH_SECRET: A high-entropy secret used for encryption and hashing (at least 32 characters).BETTER_AUTH_URL: The base URL for the server where Better Auth is mounted.
3. Auth Config
Initialize Better Auth by calling betterAuth with a config object. Export it as const auth or as a default export unless you are wrapping the initializer (for example, to pass runtime context in edge environments). The CLI looks for auth.ts in ./, ./lib, ./utils, or those directories under src.
4. Database
The database connection is the most important part of the auth config. If no database is provided, Better Auth uses the in-memory adapter (non-persistent). Built-in adapters support database clients like pg, mysql2, better-sqlite3, and bun:sqlite. You can also use adapters like drizzleAdapter, prismaAdapter, and mongodbAdapter.
5. CLI
Use the Better Auth CLI to manage schema:
npx @better-auth/cli@latest migrateapplies schema directly for the built-in Kysely adapter.npx @better-auth/cli@latest generategenerates schema for Prisma/Drizzle (apply with your ORM).- The CLI supports
--configto point to a non-defaultauth.tslocation.
6. Authentication Methods
Configure methods in the auth config. Core methods include emailAndPassword and socialProviders. Enable email/password with emailAndPassword: { enabled: true }. Social providers are defined under socialProviders. For additional OAuth integrations, use the genericOAuth plugin. Other plugins include username, phoneNumber, emailOtp, magicLink, passkey, and anonymous. Plugins can add schema requirements, so re-run CLI generate/migrate when you add or change plugins.
7. Handler
All HTTP requests are handled by auth.handler. Mount it according to the framework-specific integration docs.
8. Client
Better Auth provides clients to interact with the auth server. Client files are typically named auth-client.ts.
React example client:
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
/** The base URL of the server (optional if you're using the same domain) */
baseURL: "http://localhost:3000"
})
Other than React, there are clients for vanilla JS, Vue, Svelte, and Solid. See https://www.better-auth.com/docs/concepts/client
9. Type Safety
Better Auth is built with TypeScript and provides full type safety. Infer types with auth.$Infer.
10. Session
Sessions are stored in the database unless you provide secondaryStorage, which can be used for session data or rate limiting in high-performance stores.
11. Auth Options
Review https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts for the full configuration surface. Key skill areas include:
- Rate limiting: Configure
rateLimitdefaults, per-pathcustomRules, and storage (memory,database,secondary-storage, orcustomStorage). - Advanced security: Use
advanced.useSecureCookies, and be cautious withadvanced.disableCSRFCheckoradvanced.disableOriginCheck(these reduce protections). - Trusted origins: Prefer dynamic
trustedOriginswhen running behind proxies or multiple hostnames. - Cookie strategy: Customize cookie names/attributes, prefixes, and cross-subdomain settings via
advanced.cookies,advanced.cookiePrefix, andadvanced.crossSubDomainCookies. - Session tuning: Adjust
session.expiresIn,session.updateAge,session.cookieCache, andsession.freshAgebased on UX/security needs. - Account linking: Configure
account.accountLinkingcarefully, especiallyallowDifferentEmailsandallowUnlinkingAll. - Email flows: Use
emailVerificationhooks andemailAndPassword.sendResetPasswordto implement secure verification and recovery. - Data model mapping: Map fields and add metadata via
user.fields,session.fields,account.fields, andverification.fields. - Database hooks: Use
databaseHooksto enforce policies or auditing on user/session/account/verification lifecycle events. - Error handling: Centralize response behavior with
onAPIErrorand customize the default error page if needed. - Telemetry: Control data collection with
telemetry.enabledandtelemetry.debug. - Experimental flags: Keep
experimental.joinsoff unless the adapter supports it and the docs recommend it.
Plugin Architecture
Plugins follow a consistent structure with these key properties:
| Property | Description |
|---|---|
id | Unique string identifier |
endpoints | Object of createAuthEndpoint calls that define API routes |
schema | Database schema additions the plugin needs (triggers migrations) |
hooks.before / hooks.after | Middleware that runs before/after matched endpoints |
init | Called during auth initialization to modify context or options |
onRequest / onResponse | Request/response interceptors |
$ERROR_CODES | Plugin-specific error code constants |
rateLimit | Custom rate limit rules for plugin endpoints |
When building custom plugins, use createAuthEndpoint and createAuthMiddleware from @better-auth/core/api. Use sessionMiddleware from better-auth/api to require authentication.
Example Plugin Structure
import type { BetterAuthPlugin } from "@better-auth/core";
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
import { sessionMiddleware } from "better-auth/api";
import { z } from "zod";
export const myPlugin = (options?: MyPluginOptions) => {
return {
id: "my-plugin",
endpoints: {
myEndpoint: createAuthEndpoint(
"/my-plugin/action",
{
method: "POST",
body: z.object({ data: z.string() }),
use: [sessionMiddleware], // Require authentication
},
async (ctx) => {
const user = ctx.context.session.user;
// Implementation
return { success: true };
}
),
},
schema: {
myTable: {
fields: {
userId: { type: "string", references: { model: "user", field: "id" } },
data: { type: "string" },
},
},
},
$ERROR_CODES: {
MY_ERROR: "My custom error message",
},
} satisfies BetterAuthPlugin;
};
Client-Server Type Inference
Use auth.$Infer on the server and client plugins expose $InferServerPlugin for type bridging:
// Server plugin exports
export const myPlugin = () => ({
id: "my-plugin",
$ERROR_CODES: MY_ERROR_CODES,
// ...
}) satisfies BetterAuthPlugin;
// Client plugin
export const myPluginClient = () => ({
$InferServerPlugin: {} as ReturnType<typeof myPlugin>,
getActions: ($fetch) => ({
myAction: async () => {
return $fetch("/my-plugin/action", { method: "POST" });
},
}),
}) satisfies BetterAuthClientPlugin;
Social Providers
Built-in Providers
apple, atlassian, cognito, discord, dropbox, facebook, figma, github, gitlab, google, huggingface, kakao, kick, line, linear, linkedin, microsoft, naver, notion, paybin, paypal, polar, reddit, roblox, salesforce, slack, spotify, tiktok, twitch, twitter, vercel, vk, zoom
Generic OAuth
For providers not in this list, use the genericOAuth plugin with pre-built configurations for Auth0, Gumroad, HubSpot, Keycloak, Line, Microsoft Entra ID, Okta, Patreon, and Slack.
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";
const auth = betterAuth({
// Built-in providers
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
// Generic OAuth for other providers
plugins: [
genericOAuth({
config: [
{
providerId: "custom-provider",
clientId: process.env.CUSTOM_CLIENT_ID!,
clientSecret: process.env.CUSTOM_CLIENT_SECRET!,
authorizationUrl: "https://provider.com/oauth/authorize",
tokenUrl: "https://provider.com/oauth/token",
userInfoUrl: "https://provider.com/api/user",
},
],
}),
],
});
Scoped Packages
Some plugins live in separate packages:
| Package | Description |
|---|---|
@better-auth/stripe | Stripe subscriptions and payments |
@better-auth/expo | Expo/React Native client |
@better-auth/passkey | Passkey/WebAuthn authentication |
@better-auth/sso | SAML/OIDC enterprise SSO |
@better-auth/scim | SCIM user provisioning |
Framework Integrations
Next.js
import { toNextJsHandler } from "better-auth/integrations/next-js";
import { auth } from "./auth";
export const { GET, POST } = toNextJsHandler(auth);
For Server Components cookie handling, add nextCookies() plugin:
import { nextCookies } from "better-auth/integrations/next-js";
const auth = betterAuth({
plugins: [nextCookies()],
});
Other Frameworks
- SvelteKit: Use
svelteKitHandler() - SolidStart: Use
solidStartHandler() - TanStack Start: Use
tanstackStartHandler() - Node/Express: Mount
auth.handlerdirectly as middleware
Testing
For unit tests, use getTestInstance() from better-auth/test-utils:
import { getTestInstance } from "better-auth/test-utils";
import { describe, it, expect } from "vitest";
describe("my auth tests", () => {
it("should authenticate user", async () => {
const { client, signInWithTestUser } = await getTestInstance({
plugins: [myPlugin()],
});
const { headers, user } = await signInWithTestUser();
const result = await client.myPlugin.action({
fetchOptions: { headers },
});
expect(result.data).toBeDefined();
});
});
What getTestInstance() Provides
- In-memory SQLite database by default (or postgres/mysql/mongodb via config)
- Automatic migration execution
- Pre-configured client with custom fetch
- Test user (
test@test.com/test123456) signInWithTestUser()for authenticated test flows- Automatic cleanup via
afterAll
Account Linking
Configure account.accountLinking carefully:
| Option | Description |
|---|---|
enabled | Toggle account linking (default: true) |
trustedProviders | Array of provider IDs that can link without email verification |
allowDifferentEmails | Allow linking accounts with different emails (security risk) |
allowUnlinkingAll | Allow users to unlink all OAuth accounts |
Accounts are automatically linked when a user signs in with a different provider using the same verified email.
const auth = betterAuth({
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "github"], // These can link without email verification
allowDifferentEmails: false, // Keep false for security
},
},
});
Session Cookie Strategies
session.cookieCache.strategy options:
| Strategy | Description |
|---|---|
"compact" | (default) Stores minimal session data in cookies |
"full" | Stores complete session/user data |
"jwt" | Uses signed JWT for stateless verification (requires jwt plugin) |
const auth = betterAuth({
session: {
cookieCache: {
enabled: true,
strategy: "compact",
maxAge: 60 * 5, // 5 minutes
},
},
});
Internal Adapter
Access database operations via ctx.context.internalAdapter in endpoints:
findUserByEmail(),findUserById()createUser(),updateUser(),deleteUser()createSession(),findSession(),deleteSession()linkAccount(),findOAuthUser()createVerification(),findVerification()
These methods automatically apply databaseHooks (before/after lifecycle events).
Database Hooks
Use databaseHooks for lifecycle events on core models:
const auth = betterAuth({
databaseHooks: {
user: {
create: {
before: async (user, ctx) => {
// Modify user before creation
return { data: { ...user, customField: "value" } };
},
after: async (user, ctx) => {
// Audit log, send welcome email, etc.
await sendWelcomeEmail(user.email);
},
},
},
session: {
create: {
after: async (session, ctx) => {
// Track session creation
},
},
},
},
});
Error Handling
Use APIError from better-call for throwing errors:
import { APIError } from "better-call";
import { BASE_ERROR_CODES } from "@better-auth/core/error";
// In an endpoint
throw new APIError("BAD_REQUEST", {
message: BASE_ERROR_CODES.INVALID_PASSWORD
});
// Custom error codes in plugins
export const MY_ERROR_CODES = {
CUSTOM_ERROR: "Custom error occurred",
VALIDATION_FAILED: "Validation failed",
} as const;
Error codes should be exported as $ERROR_CODES in plugins. Reference BASE_ERROR_CODES from @better-auth/core/error for standard codes.
Background Tasks
For edge runtimes (Vercel, Cloudflare Workers), configure advanced.backgroundTasks.handler with the platform's waitUntil function:
// Vercel
import { waitUntil } from "@vercel/functions";
const auth = betterAuth({
advanced: {
backgroundTasks: {
handler: waitUntil,
},
},
});
// Cloudflare Workers (with AsyncLocalStorage)
const auth = betterAuth({
advanced: {
backgroundTasks: {
handler: (p) => execCtxStorage.getStore()?.waitUntil(p),
},
},
});
Secondary Storage
Use secondaryStorage for high-performance session/rate-limit storage:
import { betterAuth } from "better-auth";
import { Redis } from "ioredis";
const redis = new Redis();
const auth = betterAuth({
secondaryStorage: {
get: async (key) => {
const value = await redis.get(key);
return value ? JSON.parse(value) : null;
},
set: async (key, value, ttl) => {
if (ttl) {
await redis.setex(key, ttl, JSON.stringify(value));
} else {
await redis.set(key, JSON.stringify(value));
}
},
delete: async (key) => {
await redis.del(key);
},
},
});
IP Address Handling
Configure IP address extraction for rate limiting and session tracking:
const auth = betterAuth({
advanced: {
ipAddress: {
ipAddressHeaders: ["x-real-ip", "x-forwarded-for", "cf-connecting-ip"],
disableIpTracking: false, // Only disable if absolutely necessary
},
},
});
Resources
Always consult the official documentation for the most up-to-date best practices and API changes.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon



