Boring Docs

Auth — Google, Magic Links, Dev Login, Sessions

Adapters per database, JWT sessions, stale-session guard, admin allowlist.

Auth

Stack: NextAuth v4, JWT session strategy (libs/next-auth.js:140-142), custom pages under /auth.

Providers

  • Google OAuthGOOGLE_ID / GOOGLE_SECRET. Profile maps subid, keeps name/email/image.
  • Email magic links — enabled when EMAIL_SERVER is set. Works on all three DBs via adapters (SQLite, Supabase, Mongo).
  • Dev login (dev only)CredentialsProvider id=dev-login, gated by NODE_ENV=development. [email protected] (free), [email protected] (gets hasAccess, billingProvider from env). Never ships to prod.

Adapters (one per DB)

libs/next-auth.js:getAdapter() picks by DB_PROVIDER:

  • sqliteSqliteAdapter() (providers/db/sqlite/adapter.js, same app.db).
  • supabaseSupabaseAdapter() (providers/db/supabase/adapter.js, service_role only).
  • mongodbMongoDBAdapter(connectMongo).

JWT means sessions tables are mostly idle except magic-link verification tokens — expected, not a bug.

Session shape

// libs/paywall.js
{ session, user, hasAccess, planType, isLifetime, isMonthly }
  • No session / stale session (JWT valid but user row missing) → user=null, hasAccess=false. Pages redirect to /auth; APIs return 401.
  • Free user → 403 on paid routes. Never redirect-loop: /auth redirects away only when the user row exists (app/(pages)/auth/page.js).

Admin

libs/admin.js: ADMIN_EMAILS allowlist. Prod returns [] when unset (fail-closed, ADMIN_NOT_CONFIGURED). Use isAdmin() / checkAdminAccess() — never compare emails inline.

Callbacks

  • jwt: stamps token.sub from user.id, backfills by email when missing.
  • session: exposes session.user.id = token.sub for all providers.