All posts
No-Code & Tools

Supabase Edge Function CORS Error? Fix the Preflight Before Rewriting Your App

A browser CORS error does not automatically mean your Edge Function logic is broken. Trace the preflight, headers, origin and response path first.

Elvis Onunwa16 September 20266 min readLast updated: 16 September 2026
Supabase Edge Function CORS Error? Fix the Preflight Before Rewriting Your App

TL;DR

  • If an Edge Function works from curl, Postman or the Supabase dashboard but fails in a browser, inspect the browser's OPTIONS preflight before rewriting the function logic.
  • For current supabase-js versions, Supabase provides shared CORS headers and recommends handling preflight explicitly or using its wrapper that handles CORS automatically.
  • CORS headers must be present on error responses too; a real server error can otherwise appear to the browser as only a CORS failure.
  • When a custom domain, preview URL or frontend host changes, re-check any origin allow-list rather than weakening production CORS blindly.
On this page⌄

A Supabase Edge Function can be completely healthy and still look broken from your frontend.

The giveaway is simple: the function works from curl, Postman, the Supabase dashboard or another server-side caller, but the browser reports a CORS error.

Do not start by asking an AI builder to rewrite the function.

First trace this path:

browser page → OPTIONS preflight → allowed origin/method/headers → actual request → function → response headers → browser

If the preflight fails, the browser may never send the POST or PUT you are debugging. If the function throws an error but the error response omits CORS headers, the browser can also hide the useful error behind a CORS message.

That distinction saves a lot of destructive debugging.

First prove whether this is really a browser-only failure

Test the same endpoint from two places:

  1. the browser application that is failing;
  2. a non-browser caller such as curl, the Supabase dashboard invocation tool or an API client.

If both fail in the same way, CORS may not be the primary problem. Check authentication, deployment, function logs and the actual server error.

If the non-browser call succeeds but the browser fails, open the browser Network panel and look for an OPTIONS request immediately before the real request.

Supabase’s current troubleshooting guidance specifically recommends checking CORS configuration, function logs and authentication when an Edge Function cannot be invoked. See Supabase’s Edge Function troubleshooting guide.

The useful question is not merely “Do I have CORS headers?”

It is “Does every browser request path receive the headers it needs?”

Understand what the preflight is asking

For many cross-origin requests, the browser asks permission before sending the real request.

That OPTIONS request effectively asks the server:

  • Is this origin allowed?
  • Is this HTTP method allowed?
  • Are these request headers allowed?

Supabase’s current CORS documentation says browser-invoked Edge Functions need CORS preflight handling. It now provides automatic handling through its withSupabase wrapper, while functions that do not use that wrapper can handle CORS manually. See the current Supabase CORS guide.

For @supabase/supabase-js 2.95.0 and later, Supabase also provides corsHeaders from the client library so the allowed-header list stays aligned with headers its clients send.

A minimal manual pattern looks like this:

import { corsHeaders } from 'npm:@supabase/supabase-js@^2/cors'

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

  return new Response(JSON.stringify({ ok: true }), {
    headers: {
      ...corsHeaders,
      'Content-Type': 'application/json',
    },
  })
})

Do not copy code blindly. The important structure is preflight handled early, then CORS headers returned with the actual response.

A missing allowed header can break a function that used to work

This is one reason hard-coded CORS snippets age badly.

Your frontend may send more than Content-Type and Authorization. Supabase’s current helper includes the headers its client libraries use, including apikey and x-client-info; current documentation also notes retry and tracing headers that can appear in newer client configurations.

So this configuration can become fragile:

Access-Control-Allow-Headers: authorization, content-type

The function logic may be unchanged, but a client update or new request behaviour introduces a header the preflight does not permit.

That is why I prefer a maintained platform helper where it fits the security model instead of scattering slightly different CORS strings across several generated functions.

If you maintain multiple Edge Functions, centralize the rule rather than debugging the same header mismatch five times.

Your error response needs CORS headers too

This is an easy trap in AI-generated functions.

The success path includes CORS headers:

return new Response(JSON.stringify(data), {
  headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})

But the catch block does not:

catch (error) {
  return new Response(JSON.stringify({ error: error.message }), {
    status: 500,
  })
}

Now the real problem might be a database error, expired credential or third-party API failure, but the browser cannot expose the response normally because the error path itself fails CORS.

You fix the CORS symptom and finally discover the real 500 underneath it.

A better pattern is to make the response policy consistent:

catch (error) {
  return new Response(JSON.stringify({ error: 'Request failed' }), {
    status: 500,
    headers: {
      ...corsHeaders,
      'Content-Type': 'application/json',
    },
  })
}

Then keep sensitive internal details in server logs rather than returning secrets or stack traces to the browser.

Supabase provides invocation and function logs in the dashboard, including request/response metadata and execution information. Its logging guide explains the available views.

Check the origin after moving from preview to a custom domain

A common production failure is not a broken function. It is an old assumption about where the frontend lives.

During development, your application may run on:

https://project-preview.example

Then production moves to:

https://yourcompany.com

If your Edge Function uses an explicit origin allow-list and still recognizes only the preview host, the custom-domain browser request fails.

The same can happen when you add www, a staging domain or another legitimate frontend.

Do not solve that by automatically changing every endpoint to Access-Control-Allow-Origin: *.

First decide which browser origins should legitimately call the endpoint. Then update and test that policy deliberately.

This is especially important when an AI builder gets an application most of the way to production: preview success proves the feature works in one environment, not that the production trust boundary is correct.

Separate CORS from authentication

CORS and authentication are different controls.

CORS answers whether a browser is allowed to read a cross-origin response. Authentication answers who the caller is and what they may do.

A successful OPTIONS response does not authenticate the eventual user.

Likewise, adding more JWT logic will not fix an OPTIONS response that is missing the required CORS headers.

If your actual request is returning 401 rather than failing preflight, use my guide to diagnosing Supabase Edge Function 401 errors. That article focuses on the gateway and authentication boundary rather than browser origin policy.

Do not confuse CORS with a slow or failing function

Browsers sometimes make the visible symptom less informative than the server-side cause.

After the preflight succeeds, inspect the actual request separately:

  • 401/403: investigate authentication or authorization;
  • 500: inspect the function’s execution and downstream dependencies;
  • 504: investigate execution latency and whether the work belongs in a synchronous request;
  • no invocation at all: return to URL, gateway, preflight and network diagnosis.

If the request reaches the function but eventually times out, see Supabase Edge Function 504: fixing timeouts without just increasing retries.

The debugging rule is simple: name the failing boundary before changing the architecture.

A practical production test

Before declaring the fix complete, test more than one happy-path POST.

Check:

  1. OPTIONS from the real production origin;
  2. the actual authenticated request;
  3. an intentionally invalid request that produces a 4xx response;
  4. a forced or safe test error path;
  5. the production custom domain, not only localhost;
  6. any preview/staging origin you intentionally support;
  7. function invocation and application logs.

Then verify that the browser receives useful application errors without exposing sensitive server details.

If you are preparing an AI-built application for real users, my AI-built app production checklist covers the wider handoff: authentication, permissions, payments, observability, ownership and recovery.

The decision

When a Supabase Edge Function works outside the browser but fails inside it, do not treat the CORS message as permission to rewrite everything.

Find the OPTIONS request. Check the origin, method and requested headers. Make sure the preflight response is correct. Make sure every actual response path carries the appropriate CORS policy. Then diagnose authentication or application errors separately.

That is a much safer production workflow than repeatedly prompting an AI builder with “fix CORS” until the console becomes quiet.

If your AI-built MVP has reached the stage where one fix keeps creating another production problem, see my MVP development service or contact me.

Frequently asked questions

Questions, answered.

Why does my Supabase Edge Function work in Postman but fail in the browser?

Browsers enforce CORS and may send an OPTIONS preflight before the real request. Non-browser clients do not enforce the same browser policy, so a successful Postman or curl request does not prove the browser path is configured correctly.

Should I set Access-Control-Allow-Origin to *?

It can be appropriate for simple public endpoints, and Supabase's default corsHeaders helper uses a wildcard origin. For applications that intentionally restrict browser origins, keep an explicit allow-list and make sure production, preview and custom domains are handled deliberately.

Why is there no useful error in my frontend except a CORS message?

The browser may hide the underlying response when the response itself lacks the required CORS headers. Inspect the Network panel and Supabase invocation/log views, and ensure success and error responses both return the appropriate CORS headers.

NEED HELP WITH THE BUILD?

Turn what you learned here into a clearer website or MVP.

Share the goal, current stage, and constraints. I’ll help you identify the smallest responsible next step.

Continue reading

Related notes.