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:
- ✅ OPTIONS handler is the first thing in your function
- ✅ CORS headers are included in ALL responses (success and error)
- ✅ All required headers are listed in Access-Control-Allow-Headers
- ✅ Your frontend is sending the correct Content-Type header
- ✅ 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:
- OPTIONS handler must be at the very top
- Include CORS headers in ALL responses
- Never use
*with credentials in production - 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
- permission denied for table (42501)
- PGRST116: multiple (or no) rows returned
- Fix RLS insert errors: new row violates row-level security policy
- Supabase Storage File Upload Guide
- Supabase: infinite recursion detected in policy
- Supabase Auth: Invalid login credentials
- Supabase Edge Functions: Invalid JWT / 401
- Supabase Storage 403 RLS on upload
- 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!
Comments