Better-Auth: A Practical Setup Guide
Email+password auth, session management, bearer plugin for cross-origin clients, trusted origins, middleware, and admin role gating.
Better-Auth is a TypeScript-first auth library that does more of what you actually need out of the box compared to NextAuth v4 — typed sessions, plugin architecture, bearer tokens, and a clean schema that integrates with Drizzle. Here's a complete setup.
Installation and Schema
npm install better-auth
npx better-auth generate # generates DB schemaBetter-Auth needs four tables: users, sessions, accounts, verifications. If you're using Drizzle, it can generate the migration for you:
// auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { bearer } from "better-auth/plugins";
import { db } from "@/lib/db";
import * as schema from "@/db/schema";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg", schema }),
emailAndPassword: { enabled: true },
plugins: [bearer()],
trustedOrigins: [
"https://nodwatch.online",
"http://localhost:3000",
"http://localhost:8081", // Expo dev
"exp://localhost:8081", // Expo Go
],
session: {
cookieCache: { enabled: true, maxAge: 60 * 5 }, // 5-min cookie cache
},
});
export type Session = typeof auth.$Infer.Session;API Route
Better-Auth needs a catch-all API route:
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);Client Setup
// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL!,
});
export const { signIn, signOut, signUp, useSession } = authClient;Email + Password Sign Up / In
"use client";
import { authClient } from "@/lib/auth-client";
export function RegisterForm() {
async function handleSubmit(formData: FormData) {
const result = await authClient.signUp.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
name: formData.get("name") as string,
});
if (result.error) console.error(result.error.message);
}
return <form action={handleSubmit}>{/* fields */}</form>;
}Session in Server Components
// lib/session.ts
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export async function getSession() {
return auth.api.getSession({ headers: await headers() });
}// app/dashboard/page.tsx
import { getSession } from "@/lib/session";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await getSession();
if (!session) redirect("/login");
return <div>Welcome {session.user.name}</div>;
}Middleware Protection
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const PROTECTED = ["/dashboard", "/profile", "/watchlist"];
export function middleware(request: NextRequest) {
const isProtected = PROTECTED.some((path) =>
request.nextUrl.pathname.startsWith(path)
);
if (!isProtected) return NextResponse.next();
const session = request.cookies.get("better-auth.session_token");
if (!session) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}For full session validation in middleware, use Better-Auth's getSessionFromRequest helper — cookie checking is faster but doesn't verify the token signature.
Bearer Plugin for Mobile / Cross-Origin
The bearer() plugin enables Authorization: Bearer <token> authentication. This is how the Expo mobile client authenticates with the same backend:
// Mobile client
const session = await authClient.signIn.email({ email, password });
const token = session.data?.session.token;
// Subsequent requests
fetch(`${API_URL}/api/watchlist`, {
headers: { Authorization: `Bearer ${token}` },
});Admin Role Gating
Better-Auth stores role on the user. Gate admin routes:
// lib/admin.ts
import { getSession } from "@/lib/session";
const ADMIN_EMAILS = process.env.ADMIN_EMAILS?.split(",") ?? [];
export async function requireAdmin() {
const session = await getSession();
if (!session) throw new Error("Unauthenticated");
const isAdmin =
session.user.role === "admin" ||
ADMIN_EMAILS.includes(session.user.email);
if (!isAdmin) throw new Error("Forbidden");
return session;
}To promote a user to admin:
UPDATE users SET role = 'admin' WHERE email = 'you@example.com';Better-Auth's plugin model makes it easy to extend. The bearer plugin alone makes it worth the switch from NextAuth when you have a mobile client or need API token access.