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

  1. Set SUPABASE_URL to a clean https://<project>.supabase.co URL with no query, hash, or userinfo.
  2. Set SUPABASE_PUBLISHABLE_KEY to the project's anon/publishable key (starts with 'sb_publishable_' or legacy 'eyJ...') via wrangler secret or environment config.
  3. Check for typos in the env var names so values actually land in CloudFactsEnv.
  4. 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

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)