Self-contained GitHub OAuth (Arctic) with a stateless HS256 signed-cookie session (jose); Supabase is downgraded to the Postgres host only. - Origin-derived callback (no redirect-uri env); read:user scope; access token read once at callback and discarded (no token storage). - CSRF via single-use state cookie; open-redirect guard on next. - getCurrentGithubIdentity() now reads the session cookie, preserving the numeric provider_id identity contract for admin/dashboard/mint. - Remove @supabase/ssr + @supabase/supabase-js, middleware, and the supabase-dependent rls test; delete lib/supabase clients.
llmapikey
Free, capped OpenRouter API key giveaway — one key per GitHub account.
Next.js (App Router, JS + JSDoc) on Vercel. App-native GitHub OAuth (Arctic +
jose stateless signed-cookie session) — Supabase is the Postgres host only.
Per-user OpenRouter keys are minted from the owner's master Provisioning key,
each capped at a daily USD limit. Key records live in a dedicated, unexposed
llmapikey Postgres schema reached only by a server-side direct connection.
Status: code build only. Live key minting is gated behind
PROVISIONING_ENABLED=falseuntil the OpenRouter ToS approval gate (plan Phase 1) clears. Do not deploy a public giveaway before that.
Stack
- Next.js 15 App Router, plain JS + JSDoc (no TypeScript)
- App-native GitHub OAuth — Arctic for the OAuth2
dance, jose for a stateless HS256 signed-cookie
session. No auth provider; GitHub →
/auth/callbackdirectly.read:userscope only; the access token is read once and discarded (no token storage). - Postgres (
postgresnpm) — direct connection to the unexposedllmapikeyschema; the anon role can never reachapi_keys. (Supabase = DB host only.) - OpenRouter Provisioning API — mints per-user keys
Architecture notes
- Identity anchor: the numeric, immutable GitHub
provider_id(not the mutable login). One key pergithub_user_id, enforced by a DB unique constraint. - Server-only secrets:
POSTGRES_URL,OPENROUTER_MANAGEMENT_KEY,GITHUB_OAUTH_CLIENT_SECRET, andAUTH_SESSION_SECRETare guarded by theserver-onlypackage and never reach the client bundle (noNEXT_PUBLIC_). - Stateless session: signed JWT (HS256) in an httpOnly+Secure+SameSite=Lax
cookie; no DB session table. CSRF handled by a single-use
statecookie verified at the callback;nextredirects pass through a same-origin sanitizer. - Schema isolation is structural: the
llmapikeyschema is unexposed to PostgREST and reached only via the direct PG client (deny-all RLS as defense in depth). The app ships no anon DB client, so the isolation holds by construction. - Reserve-then-mint: a
pendingrow is inserted (ON CONFLICT DO NOTHING) before minting, so concurrent double-submits yield exactly one OpenRouter key. - Schema isolation:
llmapikeyis NOT added to PostgREST exposed schemas; RLS is deny-all as defense in depth. - Admin console: route
/admin(unlisted — no nav link) lists, searches, filters, revokes, and manually mints keys. Access is gated by theADMIN_GITHUB_USER_IDSallowlist against the same numericprovider_idanchor; non-admins getnotFound()(a 404, never a redirect that would leak the route's existence). Every admin server action re-checks the allowlist server-side, so the page gate is defense-in-depth only.
Setup
- Install
npm install - Environment — copy
.env.exampleto.env.localand fill in values.Var Purpose GITHUB_OAUTH_CLIENT_IDGitHub OAuth App client id (server-only) GITHUB_OAUTH_CLIENT_SECRETGitHub OAuth App client secret (server-only) AUTH_SESSION_SECRETSession JWT signing secret, ≥32 bytes ( openssl rand -base64 48)POSTGRES_URLSupabase transaction pooler string (server-only; provisioned by the Supabase Vercel integration) OPENROUTER_MANAGEMENT_KEYMaster management/provisioning key (server-only) PROVISIONING_ENABLEDfalseuntil Phase 1 ToS gate clearsMAX_TOTAL_KEYSKill-switch: stop minting past N active keys KEY_DAILY_LIMIT_USDPer-key daily cap sent to OpenRouter KEY_EXPIRY_DAYSKey lifetime (sets expires_at)ADMIN_GITHUB_USER_IDSAdmin allowlist — numeric GitHub provider_ids, CSV (server-only) - Database — apply the migration to a staging branch first, then prod
(Supabase SQL editor or
psql "$POSTGRES_URL" -f ...):Do NOT addpsql "$POSTGRES_URL" -f supabase/migrations/0001_llmapikey_schema_and_api_keys.up.sqlllmapikeyto the project's PostgREST "Exposed schemas". Rollback:...0001_...down.sql. - GitHub OAuth App — register one at GitHub > Settings > Developer settings >
OAuth Apps. Authorization callback URL points at the app (not Supabase):
https://llmapikey.vercel.app/auth/callbackfor prod (use a separate dev app withhttp://localhost:3000/auth/callbackfor local). Copy the Client ID + Secret intoGITHUB_OAUTH_CLIENT_ID/GITHUB_OAUTH_CLIENT_SECRET. The callback is derived from the request origin at runtime, so no callback env is needed — but reach the app on its canonical domain (the one registered above); GitHub rejects sign-in arriving on any other host (e.g. Vercel deployment-hash or preview URLs). - Run
npm run dev # http://localhost:3000 npm test # unit tests
Deploy (gated)
Git-triggered builds on Vercel are enabled (vercel.json →
git.deploymentEnabled: true). Builds and deploys do NOT start the giveaway:
live key minting is gated independently by PROVISIONING_ENABLED. Keep
PROVISIONING_ENABLED=false in the Vercel environment until the Phase 1
OpenRouter ToS gate clears; only then set it true to begin minting.