Supabase Edge Functions require manual CORS handling because, unlike the REST API, they do not include CORS headers automatically. Create a shared cors.ts file with the allowed origins, methods, and headers. Import these headers into every Edge Function and return them in all responses, including error responses. Handle the OPTIONS preflight request explicitly by returning a 200 response with the CORS headers. Missing CORS headers is the most common Edge Function error.
The Canonical CORS Pattern for Supabase Edge Functions
When your frontend calls a Supabase Edge Function, the browser sends a preflight OPTIONS request before the actual request. If the Edge Function does not respond with the correct CORS headers, the browser blocks the request entirely. This tutorial shows you the canonical pattern used by the Supabase team: a shared _shared/cors.ts file that you import into every Edge Function. You will learn how to handle preflight requests, include CORS headers in all response paths, and restrict origins for production security.
Prerequisites
- A Supabase project with the CLI installed
- At least one Edge Function created (supabase functions new)
- A frontend application that calls the Edge Function from the browser
- Basic understanding of HTTP headers and browser security
Step-by-step guide
Create the shared CORS headers file
Create the shared CORS headers file
Create a _shared directory inside supabase/functions/ and add a cors.ts file. This file exports a corsHeaders object that includes Access-Control-Allow-Origin, Access-Control-Allow-Headers, and Access-Control-Allow-Methods. Every Edge Function in your project will import from this file, ensuring consistent CORS configuration. Use '*' for the origin during development and restrict it to your domain in production.
1// supabase/functions/_shared/cors.ts2export const corsHeaders = {3 'Access-Control-Allow-Origin': '*',4 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',5 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',6}Expected result: The _shared/cors.ts file is created and can be imported by any Edge Function in the project.
Handle the OPTIONS preflight request
Handle the OPTIONS preflight request
Browsers send an OPTIONS request before any cross-origin POST, PUT, or DELETE request (and before GET requests with custom headers). Your Edge Function must detect this preflight request and respond immediately with a 200 status and the CORS headers, without executing any business logic. If you do not handle OPTIONS, the browser never sends the actual request.
1// supabase/functions/my-function/index.ts2import { corsHeaders } from '../_shared/cors.ts'34Deno.serve(async (req) => {5 // Handle CORS preflight6 if (req.method === 'OPTIONS') {7 return new Response('ok', { headers: corsHeaders })8 }910 // Your business logic here11 try {12 const { name } = await req.json()13 const data = { message: `Hello ${name}!` }1415 return new Response(JSON.stringify(data), {16 headers: { ...corsHeaders, 'Content-Type': 'application/json' },17 status: 20018 })19 } catch (error) {20 // CORS headers must be included in error responses too21 return new Response(JSON.stringify({ error: error.message }), {22 headers: { ...corsHeaders, 'Content-Type': 'application/json' },23 status: 40024 })25 }26})Expected result: OPTIONS requests return immediately with CORS headers. POST/GET requests include CORS headers in both success and error responses.
Include CORS headers in error responses
Include CORS headers in error responses
A common mistake is including CORS headers only in the success response. If your function throws an error and the error response does not include CORS headers, the browser blocks the error response and your frontend cannot read the error message. Wrap your business logic in a try-catch and always include corsHeaders in the catch response.
1Deno.serve(async (req) => {2 if (req.method === 'OPTIONS') {3 return new Response('ok', { headers: corsHeaders })4 }56 try {7 // Validate input8 const body = await req.json()9 if (!body.email) {10 return new Response(11 JSON.stringify({ error: 'Email is required' }),12 {13 headers: { ...corsHeaders, 'Content-Type': 'application/json' },14 status: 40015 }16 )17 }1819 // Process request...20 return new Response(21 JSON.stringify({ success: true }),22 {23 headers: { ...corsHeaders, 'Content-Type': 'application/json' },24 status: 20025 }26 )27 } catch (error) {28 return new Response(29 JSON.stringify({ error: 'Internal server error' }),30 {31 headers: { ...corsHeaders, 'Content-Type': 'application/json' },32 status: 50033 }34 )35 }36})Expected result: All response paths, including validation errors and unhandled exceptions, include CORS headers so the browser can read the response.
Restrict CORS origins for production
Restrict CORS origins for production
Using Access-Control-Allow-Origin: '*' allows any website to call your Edge Function. In production, restrict the origin to your specific domain(s). You can hardcode the production origin, read it from an environment variable, or implement dynamic origin checking that validates against an allowlist.
1// supabase/functions/_shared/cors.ts (production version)2const ALLOWED_ORIGINS = [3 'https://yourdomain.com',4 'https://www.yourdomain.com',5 'http://localhost:3000' // Keep for local dev6]78export function getCorsHeaders(origin: string | null) {9 const allowedOrigin = ALLOWED_ORIGINS.includes(origin ?? '')10 ? origin!11 : ALLOWED_ORIGINS[0]1213 return {14 'Access-Control-Allow-Origin': allowedOrigin,15 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',16 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',17 }18}1920// In your function21import { getCorsHeaders } from '../_shared/cors.ts'2223Deno.serve(async (req) => {24 const origin = req.headers.get('origin')25 const corsHeaders = getCorsHeaders(origin)2627 if (req.method === 'OPTIONS') {28 return new Response('ok', { headers: corsHeaders })29 }3031 // ... rest of your function32})Expected result: Only requests from allowed origins receive the correct CORS headers. Requests from other origins are blocked by the browser.
Call the Edge Function from the frontend with supabase.functions.invoke
Call the Edge Function from the frontend with supabase.functions.invoke
The Supabase JS client's functions.invoke method automatically includes the apikey and authorization headers. Because you configured these in your CORS Allow-Headers, the browser allows the request. The client also handles JSON serialization and deserialization for you.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78// Call the Edge Function9const { data, error } = await supabase.functions.invoke('my-function', {10 body: { name: 'World' }11})1213if (error) {14 console.error('Function error:', error.message)15} else {16 console.log('Response:', data)17}1819// Call with custom headers20const { data: custom } = await supabase.functions.invoke('my-function', {21 body: { name: 'World' },22 headers: { 'x-custom-header': 'my-value' }23})Expected result: The frontend successfully calls the Edge Function, the browser allows the cross-origin request, and the response data is returned.
Complete working example
1// supabase/functions/_shared/cors.ts2export const corsHeaders = {3 'Access-Control-Allow-Origin': '*',4 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',5 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',6}78// Helper to create JSON responses with CORS9export function jsonResponse(10 data: unknown,11 status = 20012): Response {13 return new Response(JSON.stringify(data), {14 headers: { ...corsHeaders, 'Content-Type': 'application/json' },15 status,16 })17}1819// supabase/functions/my-function/index.ts20import { corsHeaders, jsonResponse } from '../_shared/cors.ts'21import { createClient } from 'npm:@supabase/supabase-js@2'2223Deno.serve(async (req) => {24 // Handle CORS preflight25 if (req.method === 'OPTIONS') {26 return new Response('ok', { headers: corsHeaders })27 }2829 try {30 // Parse request body31 const { name } = await req.json()32 if (!name) {33 return jsonResponse({ error: 'Name is required' }, 400)34 }3536 // Create Supabase client with user's auth context37 const supabase = createClient(38 Deno.env.get('SUPABASE_URL')!,39 Deno.env.get('SUPABASE_ANON_KEY')!,40 {41 global: {42 headers: { Authorization: req.headers.get('Authorization')! },43 },44 }45 )4647 // Example: query with user's RLS context48 const { data, error } = await supabase49 .from('greetings')50 .insert({ name, message: `Hello ${name}!` })51 .select()52 .single()5354 if (error) return jsonResponse({ error: error.message }, 400)55 return jsonResponse(data)56 } catch (err) {57 return jsonResponse({ error: 'Internal server error' }, 500)58 }59})Common mistakes when handling CORS in Supabase Edge Functions
Why it's a problem: Not handling the OPTIONS preflight request, causing the browser to block all cross-origin requests
How to avoid: Add an explicit check for req.method === 'OPTIONS' at the top of your function and return a 200 response with CORS headers.
Why it's a problem: Including CORS headers only in the success response, not in error responses
How to avoid: Include corsHeaders in every response, including validation errors (400) and server errors (500). Use a helper function to avoid forgetting.
Why it's a problem: Missing x-client-info and apikey in Access-Control-Allow-Headers, blocking the Supabase JS client's automatic headers
How to avoid: Always include 'authorization, x-client-info, apikey, content-type' in the Allow-Headers value. These are sent by the Supabase JS client.
Why it's a problem: Using Access-Control-Allow-Origin: '*' in production, allowing any website to call your Edge Functions
How to avoid: In production, set the origin to your specific domain or implement a dynamic origin check against an allowlist.
Best practices
- Create a _shared/cors.ts file and import it in every Edge Function for consistent CORS configuration
- Always handle OPTIONS preflight requests before any business logic
- Include CORS headers in all responses: success, validation error, and server error
- Include authorization, x-client-info, apikey, and content-type in Access-Control-Allow-Headers
- Restrict Access-Control-Allow-Origin to your specific domain(s) in production
- Create a jsonResponse helper function that automatically includes CORS headers
- Test Edge Functions locally with supabase functions serve before deploying
- Add custom headers to Access-Control-Allow-Headers if you pass them in functions.invoke
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My Supabase Edge Function is being blocked by CORS in the browser. Show me the canonical CORS pattern with a shared cors.ts file, OPTIONS preflight handling, and how to include CORS headers in all response paths including errors.
Create a Supabase Edge Function with proper CORS handling: a _shared/cors.ts file with headers, OPTIONS preflight handling, a jsonResponse helper, and a production-ready origin allowlist. Show both the Deno function code and the frontend invocation with supabase.functions.invoke.
Frequently asked questions
Why do Supabase Edge Functions need manual CORS handling?
Unlike the Supabase REST API (which goes through the Kong API gateway that adds CORS headers automatically), Edge Functions run on a separate Deno runtime that does not add CORS headers. You must include them explicitly in every response.
What is an OPTIONS preflight request?
When a browser makes a cross-origin request with custom headers (like authorization), it first sends an OPTIONS request to check if the server allows the request. If the server does not respond with the correct CORS headers, the browser blocks the actual request.
Why does my Edge Function work in Postman but not from the browser?
Postman does not enforce CORS policies. Browsers do. If your function works in Postman but fails from the browser, you have a CORS issue. Check that you handle OPTIONS requests and include CORS headers in all responses.
Can I use a wildcard (*) for Access-Control-Allow-Origin in production?
Technically yes, but it is a security risk. Any website could call your Edge Functions. In production, restrict the origin to your specific domain(s) using a dynamic origin check.
Do I need CORS if I call the Edge Function from another Edge Function?
No. CORS is a browser-only security mechanism. Server-to-server requests (Edge Function to Edge Function, or backend to Edge Function) are not subject to CORS restrictions.
What headers does supabase.functions.invoke send automatically?
The JS client sends authorization (with the user's JWT), apikey (the anon key), x-client-info, and content-type headers automatically. All of these must be listed in your Access-Control-Allow-Headers.
Can RapidDev help debug CORS issues in my Supabase Edge Functions?
Yes. RapidDev can diagnose CORS configuration issues, set up the canonical CORS pattern across all your Edge Functions, and configure production-ready origin allowlists.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation