Auth model
This page explains how authentication is shaped and why. For the step-by-step setup, see Authentication.
One config, JWT sessions
The single source of truth is the authOptions object in libs/auth.ts. It uses the JWT session strategy, so a session is a signed token in a cookie, not a database lookup on every request. The catch-all route handler at app/api/auth/[...nextauth]/route.ts and getServerSession() both read the same config, so the client and server never disagree about who is signed in.
Providers are conditional
Google OAuth is always registered. The passwordless email provider (magic links) is added only when a real Supabase backend is configured, because it needs somewhere to persist verification tokens. This is what lets a fresh clone run before you have set anything up.
const providers = [
GoogleProvider({ clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET }),
...(hasSupabaseBackend ? [EmailProvider({ /* Mailgun SMTP */ })] : []),
];Why a Supabase adapter
Reading and protecting sessions
Read the session server-side with getServerSession(authOptions) and client-side with the useSession() hook. Route protection does not live in each page: middleware.ts redirects unauthenticated users away from protected paths before the page renders, so protection is centralized and hard to forget.
