Supabase Edge Functions: Fix "Invalid JWT" / 401 Error

7. August 2026 approx. 5 min read Supabase
Contents 10

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

  1. ✅ Session present? Log getSession() / getUser()
  2. Authorization: Bearer <access_token> set?
  3. apikey header with the anon key?
  4. ✅ For webhooks: verify_jwt = false + your own auth?
  5. ✅ 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

Still getting 401 with a token? Leave a comment – often the Bearer header is missing or verify is still on.

Comments