Fix guides

CORS error when your app calls a Supabase Edge Function or API

"Blocked by CORS policy" in the console when your Lovable, Bolt, or Cursor app calls an Edge Function or your own API. What it really means and how to fix it.

What you are seeing

Why it happens

  1. The function never answers the OPTIONS preflight. Browsers send it before any request with a JSON body or an Authorization header.
  2. CORS headers are added to the success response but not to errors. When the function throws or returns 401, the browser reports a CORS error and hides the real one.
  3. The allowed headers list leaves out authorization, apikey, x-client-info, or content-type, which supabase-js sends.
  4. Access-Control-Allow-Origin: * is combined with cookies or credentials: 'include', which browsers reject.
  5. Only localhost or the preview domain is allowed, not the production domain.
  6. The browser calls a third-party API directly that does not allow browser origins. That call belongs on a server or in an Edge Function, which also keeps the key secret.

How to fix it

1. Read the real request in DevTools

Open the Network tab, reproduce the error, and look at both the OPTIONS request and the request after it. A 404, 401, or 500 there is the actual problem; CORS is just how the browser reports it.

2. Define one set of CORS headers

// supabase/functions/_shared/cors.ts
export const corsHeaders = {
  'Access-Control-Allow-Origin': 'https://yourapp.com',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
  'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
};

3. Answer the preflight and add headers to every response, errors included

import { corsHeaders } from '../_shared/cors.ts';

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders });
  }
  try {
    const body = await req.json();
    // ... your logic
    return new Response(JSON.stringify({ ok: true }), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
    });
  } catch (err) {
    return new Response(JSON.stringify({ error: String(err) }), {
      status: 500,
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
    });
  }
});

4. Redeploy the function

supabase functions deploy my-function

5. Test the preflight from your terminal

curl -i -X OPTIONS https://<project-ref>.supabase.co/functions/v1/my-function \
  -H "Origin: https://yourapp.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization, content-type"

6. Same fix for an Express API

import cors from 'cors';

app.use(cors({
  origin: ['https://yourapp.com', 'http://localhost:5173'],
  credentials: true,
}));

How to know it is fixed

On the deployed site, the Network tab shows the OPTIONS request returning 200 or 204 with an Access-Control-Allow-Origin that matches your domain, and the real request returning your JSON. Trigger a deliberate error and confirm the app now shows that error instead of a CORS message. CORS only controls what browsers may read, so keep your auth checks on the server either way.