Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Handle CORS in Supabase Edge Functions

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.

What you'll learn

  • How to create a shared CORS headers file for Edge Functions
  • How to handle OPTIONS preflight requests in Deno
  • How to include CORS headers in both success and error responses
  • How to restrict CORS to specific origins in production
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced8 min read10-15 minSupabase (all plans), Deno runtime, @supabase/supabase-js v2+March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1// supabase/functions/_shared/cors.ts
2export 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.

2

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.

typescript
1// supabase/functions/my-function/index.ts
2import { corsHeaders } from '../_shared/cors.ts'
3
4Deno.serve(async (req) => {
5 // Handle CORS preflight
6 if (req.method === 'OPTIONS') {
7 return new Response('ok', { headers: corsHeaders })
8 }
9
10 // Your business logic here
11 try {
12 const { name } = await req.json()
13 const data = { message: `Hello ${name}!` }
14
15 return new Response(JSON.stringify(data), {
16 headers: { ...corsHeaders, 'Content-Type': 'application/json' },
17 status: 200
18 })
19 } catch (error) {
20 // CORS headers must be included in error responses too
21 return new Response(JSON.stringify({ error: error.message }), {
22 headers: { ...corsHeaders, 'Content-Type': 'application/json' },
23 status: 400
24 })
25 }
26})

Expected result: OPTIONS requests return immediately with CORS headers. POST/GET requests include CORS headers in both success and error responses.

3

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.

typescript
1Deno.serve(async (req) => {
2 if (req.method === 'OPTIONS') {
3 return new Response('ok', { headers: corsHeaders })
4 }
5
6 try {
7 // Validate input
8 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: 400
15 }
16 )
17 }
18
19 // Process request...
20 return new Response(
21 JSON.stringify({ success: true }),
22 {
23 headers: { ...corsHeaders, 'Content-Type': 'application/json' },
24 status: 200
25 }
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: 500
33 }
34 )
35 }
36})

Expected result: All response paths, including validation errors and unhandled exceptions, include CORS headers so the browser can read the response.

4

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.

typescript
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 dev
6]
7
8export function getCorsHeaders(origin: string | null) {
9 const allowedOrigin = ALLOWED_ORIGINS.includes(origin ?? '')
10 ? origin!
11 : ALLOWED_ORIGINS[0]
12
13 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}
19
20// In your function
21import { getCorsHeaders } from '../_shared/cors.ts'
22
23Deno.serve(async (req) => {
24 const origin = req.headers.get('origin')
25 const corsHeaders = getCorsHeaders(origin)
26
27 if (req.method === 'OPTIONS') {
28 return new Response('ok', { headers: corsHeaders })
29 }
30
31 // ... rest of your function
32})

Expected result: Only requests from allowed origins receive the correct CORS headers. Requests from other origins are blocked by the browser.

5

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.

typescript
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8// Call the Edge Function
9const { data, error } = await supabase.functions.invoke('my-function', {
10 body: { name: 'World' }
11})
12
13if (error) {
14 console.error('Function error:', error.message)
15} else {
16 console.log('Response:', data)
17}
18
19// Call with custom headers
20const { 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

supabase/functions/my-function/index.ts
1// supabase/functions/_shared/cors.ts
2export 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}
7
8// Helper to create JSON responses with CORS
9export function jsonResponse(
10 data: unknown,
11 status = 200
12): Response {
13 return new Response(JSON.stringify(data), {
14 headers: { ...corsHeaders, 'Content-Type': 'application/json' },
15 status,
16 })
17}
18
19// supabase/functions/my-function/index.ts
20import { corsHeaders, jsonResponse } from '../_shared/cors.ts'
21import { createClient } from 'npm:@supabase/supabase-js@2'
22
23Deno.serve(async (req) => {
24 // Handle CORS preflight
25 if (req.method === 'OPTIONS') {
26 return new Response('ok', { headers: corsHeaders })
27 }
28
29 try {
30 // Parse request body
31 const { name } = await req.json()
32 if (!name) {
33 return jsonResponse({ error: 'Name is required' }, 400)
34 }
35
36 // Create Supabase client with user's auth context
37 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 )
46
47 // Example: query with user's RLS context
48 const { data, error } = await supabase
49 .from('greetings')
50 .insert({ name, message: `Hello ${name}!` })
51 .select()
52 .single()
53
54 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.

ChatGPT Prompt

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.

Supabase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.