Auth Is Never Just "Log In and Log Out"

The moment your app grows beyond a basic weekend project, authentication morphs into a massive headache. Suddenly, you aren't just checking a password—you're juggling OAuth providers, session invalidation, email verification, 2FA, multi-tenant teams, subscription syncs, and security edge cases you never saw coming.
That’s usually where momentum dies. You either end up duct-taping three disconnected libraries and a fragile custom session layer, or you surrender to a hosted auth platform—sacrificing control over your database, your API, and your monthly cloud bill.
Better Auth offers a middle path: a modern, framework-agnostic TypeScript authentication system that runs inside your app, talks directly to your database, and grows through plugins rather than full rewrites.
What Is Better Auth?
Better Auth is an open-source, TypeScript-first auth framework. Instead of treating auth like an external service, it acts as an integrated layer inside your codebase. You define a single betterAuth() configuration on the server, pair it with a typed client on the frontend, and extend functionality as needed.
Unlike SaaS providers that lock your user records behind proprietary APIs, Better Auth leaves data ownership entirely in your hands. You own the schema, you manage the session strategy, and you pick your preferred ORM.
Suspiciously Simple Implementation
The core philosophy of Better Auth is eliminating boilerplate. You write one central configuration file on your server, export a frontend client, and you're done. No complex middleware webs or multi-file auth abstractions.
1. Server Configuration (e.g., auth.ts)
Setting up a complete auth server—including database persistence via Drizzle and password login—takes less than 15 lines of code:
TypeScript
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "@better-auth/drizzle-adapter";
import { db } from "./db";
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
database: drizzleAdapter(db, { provider: "pg" }),
emailAndPassword: { enabled: true },
});
2. Fully Typed Frontend Client (e.g., auth-client.ts)
On the client side, you create a unified auth instance that inherits types directly from your server setup:
TypeScript
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: "http://localhost:3000",
});
3. Clean React Component Integration
Calling auth actions in your components feels natural, reactive, and completely type-safe:
TypeScript
import { authClient } from "@/lib/auth-client";
export function Login() {
const { data: session, isPending } = authClient.useSession();
const handleSignIn = async () => {
await authClient.signIn.email({
email: "user@example.com",
password: "secure-password",
});
};
if (isPending) return <div>Loading...</div>;
if (session) return <div>Welcome back, {session.user.name}</div>;
return <button onClick={handleSignIn}>Sign In</button>;
}
The Power of the Plugin Ecosystem
Better Auth’s defining feature is its modular plugin system. Instead of shipping a bloated bundle with features you don't need, or forcing you to rewrite your auth when your app scales, features are simply added to your plugins array.
When you add a server plugin, its corresponding methods automatically flow into your frontend client types.
Example: Adding Social OAuth & Passkeys in Seconds
Need to add Google login and WebAuthn Passkeys? Just plug them in:
TypeScript
import { betterAuth } from "better-auth";
import { passkey } from "better-auth/plugins";
export const auth = betterAuth({
// ...base 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!,
},
},
plugins: [
passkey(), // Adds WebAuthn / Fingerprint / FaceID support
],
});
Now, logging in with a Passkey on the frontend is a single function call:
TypeScript
// Full Passkey / Biometric sign-in on the frontend
await authClient.signIn.passkey();
Example: Enterprise Two-Factor Authentication (2FA)
Upgrading security shouldn't require re-architecting your database. Drop in twoFactor():
TypeScript
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
// ...base config
plugins: [
twoFactor({
issuer: "My App",
}),
],
});
When a user with 2FA attempts to log in, the API automatically returns clear flow indicators so the frontend knows whether to render a TOTP input or offer fallback options:
TypeScript
// Enable TOTP for the current user
const { data, error } = await authClient.twoFactor.enableTOTP({
password: "user-password",
});
// Verify during login
await authClient.twoFactor.verifyTOTP({
code: "123456",
});
Unifying Auth and Billing with Zero Glue Code
A common failure point in modern SaaS development is maintaining state between your database and payment processors like Stripe:
- User registers in your auth database.
- User selects a tier, generating a Stripe customer object.
- Custom sync scripts run to bridge user IDs and Stripe IDs.
- Webhooks fail silently, leaving user permissions out of sync.
The Stripe plugin bridges this gap directly at the auth layer with minimal setup:
TypeScript
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({
// ...base config
plugins: [
stripe({
stripeClient,
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
createCustomerOnSignUp: true, // Automatically links auth users to Stripe customers
}),
],
});
Managing subscriptions or checking billing state on the frontend requires zero custom webhook logic:
TypeScript
// Create a checkout session directly from the client
await authClient.stripe.createCheckoutSession({
priceId: "price_12345",
slug: "pro-plan",
});
Note: Plugins also exist for alternative providers like Polar, Creem, and Dodo Payments if you aren't using Stripe.
End-to-End Plugin Coverage
Whatever your application demands, there is a modular plugin ready to plug straight into your existing setup:
- Authentication Strategy: Passkeys (WebAuthn), Magic Links, Phone OTP, Sign-In with Ethereum (SIWE), Anonymous/Guest Sessions.
- Authorization & Multi-Tenancy: Multi-tenant Organizations, Role-Based Access Control (RBAC), Admin Dashboards, Multi-Session Support, SSO / OIDC integration.
- Security & Hardening: Leaked password detection via Have I Been Pwned (HIBP), CAPTCHA protection, Rate limiting.
- Developer Utilities: OpenAPI spec generation, MCP / Device Auth flows, Last-used login method detection.
By keeping complexity localized inside your configuration file, Better Auth lets you ship faster without sacrificing control over your codebase, your data, or your architecture.