Back to Blog
Dec 1, 20253 min readOnuzulike Anthony

Better-Auth: A Practical Setup Guide

Email+password auth, session management, bearer plugin for cross-origin clients, trusted origins, middleware, and admin role gating.

Full-StackAuthNext.jsSecurity

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

bash
npm install better-auth
npx better-auth generate  # generates DB schema

Better-Auth needs four tables: users, sessions, accounts, verifications. If you're using Drizzle, it can generate the migration for you:

ts
// 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:

ts
// 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

ts
// 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

tsx
"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

ts
// lib/session.ts
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
 
export async function getSession() {
  return auth.api.getSession({ headers: await headers() });
}
tsx
// 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

ts
// 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:

ts
// 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:

ts
// 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:

sql
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.