🎸
🚀 Beta Running
PYNGUP: Rebellion against toxic productivity
Beta limited to 100 spots. Tasks become social commitments instead of lonely to-dos.
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.
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!
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',
}
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',
},
}
)
}
})
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.
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!
})
}
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
}
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
}
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' }
}
)
}
})
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
}
}
Open the Network tab and examine both the OPTIONS and POST requests:
# 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
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.
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
If you're still experiencing CORS issues, check these points:
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
})
CORS in Supabase Edge Functions is initially confusing, but with the right implementation it's straightforward. The key points:
*
with credentials in productionWith 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!
Still having CORS problems with Supabase? Leave a comment and I'll be happy to help!
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.
Comments