# How to Handle CORS in Supabase Edge Functions

- Tool: Supabase
- Difficulty: Advanced
- Time required: 10-15 min
- Compatibility: Supabase (all plans), Deno runtime, @supabase/supabase-js v2+
- Last updated: March 2026

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

## Before you start

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

```
// supabase/functions/_shared/cors.ts
export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
  'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
}
```

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

```
// supabase/functions/my-function/index.ts
import { corsHeaders } from '../_shared/cors.ts'

Deno.serve(async (req) => {
  // Handle CORS preflight
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  // Your business logic here
  try {
    const { name } = await req.json()
    const data = { message: `Hello ${name}!` }

    return new Response(JSON.stringify(data), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      status: 200
    })
  } catch (error) {
    // CORS headers must be included in error responses too
    return new Response(JSON.stringify({ error: error.message }), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      status: 400
    })
  }
})
```

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

```
Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  try {
    // Validate input
    const body = await req.json()
    if (!body.email) {
      return new Response(
        JSON.stringify({ error: 'Email is required' }),
        {
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          status: 400
        }
      )
    }

    // Process request...
    return new Response(
      JSON.stringify({ success: true }),
      {
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
        status: 200
      }
    )
  } catch (error) {
    return new Response(
      JSON.stringify({ error: 'Internal server error' }),
      {
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
        status: 500
      }
    )
  }
})
```

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

```
// supabase/functions/_shared/cors.ts (production version)
const ALLOWED_ORIGINS = [
  'https://yourdomain.com',
  'https://www.yourdomain.com',
  'http://localhost:3000' // Keep for local dev
]

export function getCorsHeaders(origin: string | null) {
  const allowedOrigin = ALLOWED_ORIGINS.includes(origin ?? '')
    ? origin!
    : ALLOWED_ORIGINS[0]

  return {
    'Access-Control-Allow-Origin': allowedOrigin,
    'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
    'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
  }
}

// In your function
import { getCorsHeaders } from '../_shared/cors.ts'

Deno.serve(async (req) => {
  const origin = req.headers.get('origin')
  const corsHeaders = getCorsHeaders(origin)

  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  // ... rest of your function
})
```

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

```
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Call the Edge Function
const { data, error } = await supabase.functions.invoke('my-function', {
  body: { name: 'World' }
})

if (error) {
  console.error('Function error:', error.message)
} else {
  console.log('Response:', data)
}

// Call with custom headers
const { data: custom } = await supabase.functions.invoke('my-function', {
  body: { name: 'World' },
  headers: { 'x-custom-header': 'my-value' }
})
```

**Expected result:** The frontend successfully calls the Edge Function, the browser allows the cross-origin request, and the response data is returned.

## Complete code example

File: `supabase/functions/my-function/index.ts`

```typescript
// supabase/functions/_shared/cors.ts
export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
  'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
}

// Helper to create JSON responses with CORS
export function jsonResponse(
  data: unknown,
  status = 200
): Response {
  return new Response(JSON.stringify(data), {
    headers: { ...corsHeaders, 'Content-Type': 'application/json' },
    status,
  })
}

// supabase/functions/my-function/index.ts
import { corsHeaders, jsonResponse } from '../_shared/cors.ts'
import { createClient } from 'npm:@supabase/supabase-js@2'

Deno.serve(async (req) => {
  // Handle CORS preflight
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  try {
    // Parse request body
    const { name } = await req.json()
    if (!name) {
      return jsonResponse({ error: 'Name is required' }, 400)
    }

    // Create Supabase client with user's auth context
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL')!,
      Deno.env.get('SUPABASE_ANON_KEY')!,
      {
        global: {
          headers: { Authorization: req.headers.get('Authorization')! },
        },
      }
    )

    // Example: query with user's RLS context
    const { data, error } = await supabase
      .from('greetings')
      .insert({ name, message: `Hello ${name}!` })
      .select()
      .single()

    if (error) return jsonResponse({ error: error.message }, 400)
    return jsonResponse(data)
  } catch (err) {
    return jsonResponse({ error: 'Internal server error' }, 500)
  }
})
```

## Common mistakes

- **Not handling the OPTIONS preflight request, causing the browser to block all cross-origin requests** — undefined Fix: Add an explicit check for req.method === 'OPTIONS' at the top of your function and return a 200 response with CORS headers.
- **Including CORS headers only in the success response, not in error responses** — undefined Fix: Include corsHeaders in every response, including validation errors (400) and server errors (500). Use a helper function to avoid forgetting.
- **Missing x-client-info and apikey in Access-Control-Allow-Headers, blocking the Supabase JS client's automatic headers** — undefined Fix: Always include 'authorization, x-client-info, apikey, content-type' in the Allow-Headers value. These are sent by the Supabase JS client.
- **Using Access-Control-Allow-Origin: '*' in production, allowing any website to call your Edge Functions** — undefined Fix: 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

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

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-handle-cors-in-supabase-edge-functions
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-handle-cors-in-supabase-edge-functions
