Better Auth is a comprehensive, framework-agnostic authentication and authorization library for TypeScript that provides enterprise-grade features through a flexible plugin architecture. It handles everything from basic email/password authentication to advanced features like multi-tenancy, two-factor authentication, passkeys, WebAuthn, phone authentication, username authentication, anonymous sessions, API keys, JWT tokens, Sign-In with Ethereum (SIWE), OAuth proxy, SSO/SAML, SCIM (System for Cross-domain Identity Management), Stripe integration, OpenID Connect (OIDC) provider capabilities, Model Context Protocol (MCP) OAuth authentication, captcha verification, Google One Tap authentication, OpenAPI documentation generation, and last login method tracking. The library supports multiple database adapters (Prisma, Drizzle, MongoDB, Memory), integrates seamlessly with popular frameworks (Next.js, SvelteKit, Solid Start, React Start (TanStack Start), Expo/React Native), and provides type-safe client libraries for React, Vue, Svelte, Solid, and Lynx.
The architecture centers around a server-side betterAuth() instance that handles authentication logic and exposes REST API endpoints, paired with framework-specific client libraries that provide type-safe methods and hooks for managing authentication state. Plugins extend functionality by adding endpoints, database schemas, hooks, and middleware, enabling complex features like organizations, two-factor authentication, passkey support, phone number authentication, username authentication, anonymous sessions, bearer tokens, API keys, access control, multi-session support, device authorization, OpenID Connect provider capabilities for building OAuth 2.0 authorization servers, OAuth proxy for development environments, Model Context Protocol (MCP) OAuth integration for AI agents, Stripe payment and subscription management, SSO/SAML integration, SCIM user provisioning for enterprise identity management, captcha protection, Google One Tap, automatic OpenAPI documentation, last login method tracking, custom session handling, and additional user/session fields with minimal configuration. The system is designed for production use with built-in rate limiting, CSRF protection, secure cookie handling, password breach checking via HaveIBeenPwned integration, and comprehensive error handling.
Creates a Better Auth server instance with database, social providers, and plugins.
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { organization, twoFactor } from "better-auth/plugins";
import { passkey } from "@better-auth/passkey";
import { db } from "./db";
export const auth = betterAuth({
appName: "My Application",
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
secret: process.env.BETTER_AUTH_SECRET,
database: drizzleAdapter(db, {
provider: "pg",
schema: schema,
}),
emailAndPassword: {
enabled: true,
minPasswordLength: 8,
async sendResetPassword({ user, url, token }) {
await sendEmail({
to: user.email,
subject: "Reset your password",
html: `Click <a href="${url}">here</a> to reset your password`,
});
},
},
emailVerification: {
async sendVerificationEmail({ user, url, token }) {
await sendEmail({
to: user.email,
subject: "Verify your email",
html: `Click <a href="${url}">here</a> to verify`,
});
},
sendOnSignUp: true,
autoSignInAfterVerification: true,
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
accessType: "offline",
},
},
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // 1 day
freshAge: 60 * 10, // 10 minutes
},
rateLimit: {
enabled: true,
window: 60,
max: 10,
},
plugins: [
organization(),
twoFactor({
issuer: "My App",
}),
passkey(),
],
});
export type Session = typeof auth.$Infer.Session;Exposes Better Auth REST endpoints in your application framework.
// Next.js App Router: app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);
// SvelteKit: src/routes/api/auth/[...all]/+server.ts
import { auth } from "$lib/auth";
import { svelteKitHandler } from "better-auth/svelte-kit";
export const { GET, POST } = svelteKitHandler(auth);
// Solid Start: src/routes/api/auth/[...all].ts
import { auth } from "~/lib/auth";
import { solidStartHandler } from "better-auth/solid-start";
export const { GET, POST } = solidStartHandler(auth);
// React Start (TanStack Start)
import { auth } from "~/lib/auth";
import { reactStartHandler } from "better-auth/react-start";
export const { GET, POST } = reactStartHandler(auth);
// Express.js
import express from "express";
import { auth } from "./auth";
const app = express();
app.all("/api/auth/*", async (req, res) => {
return auth.handler(new Request(
`${req.protocol}://${req.get("host")}${req.originalUrl}`,
{
method: req.method,
headers: req.headers as HeadersInit,
body: req.method !== "GET" ? JSON.stringify(req.body) : undefined,
}
)).then(response => {
res.status(response.status);
response.headers.forEach((value, key) => res.setHeader(key, value));
return response.text().then(body => res.send(body));
});
});Creates a type-safe client for authentication operations with framework-specific hooks.
// React Client
import { createAuthClient } from "better-auth/react";
import {
organizationClient,
twoFactorClient,
} from "better-auth/client/plugins";
import { passkeyClient } from "@better-auth/passkey/client";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
plugins: [
organizationClient(),
twoFactorClient({
twoFactorPage: "/two-factor",
onTwoFactorRedirect() {
window.location.href = "/two-factor";
},
}),
passkeyClient(),
],
fetchOptions: {
onError(error) {
if (error.error.status === 429) {
toast.error("Too many requests. Please try again later.");
} else if (error.error.status === 401) {
console.error("Unauthorized");
}
},
onSuccess(data) {
console.log("Auth action successful:", data);
},
},
});
export const {
signIn,
signUp,
signOut,
useSession,
organization,
twoFactor,
passkey,
} = authClient;
// Vue Client
import { createAuthClient } from "better-auth/vue";
export const authClient = createAuthClient({ /* config */ });
// Svelte Client
import { createAuthClient } from "better-auth/svelte";
export const authClient = createAuthClient({ /* config */ });
// Solid Client
import { createAuthClient } from "better-auth/solid";
export const authClient = createAuthClient({ /* config */ });
// Lynx Client
import { createAuthClient } from "better-auth/lynx";
export const authClient = createAuthClient({ /* config */ });Handles email/password authentication with validation and session management.
import { authClient } from "./auth-client";
// Email/Password Sign Up
try {
const { data, error } = await authClient.signUp.email({
name: "John Doe",
email: "john@example.com",
password: "SecurePassword123!",
image: "https://example.com/avatar.jpg", // optional
callbackURL: "/dashboard", // redirect after sign up
});
if (error) {
console.error("Sign up failed:", error.message);
return;
}
console.log("User created:", data.user);
console.log("Session created:", data.session);
} catch (err) {
console.error("Unexpected error:", err);
}
// Email/Password Sign In
const { data, error } = await authClient.signIn.email({
email: "john@example.com",
password: "SecurePassword123!",
rememberMe: true, // extends session duration
callbackURL: "/dashboard",
});
// Sign Out
await authClient.signOut({
fetchOptions: {
onSuccess() {
window.location.href = "/";
},
},
});
// Sign Out from All Devices
await authClient.signOut({
revokeAllSessions: true,
});Initiates OAuth flows with third-party providers for passwordless sign-in.
import { authClient } from "./auth-client";
// GitHub OAuth Sign In
await authClient.signIn.social({
provider: "github",
callbackURL: "/dashboard",
});
// Google OAuth Sign In
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
// Discord, Microsoft, Apple, etc.
await authClient.signIn.social({
provider: "discord",
callbackURL: "/dashboard",
});
// Link Additional Social Account
await authClient.linkSocial({
provider: "github",
callbackURL: "/settings/accounts",
});
// Unlink Social Account
await authClient.unlinkAccount({
accountId: "acc_123456",
});
// List All Linked Accounts
const { data } = await authClient.listAccounts();
data?.forEach(account => {
console.log(`${account.provider}: ${account.providerId}`);
});Retrieves and monitors authentication state in React components.
"use client";
import { useSession } from "./auth-client";
import { Loader } from "./components/loader";
export function ProfilePage() {
const { data: session, isPending, error } = useSession();
if (isPending) {
return <Loader />;
}
if (error) {
return <div>Error loading session: {error.message}</div>;
}
if (!session) {
return <div>Not authenticated. Please sign in.</div>;
}
return (
<div>
<h1>Welcome, {session.user.name}!</h1>
<p>Email: {session.user.email}</p>
<p>Email verified: {session.user.emailVerified ? "Yes" : "No"}</p>
<img src={session.user.image} alt="Profile" />
<div>
<h2>Session Info</h2>
<p>Session ID: {session.session.id}</p>
<p>Expires: {new Date(session.session.expiresAt).toLocaleString()}</p>
<p>IP Address: {session.session.ipAddress}</p>
<p>User Agent: {session.session.userAgent}</p>
</div>
</div>
);
}
// Update User Profile
import { authClient } from "./auth-client";
async function updateProfile() {
const { data, error } = await authClient.updateUser({
name: "John Smith",
image: "https://example.com/new-avatar.jpg",
});
if (!error) {
console.log("Profile updated:", data);
}
}
// Change Email
await authClient.changeEmail({
newEmail: "newemail@example.com",
callbackURL: "/verify-new-email",
});
// Change Password
await authClient.changePassword({
currentPassword: "OldPassword123!",
newPassword: "NewPassword456!",
revokeOtherSessions: true,
});Validates sessions in server components, API routes, and middleware.
// Next.js App Router Server Component
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function ProtectedPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect("/sign-in");
}
return (
<div>
<h1>Protected Content</h1>
<p>Welcome, {session.user.name}!</p>
</div>
);
}
// Next.js API Route
import { auth } from "@/lib/auth";
export async function GET(req: Request) {
const session = await auth.api.getSession({
headers: req.headers,
});
if (!session) {
return new Response("Unauthorized", { status: 401 });
}
// Perform authenticated operation
const data = await fetchUserData(session.user.id);
return Response.json({ data });
}
// SvelteKit Load Function
import { auth } from "$lib/auth";
export async function load({ request }) {
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session) {
throw redirect(302, "/sign-in");
}
return { session };
}
// Middleware for Route Protection
import { auth } from "@/lib/auth";
export async function middleware(request: Request) {
const session = await auth.api.getSession({
headers: request.headers,
});
const isProtectedRoute = request.url.includes("/dashboard");
if (isProtectedRoute && !session) {
return Response.redirect(new URL("/sign-in", request.url));
}
}Connects Better Auth to your database using various ORMs and drivers.
// Drizzle ORM Adapter (PostgreSQL, MySQL, SQLite)
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
const client = postgres(process.env.DATABASE_URL!);
const db = drizzle(client, { schema });
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // "pg" | "mysql" | "sqlite"
schema: schema,
usePlural: false, // Use singular table names
camelCase: false, // Use snake_case column names
}),
});
// Prisma Adapter
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",
usePlural: false,
}),
});
// MongoDB Adapter
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import { MongoClient } from "mongodb";
const mongoClient = new MongoClient(process.env.MONGODB_URL!);
await mongoClient.connect();
export const auth = betterAuth({
database: mongodbAdapter(mongoClient.db("auth")),
});
// Memory Adapter (Testing Only)
import { memoryAdapter } from "better-auth/adapters/memory";
export const auth = betterAuth({
database: memoryAdapter(),
});Enables team/workspace management with roles, permissions, and invitations.
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
async sendInvitationEmail({ email, organization, inviter, url }) {
await sendEmail({
to: email,
subject: `Join ${organization.name}`,
html: `${inviter.user.name} invited you to join ${organization.name}. <a href="${url}">Accept invitation</a>`,
});
},
allowUserToCreateOrganization: true,
organizationLimit: 10,
roles: {
owner: ["create", "read", "update", "delete", "invite", "remove"],
admin: ["read", "update", "invite"],
member: ["read"],
},
}),
],
});
// Client Usage
import { organizationClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [organizationClient()],
});
// Create Organization
const { data } = await authClient.organization.create({
name: "Acme Corporation",
slug: "acme-corp",
logo: "https://example.com/logo.png",
metadata: { industry: "Technology" },
});
// Invite Member
await authClient.organization.inviteMember({
email: "member@example.com",
role: "admin",
organizationId: data.id,
});
// List Members
const { data: members } = await authClient.organization.listMembers({
organizationId: "org_123",
});
// Update Member Role
await authClient.organization.updateMemberRole({
memberId: "mem_456",
role: "owner",
organizationId: "org_123",
});
// Remove Member
await authClient.organization.removeMember({
memberId: "mem_456",
organizationId: "org_123",
});
// Set Active Organization
await authClient.organization.setActive({
organizationId: "org_123",
});
// Check Permissions
const hasPermission = await authClient.organization.hasPermission({
permissions: {
post: ["create", "delete"],
user: ["invite"],
},
});Adds TOTP authenticator apps, backup codes, and OTP support for enhanced security.
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
twoFactor({
issuer: "My Application",
otpOptions: {
async sendOTP({ user, otp }) {
await sendEmail({
to: user.email,
subject: "Your verification code",
html: `Your code is: ${otp}`,
});
},
period: 300, // 5 minutes
length: 6,
},
backupCodeLength: 10,
backupCodeCount: 10,
}),
],
});
// Client Usage
import { twoFactorClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [
twoFactorClient({
twoFactorPage: "/two-factor",
onTwoFactorRedirect() {
router.push("/two-factor");
},
}),
],
});
// Enable 2FA
const { data } = await authClient.twoFactor.enable({
password: "userPassword123",
});
console.log("TOTP URI:", data.totpURI); // For QR code generation
console.log("Backup codes:", data.backupCodes);
// Verify TOTP Setup
await authClient.twoFactor.verifyTotp({
code: "123456",
});
// Send OTP via Email
await authClient.twoFactor.sendOtp();
// Verify OTP
await authClient.twoFactor.verifyOtp({
code: "123456",
});
// Use Backup Code
await authClient.twoFactor.useBackupCode({
code: "ABCD-1234-EFGH",
});
// Generate New Backup Codes
const { data: newCodes } = await authClient.twoFactor.regenerateBackupCodes({
password: "userPassword123",
});
// Disable 2FA
await authClient.twoFactor.disable({
password: "userPassword123",
});Implements passwordless authentication using biometric or hardware security keys.
// Install the passkey package
// npm install @better-auth/passkey
import { betterAuth } from "better-auth";
import { passkey } from "@better-auth/passkey";
export const auth = betterAuth({
plugins: [
passkey({
rpName: "My Application",
rpID: "example.com",
origin: "https://example.com",
}),
],
});
// Client Usage
import { passkeyClient } from "@better-auth/passkey/client";
const authClient = createAuthClient({
plugins: [passkeyClient()],
});
// Register Passkey (must be signed in)
try {
const { data } = await authClient.passkey.addPasskey({
name: "MacBook Pro Touch ID",
});
console.log("Passkey registered:", data.id);
} catch (error) {
console.error("Passkey registration failed:", error);
}
// Sign In with Passkey
await authClient.passkey.signIn({
callbackURL: "/dashboard",
});
// List User Passkeys
const { data: passkeys } = await authClient.passkey.listPasskeys();
passkeys?.forEach(key => {
console.log(`${key.name}: ${key.createdAt}`);
});
// Delete Passkey
await authClient.passkey.deletePasskey({
id: "passkey_123",
});
// Check Passkey Support
if (authClient.passkey.isSupported()) {
console.log("Passkeys are supported in this browser");
}Sends one-time sign-in links via email for passwordless authentication.
import { betterAuth } from "better-auth";
import { magicLink } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
magicLink({
async sendMagicLink({ email, url, token }) {
await sendEmail({
to: email,
subject: "Sign in to your account",
html: `
<h1>Sign in to My Application</h1>
<p>Click the link below to sign in:</p>
<a href="${url}">Sign in</a>
<p>This link expires in 5 minutes.</p>
`,
});
},
expiresIn: 300, // 5 minutes
disableSignUp: false,
}),
],
});
// Client Usage
import { magicLinkClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [magicLinkClient()],
});
// Send Magic Link
const { data, error } = await authClient.magicLink.sendMagicLink({
email: "user@example.com",
callbackURL: "/dashboard",
});
if (!error) {
console.log("Magic link sent! Check your email.");
}
// The verification happens automatically when user clicks the link
// The URL will be: /api/auth/magic-link/verify?token=xxxSends one-time password codes via email for verification and sign-in.
import { betterAuth } from "better-auth";
import { emailOTP } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
emailOTP({
async sendVerificationOTP({ email, otp, type }) {
await sendEmail({
to: email,
subject: type === "sign-in" ? "Sign in code" : "Verification code",
html: `Your verification code is: <strong>${otp}</strong>`,
});
},
otpLength: 6,
expiresIn: 300, // 5 minutes
sendOnSignUp: true,
disableSignUp: false,
}),
],
});
// Client Usage
import { emailOTPClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [emailOTPClient()],
});
// Send OTP
await authClient.emailOTP.sendVerificationOtp({
email: "user@example.com",
type: "sign-in",
});
// Verify OTP
const { data, error } = await authClient.emailOTP.verifyEmail({
email: "user@example.com",
otp: "123456",
});
if (!error) {
console.log("Signed in successfully:", data.user);
}Enables phone number registration and SMS-based OTP verification.
import { betterAuth } from "better-auth";
import { phoneNumber } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
phoneNumber({
async sendOTP({ phoneNumber, otp }) {
await sendSMS({
to: phoneNumber,
message: `Your verification code is: ${otp}`,
});
},
otpLength: 6,
expiresIn: 300, // 5 minutes
}),
],
});
// Client Usage
import { phoneNumberClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [phoneNumberClient()],
});
// Send Phone OTP
await authClient.phoneNumber.sendOtp({
phoneNumber: "+1234567890",
});
// Verify Phone OTP and Sign In
const { data, error } = await authClient.phoneNumber.verifyOtp({
phoneNumber: "+1234567890",
otp: "123456",
});
if (!error) {
console.log("Signed in with phone:", data.user);
}Adds username-based authentication alongside email/password.
import { betterAuth } from "better-auth";
import { username } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
username({
minLength: 3,
maxLength: 20,
allowedCharacters: /^[a-zA-Z0-9_-]+$/,
}),
],
});
// Client Usage
import { usernameClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [usernameClient()],
});
// Sign Up with Username
await authClient.signUp.username({
username: "johndoe",
password: "SecurePassword123!",
name: "John Doe",
email: "john@example.com", // optional
});
// Sign In with Username
await authClient.signIn.username({
username: "johndoe",
password: "SecurePassword123!",
});Allows users to create temporary anonymous sessions without credentials.
import { betterAuth } from "better-auth";
import { anonymous } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
anonymous({
sessionDuration: 60 * 60 * 24 * 30, // 30 days
}),
],
});
// Client Usage
import { anonymousClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [anonymousClient()],
});
// Create Anonymous Session
const { data } = await authClient.anonymous.signIn();
console.log("Anonymous session:", data.session);
// Link Anonymous Account to Email
await authClient.anonymous.linkEmail({
email: "user@example.com",
password: "SecurePassword123!",
});Provides administrative endpoints for managing users, sessions, and system operations.
import { betterAuth } from "better-auth";
import { admin } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
admin({
// Optional: customize admin role check
async isAdmin(user) {
return user.role === "admin" || user.email.endsWith("@company.com");
},
}),
],
});
// Client Usage
import { adminClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [adminClient()],
});
// List All Users
const { data: users } = await authClient.admin.listUsers({
limit: 50,
offset: 0,
sortBy: "createdAt",
sortDirection: "desc",
filterBy: "email",
filterValue: "@company.com",
});
// Create User (Admin)
await authClient.admin.createUser({
email: "newuser@example.com",
password: "TempPassword123!",
name: "New User",
role: "user",
emailVerified: true,
});
// Update User
await authClient.admin.updateUser({
userId: "user_123",
data: {
name: "Updated Name",
role: "admin",
emailVerified: true,
},
});
// Ban User
await authClient.admin.banUser({
userId: "user_123",
reason: "Violation of terms",
banUntil: new Date("2025-12-31"),
});
// Unban User
await authClient.admin.unbanUser({
userId: "user_123",
});
// Impersonate User
await authClient.admin.impersonateUser({
userId: "user_123",
});
// List User Sessions
const { data: sessions } = await authClient.admin.listUserSessions({
userId: "user_123",
});
// Revoke User Session
await authClient.admin.revokeUserSession({
sessionId: "session_456",
});
// Delete User
await authClient.admin.deleteUser({
userId: "user_123",
});Enables stateless authentication using bearer tokens for API access.
import { betterAuth } from "better-auth";
import { bearer } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
bearer({
expiresIn: 60 * 60 * 24 * 7, // 7 days
}),
],
});
// Client Usage
import { bearerClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [bearerClient()],
});
// Generate Bearer Token
const { data } = await authClient.bearer.generate();
console.log("Access token:", data.accessToken);
// Use Bearer Token in API Calls
fetch("/api/protected", {
headers: {
Authorization: `Bearer ${data.accessToken}`,
},
});
// Server-side Bearer Token Validation
const session = await auth.api.getSession({
headers: {
authorization: `Bearer ${token}`,
},
});Provides long-lived API keys for server-to-server authentication.
import { betterAuth } from "better-auth";
import { apiKey } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
apiKey({
// Optional: API key headers to check (default: "x-api-key")
apiKeyHeaders: "x-api-key", // or ["x-api-key", "authorization"]
// Optional: default key length (default: 64)
defaultKeyLength: 64,
// Optional: key expiration settings
keyExpiration: {
defaultExpiresIn: 365, // days
minExpiresIn: 1,
maxExpiresIn: 365,
disableCustomExpiresTime: false,
},
// Optional: enable metadata
enableMetadata: true,
// Optional: disable key hashing (not recommended)
disableKeyHashing: false,
// Optional: require name for API keys
requireName: true,
// Optional: enable sessions for API keys
enableSessionForAPIKeys: true,
// Optional: rate limiting
rateLimit: {
enabled: true,
timeWindow: 60 * 60 * 24 * 1000, // 24 hours
maxRequests: 10,
},
}),
],
});
// Client Usage
import { apiKeyClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [apiKeyClient()],
});
// Create API Key
const { data } = await authClient.apiKey.create({
name: "Production API Key",
expiresIn: 60 * 60 * 24 * 365, // 1 year (in seconds)
metadata: { environment: "production" }, // optional
});
console.log("API Key:", data.key);
console.log("Starts with:", data.startsWith); // First 6 characters
// Get API Key
const { data: apiKey } = await authClient.apiKey.get({
id: "key_123",
});
// Update API Key
await authClient.apiKey.update({
id: "key_123",
name: "Updated API Key Name",
enabled: false, // disable the key
metadata: { updated: true },
});
// List API Keys
const { data: keys } = await authClient.apiKey.list();
keys?.forEach(key => {
console.log(`${key.name}: ${key.startsWith}... (expires: ${key.expiresAt})`);
});
// Delete API Key
await authClient.apiKey.delete({
id: "key_123",
});
// Server-side: Delete All Expired API Keys
await auth.api.deleteAllExpiredApiKeys();
// Server-side API Key Validation
const session = await auth.api.getSession({
headers: {
"x-api-key": apiKey,
},
});
// Server-side: Verify API Key
const { data: verified } = await auth.api.verifyApiKey({
key: apiKey,
});
if (verified) {
console.log("Valid API key for user:", verified.userId);
}Generates and validates JSON Web Tokens for session management.
import { betterAuth } from "better-auth";
import { jwt } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
jwt({
algorithm: "HS256",
expiresIn: "7d",
issuer: "https://example.com",
audience: ["https://api.example.com"],
}),
],
});
// Client Usage
import { jwtClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [jwtClient()],
});
// Generate JWT
const { data } = await authClient.jwt.generate();
console.log("JWT:", data.token);
// Decode JWT
const decoded = await authClient.jwt.decode({
token: data.token,
});
console.log("JWT payload:", decoded);Allows users to maintain multiple concurrent sessions across devices.
import { betterAuth } from "better-auth";
import { multiSession } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
multiSession({
maximumSessions: 5, // Max sessions per user
}),
],
});
// Client Usage
import { multiSessionClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [multiSessionClient()],
});
// List Active Sessions
const { data: sessions } = await authClient.multiSession.listSessions();
sessions?.forEach(session => {
console.log(`Device: ${session.userAgent}, Last active: ${session.updatedAt}`);
});
// Revoke Specific Session
await authClient.multiSession.revokeSession({
sessionId: "session_456",
});
// Revoke All Other Sessions
await authClient.multiSession.revokeOtherSessions();Checks passwords against known data breaches for enhanced security.
import { betterAuth } from "better-auth";
import { haveibeenpwned } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
haveibeenpwned({
// Automatically check on sign-up and password change
checkOnSignUp: true,
checkOnPasswordChange: true,
// Optional: warn users instead of blocking
blockCompromisedPasswords: true,
}),
],
});
// The plugin automatically checks passwords against the HIBP database
// and rejects compromised passwords during sign-up and password changesEnables Web3 authentication using Ethereum wallet signatures.
import { betterAuth } from "better-auth";
import { siwe } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
siwe({
domain: "example.com",
uri: "https://example.com",
statement: "Sign in with your Ethereum account",
}),
],
});
// Client Usage
import { siweClient } from "better-auth/client/plugins";
import { ethers } from "ethers";
const authClient = createAuthClient({
plugins: [siweClient()],
});
// Get Nonce
const { data: nonce } = await authClient.siwe.getNonce();
// Create and Sign Message
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
const message = await authClient.siwe.prepareMessage({
address,
nonce: nonce.nonce,
});
const signature = await signer.signMessage(message);
// Verify and Sign In
await authClient.siwe.signIn({
message,
signature,
});Turns Better Auth into a full OpenID Connect (OIDC) provider, allowing your application to act as an OAuth 2.0 authorization server for other applications.
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
oidcProvider({
// OIDC configuration
codeExpiresIn: 600, // 10 minutes
accessTokenExpiresIn: 3600, // 1 hour
refreshTokenExpiresIn: 604800, // 7 days
// Supported scopes
scopes: ["openid", "profile", "email", "offline_access"],
// Optional: use JWT plugin for signing tokens
useJWTPlugin: true,
// Optional: trusted clients (no consent screen)
trustedClients: [
{
clientId: "my-trusted-app",
clientSecret: "secret",
name: "My Trusted App",
redirectUrls: ["https://app.example.com/callback"],
type: "web",
},
],
// Optional: allow dynamic client registration
allowDynamicClientRegistration: true,
// Optional: require PKCE for all clients
requirePKCE: true,
// Optional: add custom claims to tokens
async getAdditionalUserInfoClaim(user, scopes, client) {
return {
organization: user.metadata?.organization,
role: user.role,
};
},
}),
],
});
// The plugin exposes the following endpoints:
// - GET /.well-known/openid-configuration (OIDC discovery)
// - GET /oauth2/authorize (authorization endpoint)
// - POST /oauth2/consent (consent handling)
// - POST /oauth2/token (token exchange)
// - GET /oauth2/userinfo (user info endpoint)
// - POST /oauth2/register (dynamic client registration)
// - GET /oauth2/client/:id (get client info)
// - GET/POST /oauth2/endsession (logout endpoint)
// Client Registration Example
const response = await fetch("/api/auth/oauth2/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
redirect_uris: ["https://client.example.com/callback"],
client_name: "My OAuth Client",
token_endpoint_auth_method: "client_secret_post",
grant_types: ["authorization_code", "refresh_token"],
}),
});
const { client_id, client_secret } = await response.json();
// Authorization Code Flow Example
// 1. Redirect user to authorization endpoint
const authURL = new URL("/api/auth/oauth2/authorize", "https://auth.example.com");
authURL.searchParams.set("client_id", client_id);
authURL.searchParams.set("redirect_uri", "https://client.example.com/callback");
authURL.searchParams.set("response_type", "code");
authURL.searchParams.set("scope", "openid profile email");
authURL.searchParams.set("state", "random-state");
window.location.href = authURL.toString();
// 2. Exchange code for tokens
const tokenResponse = await fetch("/api/auth/oauth2/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: authorizationCode,
redirect_uri: "https://client.example.com/callback",
client_id: client_id,
client_secret: client_secret,
}),
});
const { access_token, refresh_token, id_token } = await tokenResponse.json();
// 3. Get user info
const userInfo = await fetch("/api/auth/oauth2/userinfo", {
headers: { Authorization: `Bearer ${access_token}` },
}).then(res => res.json());
console.log("User info:", userInfo);Enables OAuth authentication in development environments by proxying OAuth callbacks through your production server, solving redirect URI mismatch issues.
import { betterAuth } from "better-auth";
import { oAuthProxy } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
oAuthProxy({
// Optional: specify current URL (auto-detected by default)
currentURL: process.env.CURRENT_URL,
// Optional: production URL (defaults to BETTER_AUTH_URL)
productionURL: process.env.BETTER_AUTH_URL,
}),
],
});
// How it works:
// 1. In development (localhost:3000), initiate OAuth sign-in
// 2. Plugin detects non-production environment
// 3. Modifies callback URL to point to production server
// 4. Production server receives OAuth callback
// 5. Production server encrypts session cookies
// 6. Redirects back to development with encrypted cookies
// 7. Development server decrypts and sets cookies locally
// No client-side changes needed - plugin handles everything automatically
await authClient.signIn.social({
provider: "github",
callbackURL: "/dashboard",
});
// The plugin exposes: /oauth-proxy-callback endpoint
// This endpoint handles the proxy callback and cookie forwardingEnables OAuth authentication for AI agents and Model Context Protocol clients, allowing AI tools to securely authenticate with your Better Auth instance.
import { betterAuth } from "better-auth";
import { mcp } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
mcp({
// Login page where users will be redirected to authenticate
loginPage: "/sign-in",
// Optional: resource identifier (defaults to origin)
resource: "https://example.com",
// Optional: customize OIDC configuration
oidcConfig: {
// OIDC provider options
scopes: ["openid", "profile", "email"],
accessTokenExpiresIn: 3600, // 1 hour
},
}),
],
});
// The MCP plugin exposes OAuth 2.0 endpoints for AI agents:
// - GET /.well-known/oauth-authorization-server (OAuth server metadata)
// - GET /.well-known/oauth-protected-resource (Protected resource metadata)
// - GET /mcp/authorize (Authorization endpoint)
// - POST /mcp/token (Token endpoint)
// - GET /mcp/userinfo (User info endpoint)
// - GET /mcp/jwks (JSON Web Key Set)
// - POST /mcp/register (Client registration)
// AI agents can use these endpoints to:
// 1. Discover authentication capabilities
// 2. Initiate OAuth flows
// 3. Exchange authorization codes for tokens
// 4. Access user information
// Example: AI agent authentication flow
// 1. Agent discovers OAuth endpoints from /.well-known/oauth-authorization-server
// 2. Agent redirects user to /mcp/authorize with client credentials
// 3. User authenticates through loginPage
// 4. Agent receives authorization code
// 5. Agent exchanges code for access token at /mcp/token
// 6. Agent uses access token to access protected resources
// Server-side: Verify MCP OAuth tokens
const session = await auth.api.getSession({
headers: {
authorization: `Bearer ${accessToken}`,
},
});
if (session) {
console.log("Authenticated AI agent for user:", session.user);
}Implements OAuth 2.0 device authorization flow for limited-input devices.
import { betterAuth } from "better-auth";
import { deviceAuthorization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
deviceAuthorization({
deviceCodeExpiresIn: 600, // 10 minutes
userCodeLength: 8,
pollingInterval: 5, // seconds
}),
],
});
// Client Usage (Device)
import { deviceAuthorizationClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [deviceAuthorizationClient()],
});
// Request Device Code
const { data } = await authClient.deviceAuthorization.requestCode();
console.log("User code:", data.userCode);
console.log("Verification URL:", data.verificationUri);
// Poll for Authorization
const result = await authClient.deviceAuthorization.pollForAuthorization({
deviceCode: data.deviceCode,
});
// User visits verification URL and enters user code to authorizeProvides fine-grained permission and role-based access control.
import { betterAuth } from "better-auth";
import { access } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
access({
permissions: {
post: ["create", "read", "update", "delete"],
comment: ["create", "read", "delete"],
user: ["read", "update"],
},
roles: {
admin: {
post: ["create", "read", "update", "delete"],
comment: ["create", "read", "delete"],
user: ["read", "update"],
},
editor: {
post: ["create", "read", "update"],
comment: ["create", "read"],
},
viewer: {
post: ["read"],
comment: ["read"],
},
},
}),
],
});
// Client Usage
import { accessClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [accessClient()],
});
// Check Permission
const canEdit = await authClient.access.hasPermission({
resource: "post",
action: "update",
});
// Get User Permissions
const { data: permissions } = await authClient.access.getPermissions();
console.log("User permissions:", permissions);Generates single-use tokens for secure one-time operations.
import { betterAuth } from "better-auth";
import { oneTimeToken } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
oneTimeToken({
expiresIn: 60 * 15, // 15 minutes
}),
],
});
// Client Usage
import { oneTimeTokenClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [oneTimeTokenClient()],
});
// Generate One-Time Token
const { data } = await authClient.oneTimeToken.generate({
action: "download-report",
metadata: { reportId: "123" },
});
console.log("Token:", data.token);
// Verify and Consume Token (server-side)
const result = await auth.api.oneTimeToken.verify({
token: data.token,
});
if (result.valid) {
console.log("Token metadata:", result.metadata);
// Perform action and token is automatically consumed
}Adds support for custom OAuth 2.0 providers not built into Better Auth.
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
genericOAuth({
config: [
{
providerId: "custom-provider",
clientId: process.env.CUSTOM_CLIENT_ID!,
clientSecret: process.env.CUSTOM_CLIENT_SECRET!,
authorizationUrl: "https://oauth.example.com/authorize",
tokenUrl: "https://oauth.example.com/token",
userInfoUrl: "https://oauth.example.com/userinfo",
scopes: ["openid", "profile", "email"],
},
],
}),
],
});
// Client Usage - Same as other OAuth providers
await authClient.signIn.social({
provider: "custom-provider",
callbackURL: "/dashboard",
});Creates reusable plugins with endpoints, schemas, hooks, and middleware.
import { createAuthEndpoint, sessionMiddleware } from "better-auth";
import type { BetterAuthPlugin } from "better-auth";
import { z } from "zod";
export const customSubscriptionPlugin = (options?: {
plans?: string[];
}) => {
return {
id: "subscription",
// Add database schema
schema: {
subscription: {
fields: {
userId: {
type: "string",
required: true,
references: { model: "user", field: "id" },
},
plan: { type: "string", required: true },
status: { type: "string", required: true },
currentPeriodEnd: { type: "date", required: true },
cancelAtPeriodEnd: { type: "boolean", required: true },
},
},
},
// Add custom endpoints
endpoints: {
getSubscription: createAuthEndpoint(
"/subscription/get",
{
method: "GET",
use: [sessionMiddleware],
},
async (ctx) => {
const subscription = await ctx.context.adapter.findOne({
model: "subscription",
where: [{ field: "userId", value: ctx.context.session.user.id }],
});
return { subscription };
}
),
updateSubscription: createAuthEndpoint(
"/subscription/update",
{
method: "POST",
body: z.object({
plan: z.enum(["free", "pro", "enterprise"]),
}),
use: [sessionMiddleware],
},
async (ctx) => {
const updated = await ctx.context.adapter.update({
model: "subscription",
where: [{ field: "userId", value: ctx.context.session.user.id }],
update: { plan: ctx.body.plan },
});
return { subscription: updated };
}
),
},
// Add hooks
hooks: {
after: [
{
matcher: (context) => context.path === "/sign-up/email",
handler: async (ctx) => {
// Create free subscription on sign up
await ctx.context.adapter.create({
model: "subscription",
data: {
userId: ctx.context.session.user.id,
plan: "free",
status: "active",
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
cancelAtPeriodEnd: false,
},
});
},
},
],
},
// Add rate limiting
rateLimit: [
{
pathMatcher: (path) => path.startsWith("/subscription/"),
window: 60,
max: 10,
},
],
// Type inference for client
$Infer: {
Subscription: {} as {
plan: string;
status: string;
currentPeriodEnd: Date;
},
},
} satisfies BetterAuthPlugin;
};
// Usage
export const auth = betterAuth({
plugins: [
customSubscriptionPlugin({
plans: ["free", "pro", "enterprise"],
}),
],
});Enables enterprise Single Sign-On with SAML 2.0 and OIDC support for corporate authentication.
// Install the SSO package
// npm install @better-auth/sso
import { betterAuth } from "better-auth";
import { sso } from "@better-auth/sso";
export const auth = betterAuth({
plugins: [
sso({
// SAML Configuration
saml: {
providers: [
{
providerId: "okta",
entityId: "https://your-app.com",
assertionConsumerServiceURL: "https://your-app.com/api/auth/saml/acs",
singleSignOnServiceURL: process.env.OKTA_SSO_URL!,
x509Certificate: process.env.OKTA_CERT!,
},
],
},
// OIDC Configuration
oidc: {
providers: [
{
providerId: "azure-ad",
clientId: process.env.AZURE_CLIENT_ID!,
clientSecret: process.env.AZURE_CLIENT_SECRET!,
issuer: "https://login.microsoftonline.com/tenant-id/v2.0",
},
],
},
}),
],
});
// Client Usage
import { ssoClient } from "@better-auth/sso/client";
const authClient = createAuthClient({
plugins: [ssoClient()],
});
// Initiate SAML SSO
await authClient.sso.signIn({
providerId: "okta",
callbackURL: "/dashboard",
});
// Initiate OIDC SSO
await authClient.sso.signIn({
providerId: "azure-ad",
callbackURL: "/dashboard",
});Enables enterprise user provisioning through SCIM 2.0 (System for Cross-domain Identity Management) for automated user lifecycle management with identity providers like Okta, Azure AD, and OneLogin.
// Install the SCIM package
// npm install @better-auth/scim
import { betterAuth } from "better-auth";
import { scim } from "@better-auth/scim";
export const auth = betterAuth({
plugins: [
scim({
// Optional: customize token storage (default: "plain")
storeSCIMToken: "hashed", // "plain" | "hashed"
// Optional: hook before token generation
async beforeSCIMTokenGenerated({ user, member, scimToken }) {
console.log("Generating SCIM token for:", user.email);
},
// Optional: hook after token generation
async afterSCIMTokenGenerated({ user, member, scimToken, scimProvider }) {
// Send notification or log the event
await notifyAdmin(`SCIM token created for ${scimProvider.providerId}`);
},
}),
],
});
// The plugin automatically exposes SCIM 2.0 endpoints:
// - POST /api/auth/scim/generate-token (Generate SCIM token)
// - GET /api/auth/scim/v2/ServiceProviderConfig (SCIM metadata)
// - GET /api/auth/scim/v2/Schemas (Supported schemas)
// - GET /api/auth/scim/v2/ResourceTypes (Supported resource types)
// - POST /api/auth/scim/v2/Users (Create user)
// - GET /api/auth/scim/v2/Users (List users)
// - GET /api/auth/scim/v2/Users/:userId (Get user)
// - PUT /api/auth/scim/v2/Users/:userId (Update user)
// - PATCH /api/auth/scim/v2/Users/:userId (Patch user)
// - DELETE /api/auth/scim/v2/Users/:userId (Delete user)
// Client Usage
import { scimClient } from "@better-auth/scim/client";
const authClient = createAuthClient({
plugins: [scimClient()],
});
// Generate SCIM Token (requires authenticated user)
const { data } = await authClient.scim.generateToken({
providerId: "okta",
organizationId: "org_123", // Optional: restrict to organization
});
console.log("SCIM Token:", data.scimToken);
// Share this token with your identity provider
// Configure in your identity provider (e.g., Okta):
// - SCIM Base URL: https://your-app.com/api/auth/scim/v2
// - Authentication: Bearer Token
// - Bearer Token: [scimToken from above]
// Identity provider can now:
// 1. Create users automatically when assigned to the app
// 2. Update user information when changed in the IdP
// 3. Deactivate/delete users when unassigned from the app
// 4. Sync user attributes (name, email, etc.)
// Example: Okta creates a user via SCIM
// POST /api/auth/scim/v2/Users
// Authorization: Bearer [scimToken]
// {
// "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
// "userName": "user@example.com",
// "name": {
// "givenName": "John",
// "familyName": "Doe"
// },
// "emails": [
// { "primary": true, "value": "user@example.com" }
// ],
// "active": true
// }
// Server-side: Query SCIM-provisioned users
const scimUsers = await auth.api.listUsers({
headers: request.headers,
query: {
filter: 'emails[type eq "work" and value co "@example.com"]'
},
});
// SCIM supports filtering with standard operators:
// - eq (equal): userName eq "john@example.com"
// - co (contains): name.givenName co "John"
// - sw (starts with): userName sw "john"
// - pr (present): emails pr
// - and/or: emails pr and userName eq "john@example.com"Provides native mobile authentication for Expo and React Native applications.
// Install the Expo package
// npm install @better-auth/expo expo-web-browser expo-secure-store expo-constants expo-linking expo-crypto
// Server setup (same as web)
import { betterAuth } from "better-auth";
export const auth = betterAuth({
// ... your config
});
// Expo API Route: app/api/auth/[...route]+api.ts
import { auth } from "~/lib/auth";
export async function GET(request: Request) {
return auth.handler(request);
}
export async function POST(request: Request) {
return auth.handler(request);
}
// Client Setup
import { createAuthClient } from "@better-auth/expo/client";
export const authClient = createAuthClient({
baseURL: "https://your-api.com",
plugins: [
// Add your plugins
],
});
// Usage in React Native
import { useSession } from "@better-auth/expo/client";
function App() {
const { data: session, isPending } = useSession();
if (isPending) {
return <ActivityIndicator />;
}
return (
<View>
{session ? (
<Text>Welcome, {session.user.name}!</Text>
) : (
<Button
title="Sign In"
onPress={async () => {
await authClient.signIn.email({
email: "user@example.com",
password: "password",
});
}}
/>
)}
</View>
);
}
// OAuth with Expo
await authClient.signIn.social({
provider: "google",
callbackURL: "myapp://auth/callback",
});Integrates Stripe for subscription and payment management with automatic user-to-customer linking.
// Install the Stripe package
// npm install @better-auth/stripe stripe
import { betterAuth } from "better-auth";
import { stripe } from "@better-auth/stripe";
import Stripe from "stripe";
const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!);
export const auth = betterAuth({
plugins: [
stripe({
stripe: stripeClient,
// Optional: customize customer creation
async createCustomer(user) {
return {
email: user.email,
name: user.name,
metadata: {
userId: user.id,
},
};
},
// Optional: handle webhook events
async onWebhook(event) {
if (event.type === "customer.subscription.created") {
const subscription = event.data.object;
console.log("New subscription:", subscription.id);
}
},
}),
],
});
// Client Usage
import { stripeClient } from "@better-auth/stripe/client";
const authClient = createAuthClient({
plugins: [stripeClient()],
});
// Get or Create Stripe Customer
const { data: customer } = await authClient.stripe.getCustomer();
console.log("Stripe customer ID:", customer.id);
// Create Checkout Session
const { data: session } = await authClient.stripe.createCheckoutSession({
priceId: "price_1234567890",
successUrl: "https://example.com/success",
cancelUrl: "https://example.com/cancel",
});
// Redirect to Stripe checkout
window.location.href = session.url;
// Create Customer Portal Session
const { data: portal } = await authClient.stripe.createPortalSession({
returnUrl: "https://example.com/account",
});
// Redirect to customer portal
window.location.href = portal.url;
// Get Customer Subscriptions
const { data: subscriptions } = await authClient.stripe.getSubscriptions();
subscriptions?.forEach(sub => {
console.log(`Subscription: ${sub.id}, Status: ${sub.status}`);
});
// Server-side: Handle Stripe webhooks
// app/api/stripe/webhook/route.ts
import { auth } from "@/lib/auth";
export async function POST(req: Request) {
const signature = req.headers.get("stripe-signature");
const body = await req.text();
// Better Auth automatically handles webhook verification
// and calls the onWebhook handler configured in the plugin
return auth.handler(req);
}Allows extending session data with custom fields and logic for application-specific needs.
import { betterAuth } from "better-auth";
import { customSession } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
customSession({
// Extend session schema
schema: {
session: {
fields: {
ipAddress: {
type: "string",
required: false,
},
userAgent: {
type: "string",
required: false,
},
metadata: {
type: "json",
required: false,
},
},
},
},
// Customize session creation
async onSessionCreate(session, context) {
return {
...session,
ipAddress: context.request.headers.get("x-forwarded-for"),
userAgent: context.request.headers.get("user-agent"),
metadata: { loginMethod: "email" },
};
},
// Customize session retrieval
async onSessionGet(session, context) {
// Add runtime data to session
return {
...session,
activeDevices: await getActiveDevices(session.userId),
};
},
}),
],
});
// Type-safe access to custom session data
const session = await auth.api.getSession({ headers });
console.log("IP Address:", session?.ipAddress);
console.log("Metadata:", session?.metadata);Extends user and session objects with custom fields for application-specific data.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
user: {
additionalFields: {
role: {
type: "string",
required: false,
defaultValue: "user",
},
bio: {
type: "string",
required: false,
},
dateOfBirth: {
type: "date",
required: false,
},
isVerified: {
type: "boolean",
required: false,
defaultValue: false,
},
},
},
session: {
additionalFields: {
deviceName: {
type: "string",
required: false,
},
},
},
});
// Client Usage
import { inferAdditionalFields } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [
inferAdditionalFields({
user: {
role: {
type: "string",
required: false,
},
bio: {
type: "string",
required: false,
},
dateOfBirth: {
type: "date",
required: false,
},
isVerified: {
type: "boolean",
required: false,
},
},
session: {
deviceName: {
type: "string",
required: false,
},
},
}),
],
});
// Access additional fields in session
const { data: session } = useSession();
console.log("User role:", session?.user.role);
console.log("User bio:", session?.user.bio);
console.log("Device name:", session?.session.deviceName);
// Update user with additional fields
await authClient.updateUser({
role: "admin",
bio: "Software developer",
isVerified: true,
});Adds bot protection to authentication endpoints using Google reCAPTCHA or Cloudflare Turnstile.
import { betterAuth } from "better-auth";
import { captcha } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
captcha({
// Use Google reCAPTCHA v2 or v3
provider: "recaptcha", // "recaptcha" | "turnstile"
secretKey: process.env.RECAPTCHA_SECRET_KEY!,
// Optional: override site verify URL
siteVerifyURLOverride: "https://custom-verify-url.com",
// Optional: specify which endpoints to protect (defaults to common auth endpoints)
endpoints: [
"/sign-in/email",
"/sign-up/email",
"/reset-password",
],
}),
// Or use Cloudflare Turnstile
captcha({
provider: "turnstile",
secretKey: process.env.TURNSTILE_SECRET_KEY!,
}),
],
});
// Client Usage - Add captcha response header
// For Google reCAPTCHA
const recaptchaToken = await grecaptcha.execute();
await authClient.signIn.email({
email: "user@example.com",
password: "password",
fetchOptions: {
headers: {
"x-captcha-response": recaptchaToken,
},
},
});
// For Cloudflare Turnstile
const turnstileToken = await turnstile.getResponse();
await authClient.signUp.email({
email: "user@example.com",
password: "password",
name: "User",
fetchOptions: {
headers: {
"x-captcha-response": turnstileToken,
},
},
});
// The plugin automatically verifies the captcha token before processing the request
// If verification fails, the request is rejected with a 400 errorEnables passwordless authentication using Google One Tap for seamless sign-in.
import { betterAuth } from "better-auth";
import { oneTap } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
oneTap({
// Optional: disable signup via One Tap
disableSignup: false,
// Optional: specify Google Client ID (uses socialProviders.google.clientId if not provided)
clientId: process.env.GOOGLE_CLIENT_ID,
}),
],
});
// Client Usage
import { oneTapClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [oneTapClient()],
});
// Initialize Google One Tap on your page
google.accounts.id.initialize({
client_id: "YOUR_GOOGLE_CLIENT_ID",
callback: async (response) => {
// Send the ID token to Better Auth
const { data, error } = await authClient.oneTap.signIn({
idToken: response.credential,
});
if (!error) {
console.log("Signed in with Google One Tap:", data.user);
}
},
});
// Display the One Tap prompt
google.accounts.id.prompt();
// Or render the One Tap button
google.accounts.id.renderButton(
document.getElementById("oneTapButton"),
{
theme: "outline",
size: "large",
}
);Tracks the last authentication method used by each user for analytics and UX improvements.
import { betterAuth } from "better-auth";
import { lastLoginMethod } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
lastLoginMethod({
// Optional: custom cookie name
cookieName: "better-auth.last_used_login_method",
// Optional: cookie expiration (default: 30 days)
maxAge: 60 * 60 * 24 * 30,
// Optional: store in database instead of cookie only
storeInDatabase: true,
// Optional: custom method to resolve login method
customResolveMethod(ctx) {
// Extract login method from context
if (ctx.path.includes("/sign-in/social")) {
return "oauth";
}
if (ctx.path.includes("/passkey/")) {
return "passkey";
}
return "email";
},
}),
],
});
// When storeInDatabase is enabled, the plugin adds a lastLoginMethod field to the user table
// Access last login method on the client
const { data: session } = useSession();
if (session) {
console.log("Last login method:", session.user.lastLoginMethod);
}
// Server-side access
const session = await auth.api.getSession({ headers });
if (session) {
console.log("User's last login method:", session.user.lastLoginMethod);
}
// The plugin automatically tracks:
// - "email" for email/password authentication
// - "oauth" for social authentication
// - "passkey" for passkey authentication
// - "magic-link" for magic link authentication
// - "phone" for phone number authentication
// - Custom values from customResolveMethodAutomatically generates OpenAPI 3.0 documentation for all Better Auth endpoints with interactive API explorer.
import { betterAuth } from "better-auth";
import { openAPI } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
openAPI({
// Optional: customize the path (default: "/reference")
path: "/api-docs",
// Optional: customize Scalar theme
theme: "purple", // "default" | "alternate" | "moon" | "purple" | "solarized" | etc.
// Optional: add custom security nonce for CSP
nonce: process.env.CSP_NONCE,
}),
],
});
// The plugin automatically exposes an interactive API reference at:
// http://localhost:3000/api/auth/api-docs
// Access OpenAPI specification JSON:
// http://localhost:3000/api/auth/api-docs/openapi.json
// The documentation includes:
// - All authentication endpoints with request/response schemas
// - Security schemes and authentication requirements
// - Interactive try-it-out functionality
// - Example requests and responses
// - Type definitions and validation rules
// - Rate limiting information
// You can also use the OpenAPI spec with other tools:
// - Import into Postman, Insomnia, or other API clients
// - Generate client SDKs in multiple languages
// - Set up API testing and monitoring
// - Create custom documentation sitesControls request frequency to prevent abuse and protect endpoints from attacks.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
// Global rate limiting
rateLimit: {
enabled: true,
window: 60, // seconds
max: 10, // requests per window
storage: "memory", // "memory" | "database"
// Custom rate limit rules
customRules: [
{
pathMatcher: (path) => path === "/sign-in/email",
window: 60,
max: 5, // More restrictive for sign-in
},
{
pathMatcher: (path) => path === "/sign-up/email",
window: 60,
max: 3, // Even more restrictive for sign-up
},
{
pathMatcher: (path) => path.startsWith("/reset-password"),
window: 300, // 5 minutes
max: 3,
},
],
},
});
// Plugin-specific rate limiting
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
twoFactor({
// Plugin adds its own rate limits
rateLimit: {
window: 60,
max: 3, // Only 3 2FA attempts per minute
},
}),
],
});Better Auth provides a production-ready authentication system that scales from simple email/password flows to complex multi-tenant applications with enterprise SSO/SAML, SCIM user provisioning, Web3 authentication, device authorization, OpenID Connect provider capabilities for building OAuth 2.0 authorization servers, OAuth proxy for seamless development workflows, Model Context Protocol (MCP) OAuth integration for AI agent authentication, Stripe payment integration, captcha verification for bot protection, Google One Tap authentication, automatic OpenAPI documentation generation, last login method tracking, and native mobile support via Expo/React Native. The plugin architecture enables rapid feature development while maintaining type safety across the entire stack, supporting advanced features like phone number authentication, username login, anonymous sessions, bearer tokens, API keys, JWT tokens, password breach checking via HaveIBeenPwned, custom session handling, additional user/session fields, OIDC provider, OAuth proxy, MCP OAuth, Stripe subscriptions, SCIM enterprise provisioning, captcha protection, Google One Tap, OpenAPI docs, last login tracking, and fine-grained access control. Database adapters abstract away ORM-specific details, allowing seamless switching between Prisma, Drizzle, MongoDB, or in-memory storage. Framework integrations handle the nuances of Next.js App Router, SvelteKit, Solid Start, React Start (TanStack Start), Expo, and other modern frameworks, ensuring cookies, headers, and redirects work correctly in server components, API routes, edge runtimes, and native mobile apps.
The client libraries provide a unified API across React, Vue, Svelte, Solid, Lynx, and React Native with framework-specific primitives like hooks, stores, and signals for reactive session management. Plugins like organization, two-factor, passkey, magic link, email OTP, phone number, username, anonymous, bearer, API key, JWT, multi-session, SIWE, device authorization, access control, custom session, OIDC provider, OAuth proxy, MCP OAuth, Stripe, SCIM, SSO/SAML, captcha, Google One Tap, OpenAPI documentation, and last login method tracking add complete features with minimal configuration, including database schemas, API endpoints, hooks, rate limiting, and client methods. Additional features can be added through the user/session additionalFields configuration for custom data requirements. Separate packages like @better-auth/passkey, @better-auth/sso, @better-auth/scim, @better-auth/expo, @better-auth/stripe, and @better-auth/oauth-provider provide specialized functionality with their own dependencies. The system's security defaults include automatic CSRF protection, secure cookie handling with httpOnly and sameSite flags, bcrypt password hashing, rate limiting, captcha verification support, password breach checking via HaveIBeenPwned integration, and session validation on every request, making it suitable for production use without extensive security hardening.