llmapikey
Pending 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: pending. Live key minting is gated behind
PROJECT_STATUS=pendingandPROVISIONING_ENABLED=falseuntil a suitable provider is found. Official OpenRouter docs confirm BYOK has a 5% OpenRouter fee after the first 1M BYOK requests per month (requests, not tokens), so this giveaway is paused before public launch.
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. - Project lifecycle gate:
PROJECT_STATUSdefaults fail-closed topending; the UI hides self-serve key minting/retrieval while pending, and key creation requiresPROJECT_STATUS=liveplusPROVISIONING_ENABLED=true. - Reserve-then-mint: a
pendingrow is inserted (ON CONFLICT DO NOTHING) before minting, so concurrent double-submits yield exactly one OpenRouter key. - Key storage: the raw key is stored in
openrouter_keyso users can copy it again from the dashboard;openrouter_delete_hashis OpenRouter's revoke handle (not a hash of the key). Keys are minted into theOPENROUTER_WORKSPACE_IDworkspace. - 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) OPENROUTER_WORKSPACE_IDWorkspace minted keys are created in (create-key workspace_id); omit for the management key's defaultPROJECT_STATUSpendinguntil a suitable provider is found; onlylivecan enable the productPROVISIONING_ENABLEDLower-level minting flag; must be truein addition toPROJECT_STATUS=liveMAX_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 all migrations in order to a staging branch first,
then prod (Supabase SQL editor,
psql "$POSTGRES_URL" -f ..., ornode --env-file=.env.local scripts/run-migration.mjs <file>):Do NOT addpsql "$POSTGRES_URL" -f supabase/migrations/0001_llmapikey_schema_and_api_keys.up.sql psql "$POSTGRES_URL" -f supabase/migrations/0002_api_keys_store_raw_key.up.sql psql "$POSTGRES_URL" -f supabase/migrations/0003_rename_key_hash_to_delete_hash.up.sqlllmapikeyto the project's PostgREST "Exposed schemas". Rollback: run the matching*.down.sqlfiles in reverse order (0003 → 0001). - 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
OpenRouter BYOK finding
As of 2026-06-19, the rendered official OpenRouter docs say BYOK usage has a fee after the monthly waiver: the first 1M BYOK requests per month are free, then OpenRouter charges 5% of what the same model/provider would normally cost on OpenRouter, deducted from OpenRouter credits. This is documented as requests, not tokens.
Independent research also confirmed the policy shape (request-based monthly
waiver, then percentage fee from OpenRouter credits), but saw placeholders in
OpenRouter's markdown docs for the exact threshold/percentage. Re-verify the
rendered docs or ask OpenRouter support before setting PROJECT_STATUS=live.
Implication: the public free-key giveaway stays pending until a provider can
support the intended economics without surprise pass-through charges. See
docs/project-status.md.
Sources:
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 PROJECT_STATUS and
PROVISIONING_ENABLED. Keep PROJECT_STATUS=pending and
PROVISIONING_ENABLED=false in the Vercel environment until provider economics
are accepted; only then set PROJECT_STATUS=live and PROVISIONING_ENABLED=true
to begin minting.