Authentication & database (NextAuth + Supabase)
How ContentAI uses Auth.js (NextAuth v5) for sign-in and Supabase Postgres for per-user data and encrypted API keys.
Production ContentAI pairs Auth.js (NextAuth v5) with Supabase used as PostgreSQL — not Supabase Auth. Identity comes from OAuth (GitHub and Google); the app stores user_id from the session and reads or writes rows only from Next.js API routes using the Supabase service role (or direct SQL via DATABASE_URL).
Architecture at a glance
Browser ──OAuth──▶ NextAuth (GitHub / Google)
│
▼
Session cookie (user.id = OAuth `sub`)
│
App pages & fetch() ──▶ /api/* routes
│
auth() in route ──▶ verify session
│
▼
getSupabaseAdmin() or getPostgres() ──▶ Postgres (Supabase)- NextAuth issues the session; middleware redirects unauthenticated users away from dashboard routes.
- Supabase holds
generations,documents, anduser_settings. The browser never receives the service role key; only server code uses it afterauth()succeeds.
NextAuth (Auth.js v5)
| Topic | Detail |
|---|---|
| Config | Root auth.ts — exports handlers, auth, signIn, signOut |
| Route | app/api/auth/[...nextauth]/route.ts re-exports handlers as GET / POST |
| Providers | GitHub and Google OAuth |
| Custom sign-in UI | pages.signIn → /auth/signin |
| Session | callbacks.session copies token.sub to session.user.id (typed in types/next-auth.d.ts) |
| Secret | Prefer AUTH_SECRET; NEXTAUTH_SECRET and others are supported; dev fallback only in development |
Client components use SessionProvider from next-auth/react (see components/providers/auth-session-provider.tsx) and useSession() where needed (sidebar, user data provider, sign-in page).
Route protection
middleware.ts wraps the default export with auth(). Paths under /dashboard, /templates, /generate, /editor, /history, and /settings require a session; unauthenticated visitors are redirected to /auth/signin with callbackUrl preserved.
Supabase and Postgres
Supabase JS client (server-only)
lib/supabase/server.ts exposes getSupabaseAdmin():
- Uses
SUPABASE_URLandSUPABASE_SERVICE_ROLE_KEY(not the anon key). - Validates the key JWT so you do not accidentally configure the anon key (
role: anon) and get confusing RLS errors. - Disables Supabase client session persistence (this client is for server-side access only).
Optional direct Postgres (DATABASE_URL)
lib/db.ts provides getPostgres() when DATABASE_URL is set (from Supabase → Database → Connection string → URI, preferably the Transaction pooler on port 6543 for serverless).
When DATABASE_URL is present, getUserAiSettings and /api/user/settings use parameterized SQL against public.user_settings. Otherwise they use Supabase REST via the service role client. Same data either way; direct SQL avoids edge cases with PostgREST and service_role.
Environment variables
| Variable | Purpose |
|---|---|
AUTH_SECRET | Sign cookies / JWTs for NextAuth (e.g. openssl rand -base64 32) |
AUTH_GITHUB_ID / AUTH_GITHUB_SECRET | GitHub OAuth app |
AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET | Google OAuth client |
SUPABASE_URL | Project URL |
SUPABASE_SERVICE_ROLE_KEY | Server-only; full access to Postgres via Supabase API (keep secret) |
DATABASE_URL | Optional; direct Postgres / pooler URI for postgres package |
API_KEYS_ENCRYPTION_KEY | Optional; 64 hex chars (openssl rand -hex 32) for AES-256-GCM encryption of API keys. If omitted, AUTH_SECRET / NEXTAUTH_SECRET is used to derive a key (see lib/crypto/api-keys.ts) |
Public NEXT_PUBLIC_* variants for OAuth client IDs exist for convenience in some setups; never expose AUTH_*_SECRET or SUPABASE_SERVICE_ROLE_KEY to the client.
Database schema and migrations
SQL lives under supabase/migrations/ in the app repo (run in Supabase SQL Editor or via CLI). Core tables:
generations— template id/name, category, content, provider, model, word count, inputsjsonb,user_idtext, timestamps.documents— title, content,user_idtext, timestamps.user_settings— one row per user:api_keys_ciphertext,active_provider,selected_modelsjsonb.
user_id is the NextAuth subject (session.user.id), not auth.users from Supabase.
Later migrations lock down access: RLS disabled where appropriate, grants limited so only service_role (and backend roles) can touch these tables — the browser never queries Supabase directly for app data.
Encrypted API keys
Provider keys are not stored in localStorage in this mode. Settings are saved with PUT /api/user/settings; the server encrypts the JSON blob with AES-256-GCM and persists ciphertext only. POST /api/generate loads keys via getUserAiSettings(session.user.id) — the request body does not include API keys.
Client data loading
UserDataProvider fetches /api/generations and /api/documents when the user is authenticated, so History, Editor, and Dashboard use server-backed lists instead of Zustand-only persistence.
Local development checklist
- Create OAuth apps (GitHub and/or Google) with callback URL
http://localhost:3000/api/auth/callback/github(or/google). - Set
AUTH_SECRETand provider credentials in.env.local. - Apply migrations to a Supabase project; set
SUPABASE_URLandSUPABASE_SERVICE_ROLE_KEY. - Optionally set
DATABASE_URLfor direct Postgres access. - Run
pnpm devand sign in at/auth/signin.
Related: Add Supabase (alternate recipes) · Self-hosting · Troubleshooting