Hmbown/CodeWhale · error · Error
SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required
Error message
SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required
What it means
postgrest performs the REST calls to Supabase and requires both SUPABASE_URL and a service-role key (SUPABASE_SERVICE_ROLE_KEY, falling back to SUPABASE_SECRET_KEY) in the environment. Without them it throws before any network call, since anonymous publication to the facts tables is impossible.
Solutions
- Export both variables before running: SUPABASE_URL=https://<project>.supabase.co and SUPABASE_SERVICE_ROLE_KEY=<service-role-key> (or SUPABASE_SECRET_KEY)
- Source the secrets file (e.g. `set -a; . ./.env.secrets; set +a`) or pass them inline for the single command
- Verify with `echo "${SUPABASE_URL:?}" ${SUPABASE_SERVICE_ROLE_KEY:+set}` that both are present before invoking the script
Example fix
// before node web/scripts/facts-publish.mjs ... // after SUPABASE_URL=https://xyz.supabase.co SUPABASE_SERVICE_ROLE_KEY=eyJ... node web/scripts/facts-publish.mjs ...
Defensive patterns
Strategy: validation
Validate before calling
if (!process.env.SUPABASE_URL) throw new Error("set SUPABASE_URL (https://<project>.supabase.co) before publishing");
if (!process.env.SUPABASE_SERVICE_ROLE_KEY && !process.env.SUPABASE_SECRET_KEY) throw new Error("set SUPABASE_SERVICE_ROLE_KEY (or SUPABASE_SECRET_KEY) before publishing"); Type guard
function hasSupabaseEnv(env = process.env) {
return Boolean(env.SUPABASE_URL) && Boolean(env.SUPABASE_SERVICE_ROLE_KEY || env.SUPABASE_SECRET_KEY);
} Try / catch
try {
await publishFacts(envelope);
} catch (err) {
if (err.message === "SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required") {
console.error("Export SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY (source your secrets file) and retry");
process.exit(1);
}
throw err;
} Prevention
- Keep a secrets file sourced by the publish wrapper (set -a; . ./.env.secrets; set +a)
- Pre-flight check both env vars in the script entry point before doing expensive signing work
- Document the exact variable names (note the SUPABASE_SECRET_KEY fallback) where operators will find them
When it happens
Trigger: Calling any publishing flow that reaches postgrest when SUPABASE_URL is unset or empty, or when neither SUPABASE_SERVICE_ROLE_KEY nor SUPABASE_SECRET_KEY is set — e.g. env file not loaded, wrong shell profile, or the variable exported under a different name.
Common situations: Running the publish script outside the founder's shell that sources the secrets; .env not exported (dotenv not invoked); key renamed after a Supabase plan/SDK change (newer SUPABASE_SECRET_KEY naming is supported but the legacy var is what most setups set).
Understand the failure class
Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.
Related errors
- could not resolve home directory for FileKeyringStore
- no executable search path is configured
- supabase-not-configured
- 127
- A pinned task provider requires an explicit model
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/28688d33aff0e25f.
Report an issue: GitHub.
Appendix: source
Thrown at web/scripts/facts-publish.mjs:451
`insert into public.facts_key (key_id, scope, algorithm, public_key, status)`,
` values (${sqlLiteral(envelope.key_id)}, 'global', 'ed25519', ${sqlLiteral(publicKeyB64)}, 'active')`,
` on conflict (key_id) do nothing;`,
`insert into public.facts_release (channel_id, facts_version, schema_version, envelope_version, applies_to, key_id, payload_b64, sig_b64, sigs, payload, published_at, not_after, published_by, notes)`,
` select c.id, ${envelope.facts_version}, ${envelope.schema_version}, ${envelope.envelope}, ${sqlLiteral(envelope.applies_to)}, ${sqlLiteral(envelope.key_id)},`,
` ${sqlLiteral(envelope.payload_b64)}, ${sqlLiteral(envelope.sig_b64)}, ${sqlLiteral(JSON.stringify(envelope.sigs ?? []))}::jsonb,`,
` ${sqlLiteral(payloadJson)}::jsonb, ${sqlLiteral(envelope.published_at)}::timestamptz, ${sqlLiteral(check.payload.not_after ?? null)}::timestamptz,`,
` ${sqlLiteral(publishedBy)}, ${sqlLiteral(notes)}`,
` from public.facts_channel c where c.scope = 'global' and c.slug = ${sqlLiteral(envelope.channel)};`,
"commit;",
"",
].join("\n");
}
async function postgrest(path, { method = "GET", body, prefer } = {}) {
refuseUnderCi();
const url = process.env.SUPABASE_URL;
const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY;
if (!url || !key) throw new Error("SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required");
const endpoint = new URL(url);
if (endpoint.protocol !== "https:" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) throw new Error("invalid Supabase endpoint");
const res = await fetch(`${url.replace(/\/$/, "")}/rest/v1/${path}`, {
method,
signal: AbortSignal.timeout(30_000),
redirect: "error",
headers: {
apikey: key,
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
...(prefer ? { Prefer: prefer } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
if (!res.ok) { await res.body?.cancel(); throw new Error(`PostgREST request failed (HTTP ${res.status})`); }
const text = await readBoundedResponse(res);
return text ? JSON.parse(text) : null;
}View on GitHub (pinned to 433685b202)