# 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.

Source: https://supahost.app/docs/functions/cors · Updated 2026-11-11

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`:

```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:

```ts
// 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](/kb/function-cors-error).

## Related

- [Calling functions from your app](/docs/functions/calling-functions)
- [Authentication and JWT verification](/docs/functions/auth)
- [Writing functions: Deno, imports and structure](/docs/functions/writing-functions)
