ShipVeryFastShipVeryFast
Documentation

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.

libs/auth.ts (shape)
const providers = [
  GoogleProvider({ clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET }),
  ...(hasSupabaseBackend ? [EmailProvider({ /* Mailgun SMTP */ })] : []),
];

Why a Supabase adapter

When a backend is present, the Supabase adapter persists users and tokens so magic links and account data survive restarts. Without it, the app still runs on JWT-only Google sign-in.

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.