Browser · CORS

Supabase request blocked by CORS — what it actually means

Luca Urti

has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header

Why is my Supabase request blocked by CORS?

Supabase's REST and Auth endpoints already send permissive CORS headers, so a CORS error against *.supabase.co almost never means CORS needs configuring. It means the response carrying those headers never arrived: the request went to a wrong or malformed URL and got a 404 from an origin that sends no CORS headers, the preflight failed because of a custom header you added, the project is paused, or — most often in a vibe-coded app — the call is going to your own Edge Function or API route, which does have to send the headers itself and does not. The browser reports the missing header, not the underlying failure, which is why the message points at the wrong thing.

Read the network tab, not the console

The console shows the CORS message; the network tab shows the actual status. A 404 or 000 tells you the URL is wrong or unreachable. A 401 with the CORS complaint layered on top means the request arrived and was rejected, and the real problem is the key or the session. Fixing CORS in either case fixes nothing.

If the failing request is an OPTIONS, it is a preflight. Preflights are triggered by custom headers and non-simple content types, and an Edge Function must answer them explicitly before the real request is ever sent.

Edge Functions must handle OPTIONS themselves

A function that only handles POST returns a non-2xx to the preflight, and the browser reports it as CORS. Answer OPTIONS with the allow headers, and set an explicit origin rather than * if the function accepts credentials — * and credentialed requests are mutually exclusive by specification, and a wildcard on a function that reads a session token is a permission for any site to call it with the visitor's cookies.

The fix

CORS in an Edge Function

const ALLOWED = "https://app.example.com"; // not "*" for anything credentialed

const cors = {
  "Access-Control-Allow-Origin": ALLOWED,
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
  "Access-Control-Allow-Methods": "POST, OPTIONS",
};

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: cors });

  // The session still has to be verified here. CORS is not access control —
  // it constrains browsers, and nothing constrains curl.
  const jwt = req.headers.get("authorization")?.replace("Bearer ", "");
  if (!jwt) return new Response("unauthorized", { status: 401, headers: cors });

  return new Response(JSON.stringify({ ok: true }), {
    headers: { ...cors, "content-type": "application/json" },
  });
});

The fix that works and costs you the database

The fix that always works is to stop calling Supabase from the browser and proxy every request through your own server route using the service_role key. CORS stops applying, and so does RLS — the proxy now decides who may read what, for every table, and it is usually a thin pass-through that forwards whatever id the client sent. The other version is Access-Control-Allow-Origin: * on a function that reads an authorization header, which lets any website on the internet invoke it on behalf of your logged-in users.

Whether the trap is already in your repo is a question you can answer

Sentris reads the SQL and the client code, so it reports the shortcut above where it was actually taken — a service_role key in a browser bundle, a policy that is using (true), a table with RLS switched off. A scan needs no account and no card. How often it is wrong is measured and published.

Scan my app