Your Edge Function works locally – but from the browser or via fetch you only get:
{"code":401,"message":"Invalid JWT"}
Or: Missing authorization header. This guide walks through fixing the 401 step by step – including when you should intentionally disable JWT verification.
The Problem: Why Edge Functions Return 401
Supabase Edge Functions can run a JWT check before your code. If that check fails, your function never runs – you only see a platform 401.
Typical triggers:
- No
Authorization: Bearer …header - Anon key instead of the user access token (or the other way around)
- Expired session / invalid JWT
- Function expects auth, but the caller is a webhook/cron without a token
- New asymmetric keys vs. legacy JWT verification
The Solution: Send an Auth Header or Disable Verify
Step 1: Call with a Valid Session
const { data: { session } } = await supabase.auth.getSession()
const res = await fetch(
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/my-function`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session?.access_token}`,
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
},
body: JSON.stringify({ hello: 'world' }),
}
)
Without a valid session, access_token is empty – then 401 is expected.
Step 2: Use the Functions Client
const { data, error } = await supabase.functions.invoke('my-function', {
body: { hello: 'world' },
})
// The JS client sets Authorization automatically from the session
Step 3: Disable JWT Verify for Public Endpoints
For webhooks (Stripe, GitHub) or public endpoints you often do not need a user JWT:
# config.toml (local) or Dashboard → Edge Functions → Details
[functions.my-function]
verify_jwt = false
supabase functions deploy my-function --no-verify-jwt
Then enforce auth inside the function (secret header, Stripe signature, …).
Step 4: Validate Inside the Function
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { corsHeaders } from '../_shared/cors.ts'
Deno.serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
const authHeader = req.headers.get('Authorization')
if (!authHeader) {
return new Response(JSON.stringify({ error: 'Missing authorization header' }), {
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
)
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) {
return new Response(JSON.stringify({ error: 'Invalid JWT' }), {
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
return new Response(JSON.stringify({ userId: user.id }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
})
Common Mistakes
- Only apikey, no Bearer: many clients send the anon key but forget
Authorization. - Service role in the browser: never. For server-to-server: disable verify + use a secret, or keep the service role server-side only.
- CORS + 401: without CORS headers on the 401 response the browser often only shows a CORS error. See the CORS guide below.
Troubleshooting Checklist
- ✅ Session present? Log
getSession()/getUser() - ✅
Authorization: Bearer <access_token>set? - ✅
apikeyheader with the anon key? - ✅ For webhooks:
verify_jwt = false+ your own auth? - ✅ CORS headers on 401 responses too?
Conclusion
Invalid JWT / 401 means the platform (or your code) rejected the token. Either send a valid user JWT or deliberately disable verify and handle auth yourself – and do not forget CORS.
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 Edge Functions CORS Error Fix
- Supabase Auth: Invalid login credentials
- Supabase: infinite recursion detected in policy
- Supabase Storage 403 RLS on upload
- ChatGPT Clone with Edge Functions
- Supabase Edge Functions Auth
Still getting 401 with a token? Leave a comment – often the Bearer header is missing or verify is still on.
Comments