CORS for browser calls

Why browser calls to Edge Functions need CORS headers, how preflight requests work on SupaHost, and a reusable cors.ts helper with an origin allowlist.

Your web app runs on its own domain (https://bookings.example.com, http://localhost:5173), and your functions live on https://app-one.acme.supahost.space. A browser treats that as a cross-origin request, so it only lets your page read the response if the function sends the right CORS headers.

Server-side callers (curl, Node, webhooks, cron) don't care about CORS. You only need this for code that runs in a browser.

How a browser call works

  1. Because supabase-js sends an Authorization header and a JSON body, the browser first sends a preflight: an OPTIONS request asking which origins, headers and methods are allowed.
  2. If the preflight response allows them, the browser sends the real request.
  3. The browser then checks the real response for Access-Control-Allow-Origin before handing it to your code.

On SupaHost:

  • Preflights reach your function. The router skips the JWT check for OPTIONS, so your code must answer it.
  • Router errors already carry CORS headers. The 401, 404 and 5xx responses the router produces itself include Access-Control-Allow-Origin: * and expose the sb-error-code header, so your app can read those errors.
  • Your own responses need the headers too, including the error responses you return.

A reusable helper

Add a file with + File named _shared/cors.ts:

// _shared/cors.ts
const ALLOWED_ORIGINS = new Set([
  "https://bookings.example.com",
  "http://localhost:5173",
]);

export function corsHeaders(req: Request): Record<string, string> {
  const origin = req.headers.get("Origin") ?? "";
  return {
    "Access-Control-Allow-Origin": ALLOWED_ORIGINS.has(origin) ? origin : "https://bookings.example.com",
    "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
    "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
    "Access-Control-Max-Age": "86400",
    "Vary": "Origin",
  };
}

Use it on every path through the function:

// index.ts
import { corsHeaders } from "./_shared/cors.ts";

Deno.serve(async (req) => {
  const cors = corsHeaders(req);

  if (req.method === "OPTIONS") {
    return new Response(null, { status: 204, headers: cors });
  }

  try {
    const { guests } = await req.json();
    if (!Number.isInteger(guests) || guests < 1 || guests > 12) {
      return Response.json({ error: "guests must be between 1 and 12" }, { status: 400, headers: cors });
    }
    return Response.json({ tables: Math.ceil(guests / 4) }, { headers: cors });
  } catch {
    return Response.json({ error: "Send a JSON body" }, { status: 400, headers: cors });
  }
});

If you don't need to limit origins (a public, read-only function, say), "Access-Control-Allow-Origin": "*" is fine. Since supabase-js authenticates with the Authorization header rather than cookies, you don't need Access-Control-Allow-Credentials.

Which headers to allow

Access-Control-Allow-Headers must list every header your client sends that isn't on the browser's safe list. supabase-js sends authorization, apikey, content-type and x-client-info. If you add your own (for example x-idempotency-key via the headers option of functions.invoke), add it to the list too.

Access-Control-Allow-Methods matters when you call with PUT, PATCH or DELETE. GET and POST are allowed without it.

Debugging a CORS error

The browser console usually hides the real cause behind a generic CORS message. Check in this order:

  1. Open the Network tab and look at the OPTIONS request. If it isn't 200/204 with the headers above, your function doesn't handle OPTIONS or throws before reaching that line.
  2. Look at the real request. A 401 from the router means the token was missing or invalid, not a CORS problem. The sb-error-code header says which.
  3. A 404 NOT_FOUND means the function name in the URL is wrong or the function isn't live yet. If the project has no live functions at all, that 404 comes without CORS headers, so the browser may report it as a CORS error.
  4. Check the function's Logs card for exceptions. An uncaught error returns a router 500, which has CORS headers, but if your code returned its own response without them, the browser blocks it.

More fixes: CORS error calling an Edge Function.

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us