Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Pass Headers to Edge Functions in Supabase

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.

What you'll learn

  • How to send custom headers with supabase.functions.invoke()
  • How to read request headers inside a Deno Edge Function
  • How to forward the user's auth context to the Edge Function
  • How to add custom headers to the CORS allowlist
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced8 min read10-15 minSupabase (all plans), Deno runtime, @supabase/supabase-js v2+March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8// Send custom headers along with the automatic ones
9const { 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.

2

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.

typescript
1// supabase/functions/process-data/index.ts
2import { corsHeaders } from '../_shared/cors.ts'
3
4Deno.serve(async (req) => {
5 if (req.method === 'OPTIONS') {
6 return new Response('ok', { headers: corsHeaders })
7 }
8
9 // Read automatic headers
10 const authHeader = req.headers.get('authorization') // Bearer <jwt>
11 const apiKey = req.headers.get('apikey')
12
13 // Read custom headers
14 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')
17
18 console.log('Region:', region)
19 console.log('API Version:', apiVersion)
20 console.log('Request ID:', requestId)
21
22 // Use auth header to identify the user
23 if (!authHeader) {
24 return new Response(
25 JSON.stringify({ error: 'Missing authorization header' }),
26 { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 401 }
27 )
28 }
29
30 const body = await req.json()
31
32 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.

3

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.

typescript
1import { createClient } from 'npm:@supabase/supabase-js@2'
2import { corsHeaders } from '../_shared/cors.ts'
3
4Deno.serve(async (req) => {
5 if (req.method === 'OPTIONS') {
6 return new Response('ok', { headers: corsHeaders })
7 }
8
9 // Create a Supabase client with the user's auth context
10 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 )
21
22 // This query respects the user's RLS policies
23 const { data: { user } } = await supabase.auth.getUser()
24 const { data, error } = await supabase
25 .from('todos')
26 .select('*')
27 .eq('user_id', user?.id)
28
29 if (error) {
30 return new Response(
31 JSON.stringify({ error: error.message }),
32 { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400 }
33 )
34 }
35
36 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.

4

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.

typescript
1// supabase/functions/_shared/cors.ts
2export 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.

5

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.

typescript
1// Direct fetch call to an Edge Function
2const 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)
16
17const data = await response.json()
18
19// 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

supabase/functions/process-data/index.ts
1// supabase/functions/_shared/cors.ts
2export 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}
8
9// supabase/functions/process-data/index.ts
10import { corsHeaders } from '../_shared/cors.ts'
11import { createClient } from 'npm:@supabase/supabase-js@2'
12
13Deno.serve(async (req) => {
14 if (req.method === 'OPTIONS') {
15 return new Response('ok', { headers: corsHeaders })
16 }
17
18 try {
19 // Read custom headers
20 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'
23
24 // Create authenticated Supabase client
25 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 )
34
35 // Verify user
36 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 }
43
44 // Process the request
45 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 }
53
54 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.

ChatGPT Prompt

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.

Supabase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.