Authentication
Login, signup, sessions, OAuth OAuth A secure way to log in using another account (like Google or GitHub) without sharing your password with the app. "Like a hotel key card. The front desk (Google) vouches for you, so the room (app) lets you in."
1Who Are You, and What Can You Do?
Two different questions, two different words. Authentication Authentication Verifying WHO you are, usually by logging in with email and password, a magic link, or OAuth with Google/GitHub. Libraries like Auth.js, Better Auth, and Clerk handle the hard parts. "Like showing your ID at the door. Proving you are who you claim to be." Authorization Verifying WHAT you can do. After you're authenticated, authorization checks if you have permission for a specific action. "Like having a building pass but only for certain floors. You're in, but not everywhere."
User accounts
Save preferences, history, data.
Protected routes
Dashboards, admin, APIs.
Billing
Tie subscriptions to a real person.
2Your Options in 2026
The big news
In September 2025 the Auth.js project became part of Better Auth. Auth.js v5 still works and is still maintained, but the maintainers now recommend Better Auth for new projects. If your AI assistant reaches for NextAuth by reflex, that's training data talking.
Better AuthNEW PROJECTS
Open-source, TypeScript-first, runs in your app against your database
Best for
- • Owning your users table
- • Email/password + social + plugins
- • No per-user pricing
- • The recommended starting point from the team that now maintains Auth.js
Watch out for
- • You build the sign-in UI
- • You run the database and migrations
- • More moving parts than a hosted service
Install
npm install better-authAuth.js v5 (NextAuth)STILL BETA
The long-time Next.js default, now part of Better Auth
Best for
- • Existing NextAuth apps
- • Quick OAuth-only login
- • Huge amount of tutorials and examples
Watch out for
- • v5 is still published under the beta tag
- • npm install next-auth gives you v4
- • For brand-new apps, its maintainers point you to Better Auth
Install
npm install next-auth@betaClerkFASTEST
Hosted auth with polished drop-in UI components
Best for
- • Shipping login in minutes
- • Pre-built sign-in, profile, org UI
- • Teams/organizations and MFA without building them
Watch out for
- • User data lives on Clerk's servers
- • Per-user pricing beyond the free tier
- • Vendor lock-in if you migrate later
Install
npx -y clerk@latest initSupabase AuthWITH SUPABASE
Auth bundled with a Supabase Postgres database
Best for
- • You already use Supabase
- • Row Level Security tied to the logged-in user
- • One dashboard for DB + auth + storage
Watch out for
- • Tied to the Supabase ecosystem
- • RLS has a learning curve
- • Free projects pause after a week of inactivity
Install
npm install @supabase/supabase-js @supabase/ssrAuth0, Firebase Auth, and friends
Enterprise-grade hosted options. Reach for them when you need enterprise SSO (SAML, Okta), complex multi-tenant identity, or you're already deep in Google's mobile stack. Free tiers exist; check each vendor's current pricing page before committing, because per-user costs add up at scale.
Free-tier numbers as of Sep 2026. Check Clerk pricing and Supabase pricing for today's limits. (MRU = monthly retained users; MAU = monthly active users.)
3Quick Decision Guide
| If you want... | Use |
|---|---|
| Login working today, beautiful UI, don't care where users live | Clerk |
| Own your users table in your own Postgres (e.g. Neon), no per-user fees | Better Auth |
| You already run NextAuth / Auth.js and it works | Stay on Auth.js v5 |
| Your database is Supabase | Supabase Auth |
| Enterprise SSO (SAML, Okta) is a hard requirement | Clerk or Auth0 |
Saucy take: hosted (Clerk) trades money and control for speed. Self-hosted libraries (Better Auth, Auth.js) trade setup time for ownership. Both are fine. What's not fine is building your own password system.
4Setup: Auth.js v5 + GitHub
The shortest path to "Sign in with GitHub" in a Next.js 16 app. Also the setup behind a lot of existing codebases, so worth knowing.
Install the v5 beta and generate a secret
Terminal
npm install next-auth@beta
npx auth secret # writes AUTH_SECRET to .env.localPlain npm install next-auth installs v4, which uses different imports and env var names. Check your package.json says 5.0.0-beta.x.
Create auth.ts at the project root
auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [GitHub],
});No clientId needed: v5 reads AUTH_GITHUB_ID and AUTH_GITHUB_SECRET automatically. That magic is also the #1 way it breaks (see the traps below).
Add the route handler
app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;Set environment variables
.env.local
AUTH_SECRET=generated-by-npx-auth-secret
AUTH_GITHUB_ID=your_github_oauth_client_id
AUTH_GITHUB_SECRET=your_github_oauth_client_secretPattern: AUTH_{PROVIDER}_ID / AUTH_{PROVIDER}_SECRET. On Vercel you usually don't need AUTH_URL or AUTH_TRUST_HOST; v5 works out the host from the request. Add the same values in Vercel (see Environment Variables).
Register the callback URL with GitHub
Callback URL
http://localhost:3000/api/auth/callback/github
https://yourdomain.com/api/auth/callback/githubGetting this wrong is the OAuth Trap. Read that guide before you deploy.
Add a sign-in button (Server Action, no client JS)
components/SignIn.tsx
import { signIn } from "@/auth";
export function SignIn() {
return (
<form
action={async () => {
"use server";
await signIn("github");
}}
>
<button type="submit">Sign in with GitHub</button>
</form>
);
}Optional: a proxy.ts for fast redirects
proxy.ts
export { auth as proxy } from "@/auth";This is proxy.ts, not middleware.ts, in Next.js 16. It's a convenience, not your security. Real checks go in server components, route handlers, and Server Actions.
5Setup: Better Auth (Own Your Users)
Better Auth runs inside your app and stores users, sessions, and accounts in your database. Here's the shape of it with Postgres + Drizzle.
Install
Terminal
npm install better-auth @better-auth/drizzle-adapterEnvironment variables
.env.local
BETTER_AUTH_SECRET=a-random-string-of-32-plus-characters
BETTER_AUTH_URL=http://localhost:3000
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...Configure lib/auth.ts
lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "@better-auth/drizzle-adapter";
import { db } from "@/lib/db";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg" }),
emailAndPassword: { enabled: true },
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
},
},
});Mount the handler and generate tables
app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);Terminal
npx auth@latest generate # creates the auth tables for your schemaGitHub callback URL: /api/auth/callback/github. Include the user:email scope or users with private emails will fail to sign in. Full client-side usage lives in the Better Auth docs.
6Setup: Clerk (Fastest Path)
Let the CLI do it
Terminal
npx -y clerk@latest initIt detects Next.js, installs @clerk/nextjs, and writes NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY to .env.local.
What it sets up: the provider
app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ClerkProvider>{children}</ClerkProvider>
</body>
</html>
);
}What it sets up: proxy.ts
proxy.ts (Next.js 16)
import { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware();
export const config = {
matcher: [
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
"/(api|trpc)(.*)",
"/__clerk/(.*)",
],
};Yes, the function is still called clerkMiddleware; only the file name changed. Clerk's own docs say to protect data "as close to the resource as possible": call await auth() from @clerk/nextjs/server where you read or write data.
Drop in the UI
components/Navbar.tsx
import { SignInButton, UserButton } from "@clerk/nextjs";
// Show SignInButton to guests, UserButton to signed-in users
<SignInButton />
<UserButton />Verify the install anytime with npx -y clerk@latest doctor.
7Sign-In Methods
Email + password
Universal, but you now own password resets, breach checks, and verification emails.
Best for: any app, especially B2B
Magic link / email code
Passwordless. Needs a reliable email sender.
Best for: low-friction consumer signup
Social OAuth
"Sign in with Google/GitHub." One click for users, five config steps for you.
Best for: consumer apps, dev tools. See OAuth Providers
Passkeys / WebAuthn
Face ID, fingerprint, or a hardware key. Phishing-resistant.
Best for: security-sensitive apps, as a second option
8Common Traps
The OAuth Trap
Hit redirect_uri_mismatch or client_id=undefined? Deep dive into the 5-part OAuth setup.
Installing v4 by accident
npm install next-auth gives you v4. Then v5 docs, v5 imports, and AUTH_ env names don't line up with what you installed.
Fix: Use next-auth@beta for v5, or pick Better Auth for a new project.
v4-style env var names on a v5 app
Old guides use GITHUB_ID, NEXTAUTH_SECRET, NEXTAUTH_URL. v5 auto-reads AUTH_GITHUB_ID and AUTH_SECRET. Mismatch means login silently sends client_id=undefined.
Fix: Rename to the AUTH_{PROVIDER}_ID convention everywhere: .env.local, Vercel Production, and Vercel Preview.
"proxy.ts protects my app"
Proxy (old middleware) is an optimistic, early check. A matcher tweak or a refactor can quietly skip routes, and Server Actions are just POSTs to the page they live on.
Fix: Re-check the session wherever data is read or changed. That's the core lesson of Protected Routes.
"I'll build auth myself"
Password hashing, session rotation, CSRF, rate limiting, account recovery: each one is a place to get owned.
Fix: Use a maintained library or service. Spend the saved week on your actual product.
Tokens in localStorage
Any script on the page (including an XSS payload) can read localStorage.
Fix: Keep sessions in httpOnly cookies. Every option above does this by default; don't undo it.
Trusting IDs from the URL
/invoices/123 lets anyone try /invoices/124.
Fix: Always check that the signed-in user owns the record in the query itself (WHERE user_id = session user).
Users can log in?
Lock down what they can reach, then start charging for it.
Stuck mid-setup? Book a session.