Skip to content

better-auth

Use when implementing authentication with Better Auth — session management, route protection, user roles, email/password auth, social OAuth, or database adapters. Also use when choosing a TypeScript-first auth library with full control over session strategy.

ModelSource
sonnetpack: auth
Full Reference

┏━ 🔧 better-auth ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ TypeScript-first auth — sessions, OAuth, roles ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

TypeScript-first authentication framework — session management, route protection, roles, OAuth, and database adapters.

ItemValue
Packagebetter-auth v1.x
Companion@better-fetch/fetch
Auth catch-all route/api/auth/[...all]/route.ts
Session endpointGET /api/auth/get-session
Session viaSigned cookies (httpOnly)
Default session expiry86400s (1 day)
Cookie cache max-age300s (5 min) — skips DB lookups
Roles"user" and "admin" (lowercase)
DocWhat’s inside
reference/setup.mdServer config, client config, Drizzle adapter, catch-all route (Next.js, Hono, Express)
reference/sessions.mdSession lifecycle, cookie cache, middleware, useSession, server-side checks
reference/providers.mdEmail/password, social OAuth (Google, GitHub, etc.), magic links
reference/roles.mdRole system, admin plugin, RBAC patterns, RBAC scoping

Setup server config (lib/auth.ts):

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { admin } from "better-auth/plugins";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg", schema }),
emailAndPassword: { enabled: true },
plugins: [admin()],
});

Protect an API route:

import { auth } from "@/lib/auth";
import { headers } from "next/headers";
const session = await auth.api.getSession({ headers: await headers() });
if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 });
// session.user.id, session.user.role, session.user.email

Client: reactive session:

"use client";
import { useSession } from "@/lib/auth-client";
const { data: session, isPending } = useSession();

Client: sign in / sign out:

import { signIn, signOut } from "@/lib/auth-client";
await signIn.email({ email, password, callbackURL: "/dashboard" });
await signOut({ fetchOptions: { onSuccess: () => router.push("/login") } });

Check role:

const isAdmin = session.user.role === "admin"; // always lowercase
MistakeFix
Role as "ADMIN" / "SUPER_ADMIN"Roles are lowercase: "user" and "admin"
Forgetting await headers()Must await headers() before passing to getSession()
Using auth.api.getSession() in middlewareMiddleware (Edge) uses betterFetch("/api/auth/get-session", ...)
Skipping callbackURL on signIn.email()Without it, post-login redirect may not work
session.user.isAdmin fieldNo such field — check session.user.role === "admin"
GET /api/auth/get-session rate limitedExclude it: customRules: { "/get-session": false }