Hmbown/CodeWhale · error · Error
supabase-not-configured
supabase-not-configured
Error message
supabase-not-configured
What it means
fetchCurrentRow validates the Supabase REST configuration before issuing any request. If SUPABASE_URL is missing/unparseable, is not an https URL with no userinfo/query/hash, or SUPABASE_PUBLISHABLE_KEY is missing or fails isPublishableKey, the catch block rethrows as "supabase-not-configured". It signals the caller that cloud facts cannot be fetched because the environment is not set up for authenticated Supabase access.
Solutions
- Set SUPABASE_URL to a clean https://<project>.supabase.co URL with no query, hash, or userinfo.
- Set SUPABASE_PUBLISHABLE_KEY to the project's anon/publishable key (starts with 'sb_publishable_' or legacy 'eyJ...') via wrangler secret or environment config.
- Check for typos in the env var names so values actually land in CloudFactsEnv.
- If Supabase is intentionally not used in this environment, catch this error and fall back to local/curated facts instead of treating it as fatal.
Example fix
// before (bad URL with query) SUPABASE_URL=https://xyz.supabase.co?apikey=abc // after SUPABASE_URL=https://xyz.supabase.co SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
Defensive patterns
Strategy: validation
Validate before calling
function supabaseConfigured(env) {
try {
const u = new URL(env.SUPABASE_URL ?? '');
return u.protocol === 'https:' && !u.username && !u.password && !u.search && !u.hash && !!env.SUPABASE_PUBLISHABLE_KEY;
} catch { return false; }
} Type guard
const isConfigured = (env) => typeof env.SUPABASE_URL === 'string' && env.SUPABASE_URL.startsWith('https://') && typeof env.SUPABASE_PUBLISHABLE_KEY === 'string' && env.SUPABASE_PUBLISHABLE_KEY.length > 0; Prevention
- Add SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY to a deploy-time checklist or CI smoke test.
- Never paste query strings or credentials into SUPABASE_URL.
- Use the anon publishable key, not service_role, in client-facing env.
- Validate env vars at worker startup and fail loudly before serving traffic.
When it happens
Trigger: Calling fetchCurrentRow (directly or via row()) when env.SUPABASE_URL is undefined/empty or malformed, or not https, or embeds username/password/query/hash; or env.SUPABASE_PUBLISHABLE_KEY is absent or not a valid publishable (anon) key.
Common situations: Deploying the worker without setting SUPABASE_URL/SUPABASE_PUBLISHABLE_KEY secrets; a staging environment with only local KV; pasting a service_role key where a publishable/anon key is required; typos in env var names; a URL with a query string or trailing credentials from copy-paste.
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
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/6cac15c07920cee1.
Report an issue: GitHub.
Appendix: source
Thrown at web/lib/cloud-facts.ts:257
function isPublishableKey(key: string): boolean {
if (/^sb_publishable_[A-Za-z0-9_-]+$/.test(key)) return true;
if (key.length > 8192) return false;
try {
const parts = key.split(".");
if (parts.length !== 3) return false;
const middle = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const payload = JSON.parse(atob(middle.padEnd(Math.ceil(middle.length / 4) * 4, "=")));
return isObject(payload) && payload.role === "anon";
} catch { return false; }
}
export async function fetchCurrentRow(channel: string, env: CloudFactsEnv, opts: ResolveOptions = {}): Promise<FactsCurrentRow | null> {
if (!isValidChannel(channel)) throw new Error("invalid-channel");
const key = env.SUPABASE_PUBLISHABLE_KEY;
let base: URL;
try {
base = new URL(env.SUPABASE_URL ?? "");
if (base.protocol !== "https:" || base.username || base.password || base.search || base.hash || !key || !isPublishableKey(key)) throw new Error();
} catch { throw new Error("supabase-not-configured"); }
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? SUPABASE_TIMEOUT_MS);
try {
const url = new URL(`${base.href.replace(/\/+$/, "")}/rest/v1/facts_current`);
url.search = new URLSearchParams({ channel: `eq.${channel}`, scope: "eq.global", select: "channel,release_id,facts_version,schema_version,envelope_version,applies_to,key_id,payload_b64,sig_b64,sigs,payload_sha256,published_at,not_after", limit: "1" }).toString();
const res = await (opts.fetchImpl ?? fetch)(url, {
headers: { apikey: key!, Authorization: `Bearer ${key}`, Accept: "application/json" },
signal: controller.signal,
redirect: "error",
});
if (!res.ok) { await res.body?.cancel(); throw new Error(`supabase-http-${res.status}`); }
const bytes = await readBoundedBody(res, MAX_ENVELOPE_BYTES);
const rows: unknown = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
if (!Array.isArray(rows) || rows.length > 1) throw new Error("supabase-bad-row");
if (rows.length === 0) return null;
if (!isObject(rows[0]) || !isEnvelope(envelopeFromRow(rows[0] as unknown as FactsCurrentRow))) throw new Error("supabase-bad-row");
return rows[0] as unknown as FactsCurrentRow;View on GitHub (pinned to 433685b202)