BuiltSafely

Home/Fixes

Supabase

CORS error calling a Supabase Edge Function from the browser

The browser's OPTIONS preflight isn't answered. Handle it with withSupabase or the SDK's corsHeaders, and keep secrets server-side.

  • Docs-only
  • Reviewed Oct 2026
  • 3 official sources
Sources Last checked October 19, 2026 · Badge: docs-only (Supabase Edge Functions CORS docs) · Sources: official docs (linked below) · Educational, not an audit

The fix

Browsers send a CORS preflight (OPTIONS) before cross-origin calls. Your Edge Function must answer that preflight and return CORS headers on the real response. Supabase's docs give two supported paths (Supabase: CORS for Edge Functions):

Preferred: wrap the handler with withSupabase, which "handles CORS and preflight (OPTIONS) requests for you":

import { withSupabase } from 'npm:@supabase/server@^1'

export default {
  fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
    const { name } = await req.json()
    return Response.json({ message: `Hello ${name}!` })
  }),
}

Manual: if you don't use withSupabase, import corsHeaders from the SDK (v2.95.0+) and return them on both OPTIONS and your responses:

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

export default {
  fetch: async (req) => {
    if (req.method === 'OPTIONS') {
      return Response.json({ ok: true }, { headers: corsHeaders })
    }
    try {
      const { name } = await req.json()
      return Response.json({ message: `Hello ${name}!` }, { headers: corsHeaders })
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error)
      return Response.json({ error: message }, { status: 400, headers: corsHeaders })
    }
  },
}

Redeploy the function after the change. Call it with the user's JWT (for example supabase.functions.invoke) when the function uses auth: 'user', not with a secret key in the browser (Supabase: Securing Edge Functions).

Why it happens

A page on https://yourapp.com calling https://YOUR_PROJECT.supabase.co/functions/v1/... is cross-origin. The browser asks permission with OPTIONS first. If the function doesn't handle that request or omits the headers the browser expects, DevTools shows a CORS error and your fetch fails before your JSON runs (Supabase: CORS for Edge Functions).

Importing corsHeaders from the SDK "keeps your allow-list aligned with the headers the client libraries send" (Supabase: CORS for Edge Functions).

What not to do

  • Don't "fix" CORS by putting a secret or service_role key in the browser. Secret keys bypass RLS and must stay server-side (Supabase: API keys).
  • Don't disable RLS on tables the function writes to.
  • Don't turn off verify_jwt just to clear a 401 during development and leave it off in production. Supabase notes that turning it off removes the platform check (Supabase: Securing Edge Functions). Prefer fixing the client so it sends a real user JWT, or pick the right withSupabase auth mode (user, secret, publishable, or none for signed webhooks).

AI prompt

Calling my Supabase Edge Function from the browser fails with a CORS / preflight error.
Update the function to handle OPTIONS and return CORS headers, preferably via withSupabase from @supabase/server, or manually with corsHeaders from @supabase/supabase-js/cors.
Keep secret and service_role keys server-only. Do not disable RLS or leave verify_jwt off in production.
Show the function diff first and wait for my OK.

How to verify

  1. From your web app origin, call the function while signed in. Confirm Network shows OPTIONS → 200 with CORS headers, then the real method → 200.
  2. Signed out, confirm a user-only function rejects the call.
  3. Two-account check: as user A, the function only reads or changes A's data (RLS still applies when you use a user-scoped client) (our RLS guide).

Supabase RLS guide · Invalid API key · JWT expired · Stripe webhook signature

Your turn

Did withSupabase clear it, or did you need the manual corsHeaders path? Tell us which stack you're on (plain Deno, Next proxy, etc.).

This guide is educational and based on the vendor's public docs as of the review date. Platforms change: check the linked sources. Not a security audit. Disclaimer.

The BuiltSafely weekly

One email a week. What changed, and the one fix that matters.

Platform changes that affect the security of AI-built apps, checked against official docs. Free. Unsubscribe any time.