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
- Because supabase-js sends an
Authorizationheader and a JSON body, the browser first sends a preflight: anOPTIONSrequest asking which origins, headers and methods are allowed. - If the preflight response allows them, the browser sends the real request.
- The browser then checks the real response for
Access-Control-Allow-Originbefore 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,404and5xxresponses the router produces itself includeAccess-Control-Allow-Origin: *and expose thesb-error-codeheader, 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:
- Open the Network tab and look at the
OPTIONSrequest. If it isn't200/204with the headers above, your function doesn't handleOPTIONSor throws before reaching that line. - Look at the real request. A
401from the router means the token was missing or invalid, not a CORS problem. Thesb-error-codeheader says which. - A
404 NOT_FOUNDmeans the function name in the URL is wrong or the function isn't live yet. If the project has no live functions at all, that404comes without CORS headers, so the browser may report it as a CORS error. - 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.