ContentAIDOCS
Deployment

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, and user_settings. The browser never receives the service role key; only server code uses it after auth() succeeds.

NextAuth (Auth.js v5)

TopicDetail
ConfigRoot auth.ts — exports handlers, auth, signIn, signOut
Routeapp/api/auth/[...nextauth]/route.ts re-exports handlers as GET / POST
ProvidersGitHub and Google OAuth
Custom sign-in UIpages.signIn/auth/signin
Sessioncallbacks.session copies token.sub to session.user.id (typed in types/next-auth.d.ts)
SecretPrefer 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_URL and SUPABASE_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

VariablePurpose
AUTH_SECRETSign cookies / JWTs for NextAuth (e.g. openssl rand -base64 32)
AUTH_GITHUB_ID / AUTH_GITHUB_SECRETGitHub OAuth app
AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRETGoogle OAuth client
SUPABASE_URLProject URL
SUPABASE_SERVICE_ROLE_KEYServer-only; full access to Postgres via Supabase API (keep secret)
DATABASE_URLOptional; direct Postgres / pooler URI for postgres package
API_KEYS_ENCRYPTION_KEYOptional; 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, inputs jsonb, user_id text, timestamps.
  • documents — title, content, user_id text, timestamps.
  • user_settings — one row per user: api_keys_ciphertext, active_provider, selected_models jsonb.

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

  1. Create OAuth apps (GitHub and/or Google) with callback URL http://localhost:3000/api/auth/callback/github (or /google).
  2. Set AUTH_SECRET and provider credentials in .env.local.
  3. Apply migrations to a Supabase project; set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY.
  4. Optionally set DATABASE_URL for direct Postgres access.
  5. Run pnpm dev and sign in at /auth/signin.

Related: Add Supabase (alternate recipes) · Self-hosting · Troubleshooting

On this page