Architecture Philosophy

Most SaaS templates force you into third-party providers, which hold your user data in proprietary clouds and charge per-user fees as you grow. Better-Auth is local-first. Your users, sessions, and OAuth accounts are simply tables in your Neon PostgreSQL database, queried natively via Drizzle ORM.

Security

Full Ownership

Your data stays in your database. No per-user fees, no migration headaches, and total control over your schema.

Integration

Drizzle Sync

The auth schema is natively synced with Drizzle for strict end-to-end type safety across your entire application.

Performance

Next.js Native

Optimized for Server Components, Middleware, and Server Actions to ensure a seamless experience with zero layout shift.

Core Configuration

The authentication logic is split into specific areas to handle the API, the Server, and the Client environments separately.

src/lib/auth.tsMaster Server Config

This is where the engine is defined for the server. It uses the Drizzle adapter and enables primary plugins like admin and emailPassword.

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { admin } from "better-auth/plugins";
import { db } from "@/db";
import * as schema from "@/db/schema";

export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema,
}),
emailPassword: { enabled: 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!,
},
},
plugins: [admin()],
});
src/lib/auth-client.tsClient Instance

This instance is utilized in React components. It abstracts the fetch calls and provides reactive hooks for the browser.

import { createAuthClient } from "better-auth/react";

export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
});
src/app/api/auth/[...all]/route.tsGlobal API Handler

The catch-all route handles every authentication request (login, signup, callbacks, session management) automatically.

import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

export const { GET, POST } = toNextJsHandler(auth);

Google Provider Setup

Follow these precise steps to enable Google OAuth.

1

Create a Cloud Project

Head to the Google Cloud Console and create a new project dedicated to your SaaS.

2

Configure OAuth Credentials

Create a new OAuth 2.0 Client IDand select "Web Application".

Authorized JavaScript Origins

http://localhost:3000

Authorized Redirect URIs

http://localhost:3000/api/auth/callback/google

3

Extract Secret Keys

Copy the Client ID and Client Secret from the Google dashboard and paste them into your .env file under GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.

GitHub Provider Setup

GitHub login is highly recommended for developer-focused SaaS products.

1

Register OAuth App

Visit your GitHub Settings > Developer Settings > OAuth Appsand click "New OAuth App".

2

Fill Application Details

Ensure the callback URL points exactly to the Better-Auth API path.

Homepage URL

http://localhost:3000

Authorization Callback URL

http://localhost:3000/api/auth/callback/github

3

Link the Secrets

Generate a new Client Secret on the GitHub page. Map these to GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET in your environment variables.

Pro Tip: To add more providers (Twitter, Apple, Discord, etc.), visit the official Better Auth social provider documentation.

Accessing Session State

The method used depends entirely on where the code is executing. Next.js requires specific patterns for Server Components.

Server Components (RSC)

Standard Pattern

Fetching the session on the server ensures zero layout shift and proper metadata generation. You must pass the headers from next/headers to authorize the request.

import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

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

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

return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p>Logged in as {session.user.email}</p>
</div>
);
}

Client Components (Hooks)

Use the authClient for reactive UI elements that change state without a full page reload, such as a user dropdown or a profile settings form.

"use client";

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

export function UserProfile() {
const { data: session, isPending, error } = authClient.useSession();

if (isPending) return <div>Syncing session...</div>;
if (error) return <div>Error loading user data</div>;

return (
<div className="flex items-center gap-2">
<img src={session?.user.image} alt="" />
<span>{session?.user.name}</span>
</div>
);
}

Role-Based Access Control

Simple yet effective security gates for standard users vs. administrators.

Security Gate 1

Protecting the Admin Panel

The application features a strict server-side check within the admin route group. If a user attempts to access /dashboard/admin without the "admin" role string present in the Neon database, the server will block the request and redirect them to the standard dashboard.

if (session.user.role !== "admin") {
redirect("/dashboard");
}
Security Gate 2

Subscription Feature Gating

To restrict specific features to paid users, you check the subscription state synchronized from Polar.sh. It is highly recommended to perform these checks in Server Actions to ensure the logic cannot be bypassed by browser manipulation.

Password Reset Lifecycle

Secure credential recovery powered by Resend.

The full lifecycle for password recovery is pre-configured. When a user enters their email on the "Forgot Password" form, Better-Auth generates a cryptographically secure token.

This token is then passed to a custom email template via the mail utility in src/lib/mail.ts and delivered via the Resend API. Once the user clicks the secure link in their inbox, they are redirected back to your application with the valid token to set their new password.

Extending the Schema

Adding custom user data is a streamlined two-step process.

Step 1

Database Schema

Add the new column directly to the user table within src/db/schema.ts. Use Drizzle to push the changes:

pnpm drizzle-kit push
Step 2

Auth Registration

Register the new field in the user.additionalFields object inside your server configuration in src/lib/auth.ts. This allows the engine to map the database column to the session object.

Frequently Asked Questions

Can I use Magic Links or Passwordless login?

Yes! Better-Auth supports Magic Links natively. You can enable the magic link plugin in yoursrc/lib/auth.ts file and use the mail utility to send the tokens.

Is it possible to force email verification?

Absolutely. You can enable the email verification plugin in Better-Auth, which will automatically block access to certain routes or flag the session if the user's email is not verified.

Official Documentation

For advanced plugin configurations, multi-factor auth setup, or internal engine details, browse the official source documentation.