Awesome-omni-skill Better Auth Best Practices

A skill to integrate better-auth. And auth framework for TypeScript projects.

install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
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-aloish" ~/.claude/skills/diegosouzapw-awesome-omni-skill-better-auth-best-practices-f9e123 && rm -rf "$T"
manifest: skills/development/better-auth-best-practices-aloish/SKILL.md
source content

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 migrate
    applies schema directly for the built-in Kysely adapter.
  • npx @better-auth/cli@latest generate
    generates schema for Prisma/Drizzle (apply with your ORM).
  • The CLI supports
    --config
    to point to a non-default
    auth.ts
    location.

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
    rateLimit
    defaults, per-path
    customRules
    , and storage (
    memory
    ,
    database
    ,
    secondary-storage
    , or
    customStorage
    ).
  • Advanced security: Use
    advanced.useSecureCookies
    , and be cautious with
    advanced.disableCSRFCheck
    or
    advanced.disableOriginCheck
    (these reduce protections).
  • Trusted origins: Prefer dynamic
    trustedOrigins
    when running behind proxies or multiple hostnames.
  • Cookie strategy: Customize cookie names/attributes, prefixes, and cross-subdomain settings via
    advanced.cookies
    ,
    advanced.cookiePrefix
    , and
    advanced.crossSubDomainCookies
    .
  • Session tuning: Adjust
    session.expiresIn
    ,
    session.updateAge
    ,
    session.cookieCache
    , and
    session.freshAge
    based on UX/security needs.
  • Account linking: Configure
    account.accountLinking
    carefully, especially
    allowDifferentEmails
    and
    allowUnlinkingAll
    .
  • Email flows: Use
    emailVerification
    hooks and
    emailAndPassword.sendResetPassword
    to implement secure verification and recovery.
  • Data model mapping: Map fields and add metadata via
    user.fields
    ,
    session.fields
    ,
    account.fields
    , and
    verification.fields
    .
  • Database hooks: Use
    databaseHooks
    to enforce policies or auditing on user/session/account/verification lifecycle events.
  • Error handling: Centralize response behavior with
    onAPIError
    and customize the default error page if needed.
  • Telemetry: Control data collection with
    telemetry.enabled
    and
    telemetry.debug
    .
  • Experimental flags: Keep
    experimental.joins
    off unless the adapter supports it and the docs recommend it.

Plugin Architecture

Plugins follow a consistent structure with these key properties:

PropertyDescription
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:

PackageDescription
@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.handler
    directly 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:

OptionDescription
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:

StrategyDescription
"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.