Skip to main content
NikoFischer.com

Main navigation

  • Home
  • About
    • My Reading List
    • Recommended Youtube Channels
    • Life Rules
    • Podcast
  • 50-Day Challenge
  • Impressum
Sprachumschalter
  • German
  • English

Breadcrumb

  1. Home

Supabase Edge Functions CORS Error Fix - Complete Guide 2025

🎸
🚀 Beta Running

PYNGUP: Rebellion against toxic productivity

Beta limited to 100 spots. Tasks become social commitments instead of lonely to-dos.

🚀 Join Beta 📖 Read Story "€487 wasted"

CORS errors are among the most frustrating problems when developing with Supabase Edge Functions. They work perfectly in Postman or Insomnia, but as soon as you call them from the browser, you get this cryptic error message:

Access to fetch at 'https://yourproject.supabase.co/functions/v1/your-function' 
from origin 'http://localhost:3000' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

In this article, I'll show you exactly how to properly configure CORS in Supabase Edge Functions - with working code examples and common pitfalls.

The Problem: Why CORS is Different in Edge Functions

Unlike Supabase's REST API, you must handle CORS manually in Edge Functions. This is because Edge Functions are fully customizable server functions that you control yourself.

Important: Supabase does not provide automatic CORS configuration for Edge Functions!

The Solution: Implementing CORS Correctly

Step 1: Define CORS Headers

First, create a reusable CORS configuration. I recommend creating a _shared/cors.ts file:

// 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',
}

Step 2: Handle OPTIONS Request (CRITICAL!)

This is the most important part: The OPTIONS check must be at the very top of your function:

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

Deno.serve(async (req) => {
  // THIS CHECK MUST BE AT THE TOP!
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  // Your actual function logic here
  try {
    const data = await req.json()
    
    // Processing...
    
    return new Response(
      JSON.stringify({ success: true, data }),
      {
        headers: {
          ...corsHeaders,
          'Content-Type': 'application/json',
        },
      }
    )
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      {
        status: 400,
        headers: {
          ...corsHeaders,
          'Content-Type': 'application/json',
        },
      }
    )
  }
})

Why the OPTIONS Check Must Be at the Top

Browsers send a preflight request (OPTIONS) before executing the actual request. This preflight asks: "Am I allowed to make this request?"

If your OPTIONS handler doesn't come first, your function might throw an error before it can even send the CORS headers.

Common Mistakes and Their Solutions

Mistake 1: CORS Headers Only on Successful Responses

Wrong:

if (error) {
  return new Response('Error', { status: 500 })
  // No CORS headers!
}

Correct:

if (error) {
  return new Response('Error', { 
    status: 500,
    headers: corsHeaders  // CORS headers on errors too!
  })
}

Mistake 2: Incomplete Headers

Make sure your headers contain all necessary values:

export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 
    'authorization, x-client-info, apikey, content-type, x-requested-with',
  'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE, PATCH',
  'Access-Control-Max-Age': '86400', // Cache preflight for 24h
}

Mistake 3: Credentials with Wildcard Origin

Security issue:

// NEVER in production!
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'

Better for production:

const allowedOrigins = [
  'https://yourapp.com',
  'https://www.yourapp.com',
  'http://localhost:3000' // only for development
]

const origin = req.headers.get('origin')
const corsOrigin = allowedOrigins.includes(origin) ? origin : 'null'

const corsHeaders = {
  'Access-Control-Allow-Origin': corsOrigin,
  'Access-Control-Allow-Credentials': 'true',
  // ...other headers
}

Complete Working Example

Here's a complete Edge Function with proper CORS handling:

// supabase/functions/user-profile/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { corsHeaders } from '../_shared/cors.ts'

interface ProfileRequest {
  userId: string
  name: string
  email: string
}

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

  try {
    // Initialize Supabase client
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
    )

    // Parse request body
    const { userId, name, email }: ProfileRequest = await req.json()

    // Validation
    if (!userId || !name || !email) {
      return new Response(
        JSON.stringify({ 
          error: 'userId, name, and email are required' 
        }),
        {
          status: 400,
          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
        }
      )
    }

    // Database operation
    const { data, error } = await supabase
      .from('profiles')
      .upsert({ user_id: userId, name, email })
      .select()

    if (error) {
      return new Response(
        JSON.stringify({ error: error.message }),
        {
          status: 500,
          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
        }
      )
    }

    // Successful response
    return new Response(
      JSON.stringify({ success: true, profile: data[0] }),
      {
        headers: { ...corsHeaders, 'Content-Type': 'application/json' }
      }
    )

  } catch (error) {
    return new Response(
      JSON.stringify({ error: 'Unknown error occurred' }),
      {
        status: 500,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' }
      }
    )
  }
})

Frontend Integration

How to correctly call the function from your frontend:

// React/Next.js example
const updateProfile = async (userData) => {
  try {
    const response = await fetch(
      'https://your-project.supabase.co/functions/v1/user-profile',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${supabase.auth.session()?.access_token}`,
          'apikey': 'your-anon-key'
        },
        body: JSON.stringify(userData)
      }
    )

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }

    const result = await response.json()
    return result
  } catch (error) {
    console.error('Profile update failed:', error)
    throw error
  }
}

Testing and Debugging

1. Use Browser Developer Tools

Open the Network tab and examine both the OPTIONS and POST requests:

  • OPTIONS Request: Should return status 200 with CORS headers
  • Actual Request: Your actual API call

2. CURL for Quick Tests

# Test OPTIONS request
curl -X OPTIONS \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type" \
  https://your-project.supabase.co/functions/v1/your-function

# Expected response should contain CORS headers

3. Common Browser-Specific Issues

Firefox: Sometimes you need to completely restart the browser when CORS issues occur. Firefox can fail to properly clean up WebSocket connections.

Safari: Particularly strict with CORS policies. Make sure all headers are exactly correct.

Local Development

For local testing with the Supabase CLI:

# Start Edge Functions locally
supabase functions serve --debug

# Your function will be available at:
# http://localhost:54321/functions/v1/your-function

Troubleshooting Checklist

If you're still experiencing CORS issues, check these points:

  1. ✅ OPTIONS handler is the first thing in your function
  2. ✅ CORS headers are included in ALL responses (success and error)
  3. ✅ All required headers are listed in Access-Control-Allow-Headers
  4. ✅ Your frontend is sending the correct Content-Type header
  5. ✅ You're not mixing wildcard origins with credentials in production

Advanced: Dynamic Origin Handling

For applications with multiple domains:

const getDynamicCorsHeaders = (request: Request) => {
  const origin = request.headers.get('origin')
  
  // Define your allowed origins
  const allowedOrigins = [
    'https://app.yoursite.com',
    'https://admin.yoursite.com',
    ...(Deno.env.get('ENVIRONMENT') === 'development' 
      ? ['http://localhost:3000', 'http://127.0.0.1:3000'] 
      : [])
  ]
  
  const corsOrigin = allowedOrigins.includes(origin) ? origin : null
  
  return {
    'Access-Control-Allow-Origin': corsOrigin || 'null',
    'Access-Control-Allow-Headers': 
      'authorization, x-client-info, apikey, content-type',
    'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
    'Access-Control-Allow-Credentials': 'true',
  }
}

// Use in your function:
Deno.serve(async (req) => {
  const corsHeaders = getDynamicCorsHeaders(req)
  
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }
  
  // ... rest of your function
})

Conclusion

CORS in Supabase Edge Functions is initially confusing, but with the right implementation it's straightforward. The key points:

  1. OPTIONS handler must be at the very top
  2. Include CORS headers in ALL responses
  3. Never use * with credentials in production
  4. Use browser developer tools for debugging

With these examples, your CORS problems should be a thing of the past. If you're still having issues, double-check the order of your code - the OPTIONS check really must come first!

Additional Resources

  • Supabase CORS Documentation
  • MDN CORS Guide
  • Supabase Edge Functions Examples

Still having CORS problems with Supabase? Leave a comment and I'll be happy to help!

Tags

  • Supabase

Comments

About text formats

Restricted HTML

  • Allowed HTML tags: <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.

Related articles

Supabase: How to query users table?
Supabase throws "new row violates row-level security policy for table" even though I had created a row level policy for inserts
Supabase vs. Firebase - Unveiling the Differences
Supabase Storage: How to Implement File Upload Properly
Building a ChatGPT Clone with Supabase Edge Functions and OpenAI

About the author

Nikolai Fischer is the founder of Kommune3 (since 2007) and a leading expert in Drupal development and tech entrepreneurship. With 17+ years of experience, he has led hundreds of projects and achieved #1 on Hacker News. As host of the "Kommit mich" podcast and founder of skillution, he combines technical expertise with entrepreneurial thinking. His articles about Supabase, modern web development, and systematic problem-solving have influenced thousands of developers worldwide.

Ihre Anmeldung konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.
Ihre Anmeldung war erfolgreich.

Newsletter

Join a growing community of friendly readers. From time to time I share my thoughts about rational thinking, productivity and life.

Nikolai Fischer

✌ Hi, I'm Niko
Entrepreneur, developer & podcaster

Contact me:

  • E-Mail
  • Phone
  • LinkedIn

My Reading List

  • Algorithmic Trading - Ernie Chan
  • Let Me Tell You a Story: Tales Along the Road to Happiness - Jorge Bucay
  • Mindset: The New Psychology of Success - Carol S. Dweck
  • Deep Work: Rules for Focused Success in a Distracted World - Cal Newport
  • The Café on the Edge of the World: A Story About the Meaning of Life - John Strelecky
more
RSS feed