Framework-agnostic TypeScript authentication library — email/password, OAuth, sessions, 2FA & more
Security / AuthBetter 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.
# 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// 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,
});// 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);BETTER_AUTH_URL (your app URL) and BETTER_AUTH_SECRET (random 32+ char string). Generate a secret with openssl rand -base64 32.
// 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;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);
}const { data, error } = await authClient.signIn.email({
email: "user@example.com",
password: "securePassword123",
callbackURL: "/dashboard", // redirect after sign in (optional)
});await authClient.signOut({
fetchOptions: {
onSuccess: () => {
router.push("/login"); // redirect to login page
},
},
});// 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",
});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!,
},
},
});// Sign in with Google
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
// Sign in with GitHub
await authClient.signIn.social({
provider: "github",
callbackURL: "/dashboard",
});| Provider | Key | Scopes Available |
|---|---|---|
google | email, profile, openid | |
| GitHub | github | user:email, read:user |
| Discord | discord | identify, email |
| Apple | apple | name, email |
| Microsoft | microsoft | User.Read, openid |
| Twitter/X | twitter | users.read, tweet.read |
| Spotify | spotify | user-read-email |
| Twitch | twitch | user:read:email |
{baseURL}/api/auth/callback/{provider} in each provider's developer console.
// 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 });
}{
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 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.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*"],
};// 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);
});// 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
});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
},
}),
],
});import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [twoFactorClient()],
});// 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",
});| Plugin | Import | Description |
|---|---|---|
| Two Factor | twoFactor | TOTP, backup codes, recovery |
| Magic Link | magicLink | Passwordless email login |
| Organization | organization | Multi-tenant org management |
| Admin | admin | User management, impersonation |
| Username | username | Add username field to users |
| Phone Number | phoneNumber | Phone-based auth with OTP |
| Anonymous | anonymous | Link anonymous users to accounts |
| Bearer | bearer | API key / bearer token auth |
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(),
],
});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",
}),
});import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./db";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // or "mysql", "sqlite"
}),
});| Table | Purpose | Key Columns |
|---|---|---|
user | User accounts | id, name, email, emailVerified, image |
session | Active sessions | id, userId, expiresAt, ipAddress, userAgent |
account | OAuth linked accounts | id, userId, providerId, accountId |
verification | Email verification tokens | id, identifier, value, expiresAt |
// 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>
);
}├── 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// 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>
);
}auth.api.getSession() in server components and API routes. Use authClient.useSession() in client components. Never import the server auth config in client code.