Better Auth Cheatsheet

Framework-agnostic TypeScript authentication library — email/password, OAuth, sessions, 2FA & more

Security / Auth
Contents
📦

Overview & Installation

Better Auth is a framework-agnostic, TypeScript-first authentication library. It provides a complete auth solution with built-in support for email/password, OAuth, sessions, and extensibility via plugins.

Key Features

Installation

# Install better-auth
npm install better-auth

# Or with your preferred package manager
pnpm add better-auth
bun add better-auth
yarn add better-auth
# Generate database tables (after setting up auth config)
npx better-auth migrate

# Or generate the schema and migrate yourself
npx better-auth generate
⚙️

Server Setup

Basic Configuration

// auth.ts — server-side auth configuration
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  // Database connection
  database: {
    provider: "postgresql",   // or "mysql", "sqlite"
    url: process.env.DATABASE_URL,
  },

  // Email & password authentication
  emailAndPassword: {
    enabled: true,
    minPasswordLength: 8,
    maxPasswordLength: 128,
    requireEmailVerification: false,
  },

  // Session configuration
  session: {
    expiresIn: 60 * 60 * 24 * 7,   // 7 days (in seconds)
    updateAge: 60 * 60 * 24,       // Update session every 24h
    cookieCache: {
      enabled: true,
      maxAge: 60 * 5,              // Cache session in cookie for 5 min
    },
  },

  // Base URL of your app
  baseURL: process.env.BETTER_AUTH_URL,  // e.g. "http://localhost:3000"
  secret: process.env.BETTER_AUTH_SECRET,
});

Mount as Route Handler

// Express
import express from "express";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./auth";

const app = express();
app.all("/api/auth/*", toNodeHandler(auth));

// Hono
import { Hono } from "hono";
import { toHonoHandler } from "better-auth/hono";

const app = new Hono();
app.all("/api/auth/*", toHonoHandler(auth));

// Next.js (app/api/auth/[...all]/route.ts)
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "@/lib/auth";

export const { GET, POST } = toNextJsHandler(auth);
Environment variables: Set BETTER_AUTH_URL (your app URL) and BETTER_AUTH_SECRET (random 32+ char string). Generate a secret with openssl rand -base64 32.
💻

Client Setup

// lib/auth-client.ts — client-side auth instance
import { createAuthClient } from "better-auth/client";

export const authClient = createAuthClient({
  baseURL: "http://localhost:3000",  // Your server URL
});

// Destructure commonly used methods
export const {
  signIn,
  signUp,
  signOut,
  useSession,      // React hook
  getSession,      // Async getter
} = authClient;
📧

Email & Password Auth

Sign Up

import { authClient } from "@/lib/auth-client";

const { data, error } = await authClient.signUp.email({
  email: "user@example.com",
  password: "securePassword123",
  name: "John Doe",
  image: "https://example.com/avatar.jpg",  // optional
});

if (error) {
  console.error(error.message);  // e.g. "User already exists"
} else {
  console.log(data.user, data.session);
}

Sign In

const { data, error } = await authClient.signIn.email({
  email: "user@example.com",
  password: "securePassword123",
  callbackURL: "/dashboard",  // redirect after sign in (optional)
});

Sign Out

await authClient.signOut({
  fetchOptions: {
    onSuccess: () => {
      router.push("/login");  // redirect to login page
    },
  },
});

Password Reset

// Server config — enable password reset
emailAndPassword: {
  enabled: true,
  sendResetPassword: async ({ user, url }) => {
    // Send email with reset link
    await sendEmail({
      to: user.email,
      subject: "Reset your password",
      html: `<a href="${url}">Reset Password</a>`,
    });
  },
}

// Client — request reset
await authClient.forgetPassword({
  email: "user@example.com",
  redirectTo: "/reset-password",
});

// Client — submit new password (on reset page)
await authClient.resetPassword({
  newPassword: "newSecurePassword",
});
🔗

OAuth Providers

Server Configuration

export const auth = betterAuth({
  // ... other config
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
    discord: {
      clientId: process.env.DISCORD_CLIENT_ID!,
      clientSecret: process.env.DISCORD_CLIENT_SECRET!,
    },
    apple: {
      clientId: process.env.APPLE_CLIENT_ID!,
      clientSecret: process.env.APPLE_CLIENT_SECRET!,
    },
  },
});

Client — Social Sign In

// Sign in with Google
await authClient.signIn.social({
  provider: "google",
  callbackURL: "/dashboard",
});

// Sign in with GitHub
await authClient.signIn.social({
  provider: "github",
  callbackURL: "/dashboard",
});

Available Providers

ProviderKeyScopes Available
Googlegoogleemail, profile, openid
GitHubgithubuser:email, read:user
Discorddiscordidentify, email
Appleapplename, email
MicrosoftmicrosoftUser.Read, openid
Twitter/Xtwitterusers.read, tweet.read
Spotifyspotifyuser-read-email
Twitchtwitchuser:read:email
Redirect URI: Set your OAuth redirect URI to {baseURL}/api/auth/callback/{provider} in each provider's developer console.
🍪

Session Management

Get Current Session

// Client-side — async
const { data: session } = await authClient.getSession();
console.log(session?.user);    // { id, name, email, image, ... }
console.log(session?.session); // { id, userId, expiresAt, ... }

// Server-side — in API route or middleware
const session = await auth.api.getSession({
  headers: req.headers,  // Pass the request headers
});

if (!session) {
  return new Response("Unauthorized", { status: 401 });
}

Session Object Shape

{
  user: {
    id: "user_abc123",
    name: "John Doe",
    email: "john@example.com",
    emailVerified: true,
    image: "https://...",
    createdAt: "2024-01-01T00:00:00Z",
    updatedAt: "2024-01-01T00:00:00Z",
  },
  session: {
    id: "sess_xyz",
    userId: "user_abc123",
    expiresAt: "2024-01-08T00:00:00Z",
    ipAddress: "192.168.1.1",
    userAgent: "Mozilla/5.0 ...",
  }
}

List Active Sessions

// List all active sessions for current user
const { data: sessions } = await authClient.listSessions();

// Revoke a specific session (e.g. logout from another device)
await authClient.revokeSession({ id: sessionId });

// Revoke all other sessions
await authClient.revokeOtherSessions();
🛡️

Middleware & Route Protection

Next.js Middleware

// middleware.ts
import { betterAuth } from "better-auth/next-js";
import { auth } from "@/lib/auth";

export default async function middleware(req) {
  const session = await auth.api.getSession({
    headers: req.headers,
  });

  if (!session) {
    return NextResponse.redirect(new URL("/login", req.url));
  }
}

export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*"],
};

Server-Side Protection (API Routes)

// Express middleware
async function requireAuth(req, res, next) {
  const session = await auth.api.getSession({
    headers: req.headers,
  });
  
  if (!session) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  
  req.user = session.user;
  req.session = session.session;
  next();
}

app.get("/api/profile", requireAuth, (req, res) => {
  res.json(req.user);
});
👤

User Management

// Update user profile
await authClient.updateUser({
  name: "New Name",
  image: "https://new-avatar.com/pic.jpg",
});

// Change password
await authClient.changePassword({
  currentPassword: "oldPassword",
  newPassword: "newPassword",
  revokeOtherSessions: true,  // logout everywhere else
});

// Change email
await authClient.changeEmail({
  newEmail: "new@example.com",
  callbackURL: "/settings",
});

// Delete account
await authClient.deleteUser({
  password: "currentPassword",  // re-authenticate
});
🔐

Two-Factor Authentication (2FA)

Server Setup

import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";

export const auth = betterAuth({
  // ... other config
  plugins: [
    twoFactor({
      issuer: "MyApp",           // Shown in authenticator app
      otpOptions: {
        digits: 6,
        period: 30,              // seconds
      },
      backupCodes: {
        length: 10,              // number of backup codes
        customCodes: undefined,   // or provide your own
      },
    }),
  ],
});

Client Setup

import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [twoFactorClient()],
});

2FA Flow

// 1. Enable 2FA — returns TOTP URI & backup codes
const { data } = await authClient.twoFactor.enable({
  password: "currentPassword",
});
// data.totpURI → use to generate QR code
// data.backupCodes → show once to user

// 2. Verify setup with a TOTP code from authenticator
await authClient.twoFactor.verifyTotp({
  code: "123456",
});

// 3. During sign-in, if 2FA is enabled:
const { data, error } = await authClient.signIn.email({
  email: "user@example.com",
  password: "password",
});
// If 2FA is required, data.twoFactorRedirect = true

// 4. Submit TOTP code
await authClient.twoFactor.verifyTotp({
  code: "123456",
});

// 5. Disable 2FA
await authClient.twoFactor.disable({
  password: "currentPassword",
});
🧩

Plugins & Extensions

PluginImportDescription
Two FactortwoFactorTOTP, backup codes, recovery
Magic LinkmagicLinkPasswordless email login
OrganizationorganizationMulti-tenant org management
AdminadminUser management, impersonation
UsernameusernameAdd username field to users
Phone NumberphoneNumberPhone-based auth with OTP
AnonymousanonymousLink anonymous users to accounts
BearerbearerAPI key / bearer token auth

Using Multiple Plugins

import { twoFactor, magicLink, organization } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    twoFactor({ issuer: "MyApp" }),
    magicLink({
      sendMagicLink: async ({ email, url }) => {
        await sendEmail({ to: email, subject: "Login Link", html: `<a href="${url}">Login</a>` });
      },
    }),
    organization(),
  ],
});
🗄️

Database Adapters

Prisma

import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
});

Drizzle

import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./db";

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",  // or "mysql", "sqlite"
  }),
});

Database Tables Created

TablePurposeKey Columns
userUser accountsid, name, email, emailVerified, image
sessionActive sessionsid, userId, expiresAt, ipAddress, userAgent
accountOAuth linked accountsid, userId, providerId, accountId
verificationEmail verification tokensid, identifier, value, expiresAt
⚛️

React Integration

// hooks/useAuth.ts
import { authClient } from "@/lib/auth-client";

export function useAuth() {
  const { data: session, isPending, error } = authClient.useSession();
  
  return {
    user: session?.user ?? null,
    session: session?.session ?? null,
    isLoading: isPending,
    isAuthenticated: !!session?.user,
    error,
  };
}

// components/ProtectedPage.tsx
export function ProtectedPage({ children }) {
  const { user, isLoading, isAuthenticated } = useAuth();

  if (isLoading) return <div>Loading...</div>;
  if (!isAuthenticated) return <Navigate to="/login" />;

  return children;
}

// components/LoginButton.tsx
export function LoginButton() {
  return (
    <button onClick={() => authClient.signIn.social({ provider: "google" })}>
      Sign in with Google
    </button>
  );
}

Next.js Integration

File Structure

├── lib/
│   ├── auth.ts              # Server-side auth config
│   └── auth-client.ts       # Client-side auth instance
├── app/
│   ├── api/auth/[...all]/
│   │   └── route.ts         # Auth API route handler
│   ├── (auth)/
│   │   ├── login/page.tsx
│   │   └── register/page.tsx
│   └── (protected)/
│       ├── layout.tsx        # Auth check wrapper
│       └── dashboard/page.tsx
└── middleware.ts             # Route protection

Server Components

// app/(protected)/dashboard/page.tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const session = await auth.api.getSession({
    headers: await headers(),
  });

  if (!session) redirect("/login");

  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
      <p>{session.user.email}</p>
    </div>
  );
}
Pro tip: Use auth.api.getSession() in server components and API routes. Use authClient.useSession() in client components. Never import the server auth config in client code.
Back to top