🟢The Backend
Lesson 77 of 117
30 min
Better Auth
The newer, you-own-the-schema TypeScript-first auth library.
Better Auth arrived in late 2024 and won the argument faster than anyone expected. In September 2025 the Auth.js maintainers handed their project to the Better Auth team, which makes this the library the previous default now points at. It is TypeScript-first, you own the DB schema, and the plugin model is unusually clean.
Why people are switching
- You own the schema. Better Auth generates the SQL tables you need; they live in your migrations like any other table. Inspect, modify, query.
- Framework-agnostic. Works with Next, Remix, Hono, SvelteKit, Astro, Express, Solid Start. The core is just functions.
- Plugins are first-class. Passkeys, organizations, 2FA, magic links, OAuth proxy: each is a plugin you opt in to.
- End-to-end types. The client knows what the server configured. No casting, no "user as any".
Install
bash
pnpm add better-auth
pnpm add -D @better-auth/cliThe auth instance
lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg" }),
emailAndPassword: { enabled: true },
socialProviders: {
github: {
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
},
},
});Generate the schema
bash
npx @better-auth/cli generate
# Adds user, session, account, verification tables to your Drizzle schema
# Then run your usual migration step
pnpm drizzle-kit pushThe schema is yours
After generation, those tables are normal tables. Add columns (
plan, company, etc.) directly. Just rerun generate after Better Auth upgrades to merge in any new required columns.The route handler
app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);Client SDK (typed)
lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
});
export const { signIn, signOut, signUp, useSession } = authClient;tsx
"use client";
import { useSession, signIn, signOut } from "@/lib/auth-client";
export function Nav() {
const { data: session, isPending } = useSession();
if (isPending) return null;
return session ? (
<button onClick={() => signOut()}>Sign out, {session.user.name}</button>
) : (
<button onClick={() => signIn.social({ provider: "github" })}>
Sign in with GitHub
</button>
);
}Reading the session on the server
tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export default async function Dashboard() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) redirect("/login");
return <h1>Hello, {session.user.name}</h1>;
}Plugins that ship
- passkey: WebAuthn / FIDO2, built on SimpleWebAuthn. This one lives in its own package,
@better-auth/passkey, rather than in core. - organization: multi-tenant orgs, invites, roles.
- twoFactor: TOTP, with backup codes.
- magicLink: email-based passwordless.
- admin: built-in admin user model with banning.
- jwt: emit JWT alongside session cookies, for mobile or third-party consumers. Import it from
better-auth/plugins; some older posts show a@better-auth/jwtpackage that does not exist on npm.
oidcProvider was removed in 1.7
If you are standing up your own OAuth/OIDC server, the
oidcProvider plugin is gone as of Better Auth 1.7 and its docs page 404s. The replacement is @better-auth/oauth-provider, which also added DPoP and back-channel logout.Better Auth vs Auth.js: how to pick
- Better Auth wins for: organizations/multi-tenant, passkeys out of the box, owning the schema, fewer surprises in non-Next frameworks.
- Auth.js wins for: an enormous provider ecosystem, and the sheer number of production deployments that have already shaken its bugs out.
- New TypeScript project, Drizzle or Prisma, Next 16: start with Better Auth unless you need a provider only Auth.js has.
The two projects are now one team
This is not a neutral rivalry any more. The Better Auth team took over maintenance of Auth.js in September 2025 and said plainly that new projects should start with Better Auth, with the goal of the ecosystem converging rather than staying split. Today authjs.dev itself says the same thing. The one gap they admitted at handover was stateless sessions with no database; that shipped in Better Auth 1.4 in November 2025, so it is no longer a reason to pick Auth.js.
Sessions without a database
If you want a session with no persistence layer at all, omit the database option. Better Auth then keeps the session in a signed or encrypted cookie, configured through session.cookieCache, with HMAC-SHA256 by default and JWT or JWE strategies available.
Stateless means you cannot revoke
This is the same trade as any self-contained token. With nothing server-side to delete, a "log out everywhere" button has nothing to act on. The documented escape hatch is bumping
cookieCache.version and redeploying, which invalidates everyone. If you need real revocation, add Redis as secondaryStorage and take the hybrid approach.Quiz
Quiz1 / 3
What is the biggest practical difference between Better Auth and Auth.js?
Recap
- Better Auth = TS-first, you-own-the-schema, framework-agnostic.
- Plugins for passkeys, orgs, 2FA, magic links, admin, JWT.
- Typed client SDK with
useSession,signIn,signOut. - Pick Better Auth if you want multi-tenancy or passkeys, Auth.js if you need an obscure provider or are deep in legacy Auth.js setup.