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 OAuth —
GOOGLE_ID/GOOGLE_SECRET. Profile mapssub→id, keeps name/email/image. - Email magic links — enabled when
EMAIL_SERVERis set. Works on all three DBs via adapters (SQLite, Supabase, Mongo). - Dev login (dev only) —
CredentialsProvider id=dev-login, gated byNODE_ENV=development.[email protected](free),[email protected](getshasAccess,billingProviderfrom env). Never ships to prod.
Adapters (one per DB)
libs/next-auth.js:getAdapter() picks by DB_PROVIDER:
sqlite→SqliteAdapter()(providers/db/sqlite/adapter.js, sameapp.db).supabase→SupabaseAdapter()(providers/db/supabase/adapter.js, service_role only).mongodb→MongoDBAdapter(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:
/authredirects 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: stampstoken.subfromuser.id, backfills by email when missing.session: exposessession.user.id = token.subfor all providers.