Pass custom headers to Supabase Edge Functions using the headers option in supabase.functions.invoke() from the client, or by setting headers directly in a fetch request. Inside the Edge Function, read headers from the Request object with req.headers.get('header-name'). The Supabase JS client automatically sends authorization, apikey, and x-client-info headers. Custom headers like API keys for third-party services or content types must be added to your CORS Access-Control-Allow-Headers list.
Sending and Reading Custom Headers in Supabase Edge Functions
Supabase Edge Functions receive HTTP requests with headers that carry authentication tokens, content types, API keys, and custom metadata. This tutorial shows you how to send custom headers from the frontend using the Supabase JS client, how to read them inside the Deno function, and how to configure CORS to allow your custom headers through the browser's preflight check. You will also learn how to forward the user's auth token to create an authenticated Supabase client inside the function.
Prerequisites
- A Supabase project with at least one Edge Function deployed
- The Supabase CLI installed for local development
- @supabase/supabase-js v2 installed in your frontend project
- Understanding of CORS headers (see the CORS tutorial for background)
Step-by-step guide
Send custom headers with supabase.functions.invoke
Send custom headers with supabase.functions.invoke
The Supabase JS client's functions.invoke method accepts a headers option for sending custom headers alongside the automatic ones. The client always sends authorization (with the user's JWT), apikey, and x-client-info headers. Any custom headers you add are merged with these defaults. Common custom headers include x-region for geo-routing, x-api-version for versioning, or x-custom-key for third-party API keys that the Edge Function needs.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78// Send custom headers along with the automatic ones9const { data, error } = await supabase.functions.invoke('process-data', {10 body: { items: [1, 2, 3] },11 headers: {12 'x-region': 'us-east-1',13 'x-api-version': '2024-01',14 'x-request-id': crypto.randomUUID()15 }16})Expected result: The Edge Function receives the request with both the automatic Supabase headers and your custom headers merged together.
Read headers inside the Edge Function
Read headers inside the Edge Function
Inside the Deno Edge Function, access request headers via the req.headers object. Use req.headers.get('header-name') to read a specific header. Header names are case-insensitive. The authorization header contains the user's JWT, which you can use to create an authenticated Supabase client inside the function.
1// supabase/functions/process-data/index.ts2import { corsHeaders } from '../_shared/cors.ts'34Deno.serve(async (req) => {5 if (req.method === 'OPTIONS') {6 return new Response('ok', { headers: corsHeaders })7 }89 // Read automatic headers10 const authHeader = req.headers.get('authorization') // Bearer <jwt>11 const apiKey = req.headers.get('apikey')1213 // Read custom headers14 const region = req.headers.get('x-region')15 const apiVersion = req.headers.get('x-api-version')16 const requestId = req.headers.get('x-request-id')1718 console.log('Region:', region)19 console.log('API Version:', apiVersion)20 console.log('Request ID:', requestId)2122 // Use auth header to identify the user23 if (!authHeader) {24 return new Response(25 JSON.stringify({ error: 'Missing authorization header' }),26 { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 401 }27 )28 }2930 const body = await req.json()3132 return new Response(33 JSON.stringify({ received: body, region, requestId }),34 { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }35 )36})Expected result: The Edge Function reads all headers from the request and can use them for routing, versioning, or authentication logic.
Forward the auth token to create an authenticated Supabase client
Forward the auth token to create an authenticated Supabase client
The most common use of headers in Edge Functions is forwarding the user's JWT to create a Supabase client that operates in the user's auth context. This means the client inside the Edge Function respects the same RLS policies as if the user were querying directly. Pass the authorization header from the request to the global headers option of createClient.
1import { createClient } from 'npm:@supabase/supabase-js@2'2import { corsHeaders } from '../_shared/cors.ts'34Deno.serve(async (req) => {5 if (req.method === 'OPTIONS') {6 return new Response('ok', { headers: corsHeaders })7 }89 // Create a Supabase client with the user's auth context10 const supabase = createClient(11 Deno.env.get('SUPABASE_URL')!,12 Deno.env.get('SUPABASE_ANON_KEY')!,13 {14 global: {15 headers: {16 Authorization: req.headers.get('Authorization')!17 }18 }19 }20 )2122 // This query respects the user's RLS policies23 const { data: { user } } = await supabase.auth.getUser()24 const { data, error } = await supabase25 .from('todos')26 .select('*')27 .eq('user_id', user?.id)2829 if (error) {30 return new Response(31 JSON.stringify({ error: error.message }),32 { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400 }33 )34 }3536 return new Response(37 JSON.stringify(data),38 { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }39 )40})Expected result: The Edge Function queries the database with the user's identity, returning only rows that the user's RLS policies allow.
Add custom headers to the CORS allowlist
Add custom headers to the CORS allowlist
Browsers block custom headers in cross-origin requests unless the server explicitly allows them in the Access-Control-Allow-Headers response. Update your _shared/cors.ts file to include any custom header names you send from the frontend. If you forget this step, the browser's preflight check fails and the request is never sent.
1// supabase/functions/_shared/cors.ts2export const corsHeaders = {3 'Access-Control-Allow-Origin': '*',4 'Access-Control-Allow-Headers':5 'authorization, x-client-info, apikey, content-type, ' +6 'x-region, x-api-version, x-request-id',7 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',8}Expected result: The browser's preflight check passes for all listed headers, and the actual request includes your custom headers.
Send headers with a direct fetch request (alternative approach)
Send headers with a direct fetch request (alternative approach)
If you are not using the Supabase JS client, you can call Edge Functions directly with fetch. This gives you full control over all headers. Include the apikey, Authorization, Content-Type, and any custom headers manually. This approach is useful for non-JavaScript environments, testing with curl, or when you need maximum control.
1// Direct fetch call to an Edge Function2const response = await fetch(3 `${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/process-data`,4 {5 method: 'POST',6 headers: {7 'apikey': process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,8 'Authorization': `Bearer ${session.access_token}`,9 'Content-Type': 'application/json',10 'x-region': 'eu-west-1',11 'x-request-id': crypto.randomUUID()12 },13 body: JSON.stringify({ items: [1, 2, 3] })14 }15)1617const data = await response.json()1819// curl equivalent for testing:20// curl -X POST 'https://project.supabase.co/functions/v1/process-data' \21// -H 'apikey: your-anon-key' \22// -H 'Authorization: Bearer your-anon-key' \23// -H 'Content-Type: application/json' \24// -H 'x-region: eu-west-1' \25// -d '{"items": [1, 2, 3]}'Expected result: The Edge Function receives all headers from the direct fetch request, including the custom ones, and returns the processed result.
Complete working example
1// supabase/functions/_shared/cors.ts2export const corsHeaders = {3 'Access-Control-Allow-Origin': '*',4 'Access-Control-Allow-Headers':5 'authorization, x-client-info, apikey, content-type, x-region, x-api-version, x-request-id',6 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',7}89// supabase/functions/process-data/index.ts10import { corsHeaders } from '../_shared/cors.ts'11import { createClient } from 'npm:@supabase/supabase-js@2'1213Deno.serve(async (req) => {14 if (req.method === 'OPTIONS') {15 return new Response('ok', { headers: corsHeaders })16 }1718 try {19 // Read custom headers20 const region = req.headers.get('x-region') ?? 'default'21 const apiVersion = req.headers.get('x-api-version') ?? '2024-01'22 const requestId = req.headers.get('x-request-id') ?? 'unknown'2324 // Create authenticated Supabase client25 const supabase = createClient(26 Deno.env.get('SUPABASE_URL')!,27 Deno.env.get('SUPABASE_ANON_KEY')!,28 {29 global: {30 headers: { Authorization: req.headers.get('Authorization')! },31 },32 }33 )3435 // Verify user36 const { data: { user }, error: authError } = await supabase.auth.getUser()37 if (authError || !user) {38 return new Response(39 JSON.stringify({ error: 'Unauthorized' }),40 { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 401 }41 )42 }4344 // Process the request45 const body = await req.json()46 const result = {47 user_id: user.id,48 region,49 api_version: apiVersion,50 request_id: requestId,51 processed_items: body.items?.length ?? 0,52 }5354 return new Response(55 JSON.stringify(result),56 { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }57 )58 } catch (error) {59 return new Response(60 JSON.stringify({ error: 'Internal server error' }),61 { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 500 }62 )63 }64})Common mistakes when passing Headers to Edge Functions in Supabase
Why it's a problem: Adding custom headers in the invoke call but not listing them in Access-Control-Allow-Headers, causing the browser to block the request
How to avoid: Every custom header name must appear in your CORS Access-Control-Allow-Headers list. Update _shared/cors.ts whenever you add a new custom header.
Why it's a problem: Overriding the authorization header with a custom value, disconnecting the user's auth context
How to avoid: Do not set authorization in the custom headers object. The Supabase JS client sets it automatically from the logged-in user's session.
Why it's a problem: Reading headers with the wrong case, getting null instead of the value
How to avoid: HTTP headers are case-insensitive. Use req.headers.get('x-region') or req.headers.get('X-Region') — both work. However, be consistent in your code.
Best practices
- Use the x- prefix for all custom header names to distinguish them from standard HTTP headers
- Always add custom header names to the Access-Control-Allow-Headers CORS configuration
- Forward the Authorization header to createClient to maintain the user's RLS context inside Edge Functions
- Use getUser() inside Edge Functions to verify the JWT before processing the request
- Include CORS headers in all response paths, including error responses
- Log headers during development with console.log to verify they arrive correctly
- Use supabase.functions.invoke for browser calls and direct fetch for server-to-server calls
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to pass custom headers (x-region, x-api-version) to a Supabase Edge Function from my frontend and also forward the user's auth token. Show me the client-side invoke call, the Deno function that reads these headers, and the CORS configuration needed.
Create a Supabase Edge Function that reads custom headers (x-region, x-api-version, x-request-id) from the request, forwards the Authorization header to create an authenticated Supabase client, and includes all custom headers in the CORS allowlist. Show both the Deno function and the frontend invoke call.
Frequently asked questions
What headers does supabase.functions.invoke send automatically?
The JS client automatically sends authorization (with the user's JWT or anon key), apikey (the project's anon key), x-client-info (client version), and content-type (application/json for JSON bodies).
Can I override the automatic headers sent by the Supabase JS client?
You can add custom headers, but avoid overriding authorization, apikey, or content-type. Overriding authorization disconnects the user's auth context, and overriding apikey can cause authentication failures.
How do I read the user's JWT from the Authorization header?
The Authorization header contains 'Bearer <jwt>'. Extract the token with: const token = req.headers.get('Authorization')?.replace('Bearer ', ''). Then pass the full header to createClient for authenticated queries.
Do I need to add custom headers to CORS if my Edge Function is only called server-to-server?
No. CORS is enforced only by browsers. Server-to-server calls (from another Edge Function, a backend service, or curl) do not need CORS configuration.
Can I send binary data or file uploads via Edge Function headers?
Headers are for metadata, not data. For file uploads, send the file in the request body using FormData and read it with req.formData() inside the function. Set the content-type to multipart/form-data.
Can RapidDev help design Edge Function APIs with custom header handling?
Yes. RapidDev can architect your Edge Function API layer including custom header schemas, authentication flows, CORS configuration, and request validation.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation