Skip to content
Skip to main content
Novel Systems home
Decision log
D-029August 4, 2026

An environment variable is a name, not a promise; the key inside it is checked for shape

Decided

lib/supabase-admin.ts gained looksLikeSecretKey(), and getSupabaseAdmin() now throws a *distinct* error when SUPABASE_SERVICE_ROLE_KEY holds a browser-level key — separate from the error it throws when the variable is absent.

The incident. Production's SUPABASE_SERVICE_ROLE_KEY contained a value beginning sb_publishable_. Supabase issues two current key formats, sb_publishable_… for browsers and sb_secret_… for servers, and both are perfectly well-formed API keys. createClient accepted it. The connection opened. Authentication succeeded. The failure arrived hundreds of milliseconds later as an opaque PostgREST rejection on an insert, with no mention of keys anywhere in it — because the four site intake tables carry revoke all … from anon plus restrictive deny policies, so a publishable key is not a *degraded* credential there, it is a credential with no privileges at all.

This is worse than a missing key. A missing key fails immediately, at the point of configuration, with a message naming the variable. A wrong-kind key fails later, at the row, in a message about paths and policies. The variable name is the only place the word "service role" appears, and a name cannot enforce anything.

Why the check is deliberately permissive. looksLikeSecretKey returns false for exactly two things: a value starting sb_publishable_, and a legacy eyJ… JWT whose decoded role claim is not service_role. Everything else — including formats Supabase has not shipped yet — returns true. It is a guard against one observed mistake, not an oracle for key validity. Refusing to boot on an unrecognised-but-working format would be a worse failure than the one it prevents, and it is the kind of check that ages badly precisely because it is written against a vendor's current naming.

Why two error messages rather than one. "You set the wrong kind of key" and "you set no key" have different fixes, and the first reads as the second unless it is said out loud. The wrong-kind branch names the dashboard path to copy the right value from, and repeats that it must never carry a NEXT_PUBLIC_ prefix — which is the mistake that produces a publishable key in a server slot in the first place.

Corollary already in the file, restated because it is easy to undo. The key is read at *call time*, not at module load, and the admin client is never memoized. Both exist so a rotation takes effect without a redeploy. A module-level constant would have frozen the publishable key in place even after it was corrected in the dashboard.