Better Auth Documentation

repository·main·Indexed 42 days ago

https://github.com/better-auth/skills

A TypeScript/JavaScript authentication framework featuring a structured workflow for scaffolding, implementation, and migration. Includes support for various ORM adapters (Drizzle, Prisma, MongoDB), session storage strategies, and a plugin system for features like Two-Factor and Passkeys. Provides framework-specific clients for React, Vue, Svelte, and Solid, along with a CLI for database migrations and error explanation.

Tokens
14.9K
Snippets
48
Records
58
Agent score
53%

What's inside Better Auth

  1. Manage active organizations in session

    main

    The 'active organization' is stored in the user's session. Once set, subsequent API calls (like listMembers or inviteMember) will automatically scope to this organization if an organizationId is not explicitly provided. Use setActive to switch the current context.

    const setActive = async (organizationId: string) => {
      const { data, error } = await authClient.organization.setActive({
        organizationId,
      });
    };
  2. Configure Database and ORM Adapters

    main

    Better Auth supports direct database connections and ORM adapters.

    Direct Connections

    You can pass instances of:

    • pg.Pool (Postgres)
    • mysql2 pool
    • better-sqlite3 or bun:sqlite (SQLite)
    • postgres (postgres.js)
    • @neondatabase/serverless

    ORM Adapters

    Import adapters from specific paths:

    • better-auth/adapters/drizzle
    • better-auth/adapters/prisma
    • better-auth/adapters/mongodb

    Drizzle Configuration

    When using Drizzle, provide the provider value ("pg", "mysql", or "sqlite") which must match your driver.

    ⚠️ Critical: Model vs Table Names

    Better Auth uses adapter model names, NOT underlying database table names. If your Prisma model is User but the table is users, use modelName: "user" in your configuration.

  3. Manage Session Storage and Cookie Caching

    main

    Better Auth manages sessions using a hierarchy of storage and cookie strategies.

    Storage Priority

    1. Secondary Storage: If secondaryStorage (e.g., Redis/KV) is defined, sessions are stored there instead of the database.
    2. Database: Set session.storeSessionInDatabase: true to persist sessions to the database in addition to secondary storage.
    3. Stateless Mode: If no database is provided and cookieCache is used, the system operates in a fully stateless mode.

    Use session.cookieCache to configure how session data is cached in cookies:

    • compact (default): Uses Base64url + HMAC. Smallest footprint.
    • jwt: Standard JWT. Readable but signed.
    • jwe: Encrypted. Provides maximum security.

    Key Session Options

    • session.expiresIn: Duration before session expiry (default: 7 days).
    • session.updateAge: Interval to refresh the session.
    • session.cookieCache.maxAge: Expiry for the cookie cache.
    • session.cookieCache.version: Change this value to invalidate all existing sessions.
  4. Configure Trusted Devices and Session Security

    main

    Trusted Devices

    When verifying a code, pass trustDevice: true to remember the device. The default trust duration is 30 days, controlled by trustDeviceMaxAge in the server config. This duration is refreshed on every sign-in.

    Session Management

    2FA uses a temporary cookie during the verification phase. You can control its lifespan using twoFactorCookieMaxAge (default is 600 seconds / 10 minutes).

    Rate Limiting

    • Global: 3 requests per 10 seconds for all 2FA endpoints.
    • OTP Specific: You can set allowedAttempts in otpOptions to limit attempts per specific code.
  5. Configure teams within an organization

    main

    Teams allow grouping members within an organization.

    1. Enable: Set teams: { enabled: true } in the organization() plugin config.
    2. Create: Use authClient.organization.createTeam({ name: '...' }).
    3. Manage Members: Use addTeamMember({ teamId, userId }) and removeTeamMember({ teamId, userId }). Note that a user must be a member of the organization before they can be added to a team.
    4. Set Active Team: Use setActiveTeam({ teamId }) to switch the current team context.
    // Enable teams in server config
    organization({
      teams: {
        enabled: true
      }
    });
    
    // Client-side team management
    await authClient.organization.createTeam({ name: "Engineering" });
    await authClient.organization.addTeamMember({ teamId: "team-id", userId: "user-id" });
  6. Set up Drizzle with PostgreSQL

    main

    To use Drizzle with PostgreSQL in Better Auth, you must first initialize your Drizzle db instance using one of the supported drivers (node-postgres, postgres.js, or Neon serverless), then pass it to the drizzleAdapter within the betterAuth configuration.

    Additionally, ensure your drizzle.config.ts is configured to point to your auth schema so drizzle-kit can manage migrations.

    // 1. Initialize Drizzle (Example using node-postgres)
    import { drizzle } from "drizzle-orm/node-postgres"
    import { Pool } from "pg"
    import * as schema from "./auth-schema"
    
    const pool = new Pool({ connectionString: process.env.DATABASE_URL })
    export const db = drizzle(pool, { schema })
    
    // 2. Pass to Better Auth
    import { betterAuth } from "better-auth"
    import { drizzleAdapter } from "better-auth/adapters/drizzle"
    import { db } from "./db"
    
    export const auth = betterAuth({
      database: drizzleAdapter(db, { provider: "pg" }),
      // ...
    })
  7. Install Better Auth

    main

    To add authentication to your project, install the core package. Depending on your requirements, you may also need to install specific scoped packages for features like Passkeys, SSO, or Stripe integration.

    Core package:

    npm install better-auth

    Scoped packages:

    • @better-auth/passkey: WebAuthn/Passkey auth
    • @better-auth/sso: SAML/OIDC enterprise SSO
    • @better-auth/stripe: Stripe payments
    • @better-auth/scim: SCIM user provisioning
    • @better-auth/expo: React Native/Expo
  8. Configure and use OTP (Email/SMS)

    main

    The plugin supports One-Time Passwords (OTP) delivered via Email or SMS.

    Configuration

    In the server config, use otpOptions to define how codes are sent and validated:

    • sendOTP: An async function receiving { user, otp, ctx }. Use this to integrate your email/SMS provider.
    • period: Validity in minutes (default: 3).
    • digits: Number of digits (default: 6).
    • allowedAttempts: Max verification attempts (default: 5).
    • storeOTP: Storage strategy ("plain", "encrypted", or "hashed"). You can also provide a custom object with encrypt and decrypt functions.

    Usage

    • Send: authClient.twoFactor.sendOtp()
    • Verify: authClient.twoFactor.verifyOtp({ code, trustDevice: true })
    // Server Configuration Example
    export const auth = betterAuth({
      plugins: [
        twoFactor({
          otpOptions: {
            sendOTP: async ({ user, otp }) => {
              await sendEmail({
                to: user.email,
                subject: "Your verification code",
                text: `Your code is: ${otp}`,
              });
            },
            period: 5,
            digits: 6,
            allowedAttempts: 5,
            storeOTP: "encrypted",
          },
        }),
      ],
    });
    
    // Client Usage
    const verifyOtp = async (code: string) => {
      const { data, error } = await authClient.twoFactor.verifyOtp({
        code,
        trustDevice: true,
      });
    };
  9. Explain Better Auth error codes and messages

    main

    Use the explain-error command to receive a comprehensive breakdown of a specific Better Auth error. The command accepts either a specific error code or a full error message as an argument.

    For a given error, the tool provides:

    • A plain English explanation of the error's meaning.
    • A list of common causes.
    • Specific solutions and handling recommendations.
    • Production-ready code examples demonstrating how to catch the error, display user-friendly messages, and determine if a retry is appropriate.
    • Related error codes for context.
    • Links to the official Better Auth documentation.
    # Usage pattern
    explain-error [error_code or error_message]
  10. Implement Dynamic Access Control (Custom Roles)

    main

    If the default roles (owner, admin, member) are insufficient, you can enable Dynamic Access Control using the @better-auth/organization/addons package. This allows you to create custom roles with granular permissions.

    Workflow:

    1. Enable dynamicAccessControl: { enabled: true } in the organization() plugin.
    2. Create a role using authClient.organization.createRole and define its permission object (e.g., member: ['read']).
    import { dynamicAccessControl } from "@better-auth/organization/addons";
    
    // Server config
    organization({
      dynamicAccessControl: {
        enabled: true
      }
    });
    
    // Client-side: Create custom role
    await authClient.organization.createRole({
      role: "moderator",
      permission: {
        member: ["read"],
        invitation: ["read"],
      },
    });
  11. Set up Route Handlers for different frameworks

    main

    Better Auth requires a catch-all route handler to manage authentication requests. The implementation varies by framework:

    FrameworkFile PathImplementation
    Next.js App Routerapp/api/auth/[...all]/route.tstoNextJsHandler(auth) $\rightarrow$ export { GET, POST }
    Next.js Pagespages/api/auth/[...all].tstoNextJsHandler(auth) (default export)
    ExpressAny fileapp.all("/api/auth/*", toNodeHandler(auth))
    SvelteKitsrc/hooks.server.tssvelteKitHandler(auth)
    SolidStartRoute filesolidStartHandler(auth)
    HonoRoute fileauth.handler(c.req.raw)

    Note for Next.js Server Components: You must add the nextCookies() plugin to your server auth configuration.

  12. Create organizations

    main

    Organizations can be created via the client (where the creator is automatically assigned the owner role) or via the server API (for administrative actions).

    Client-side creation: Use authClient.organization.create. Requires name and slug.

    Server-side creation (on behalf of users): Use auth.api.createOrganization. This requires a userId for the new owner. Note that when using userId, you cannot also pass session headers.

    // Client-side
    const createOrg = async () => {
      const { data, error } = await authClient.organization.create({
        name: "My Company",
        slug: "my-company",
        logo: "https://example.com/logo.png",
        metadata: { plan: "pro" },
      });
    };
    
    // Server-side (Admin)
    await auth.api.createOrganization({
      body: {
        name: "Client Organization",
        slug: "client-org",
        userId: "user-id-who-will-be-owner",
      },
    });