Edge Function returns 401
Fix 401 Unauthorized from a SupaHost Edge Function: UNAUTHORIZED_NO_AUTH_HEADER and UNAUTHORIZED_INVALID_JWT, the Require JWT toggle, webhooks and calls from cron or pg_net.
What you see
Calling a function returns 401 with a JSON body and an sb-error-code response header set to one of two codes:
HTTP/2 401
sb-error-code: UNAUTHORIZED_NO_AUTH_HEADER
HTTP/2 401
sb-error-code: UNAUTHORIZED_INVALID_JWT
In supabase-js, supabase.functions.invoke() returns a FunctionsHttpError with context.status 401. In the dashboard's Logs card the request line shows status 401 and your function's own logs are missing, because your code never ran.
Why it happens
When Require JWT is on for a function (the default), SupaHost's function router checks the Authorization: Bearer <token> header before your code runs:
UNAUTHORIZED_NO_AUTH_HEADER: there's noAuthorizationheader. Typical with plainfetch, curl, webhooks from Stripe or GitHub, and cron jobs that call the function over HTTP.UNAUTHORIZED_INVALID_JWT: the token isn't a valid JWT for this project. It's signed with another project's secret (each project has its own keys), it has expired (exp), it isn't valid yet (nbf), or it's not a JWT at all.
If the 401 has no sb-error-code header, it came from your own code, for example a getUser() check that failed.
How to fix it
Call through supabase-js.
invokesends the signed-in user's access token, or the anon key when nobody is signed in:const { data, error } = await supabase.functions.invoke('send-invoice', { body: { invoiceId: 1042 }, })Add the header for manual calls:
curl -X POST 'https://app-one.acme.supahost.space/functions/v1/send-invoice' \ -H "Authorization: Bearer $SUPABASE_ANON_KEY" \ -H 'Content-Type: application/json' \ -d '{"invoiceId":1042}'Turn off Require JWT for webhooks. Services like Stripe can't send your JWT. In Dashboard → your project → Functions tab, switch off the function's Require JWT toggle, then verify the provider's signature inside the function instead. See Example: Stripe webhooks.
Use the right project's key. After copying code from another project, or importing from Supabase Cloud, replace the old keys and have users sign in again so their tokens are issued by this project.
Refresh expired user tokens. A session token that sat in local storage past its lifetime fails. supabase-js refreshes it automatically if the client is alive; for server code, get a fresh session before calling.
For pg_net or cron calls, include the header. Store the key in Vault and read it in the job, rather than pasting it into SQL. See Running functions on a schedule.
On SupaHost
The router accepts any valid JWT for the project, including the public anon key. So Require JWT stops random internet traffic, but it doesn't prove who the caller is. If a function acts on a user's data, check the user inside the function:
import { createClient } from 'npm:@supabase/supabase-js@2'
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
)
Deno.serve(async (req) => {
const token = (req.headers.get('Authorization') ?? '').replace('Bearer ', '')
const { data: { user } } = await supabase.auth.getUser(token)
if (!user) return Response.json({ error: 'Sign in first' }, { status: 401 })
// ...act on behalf of user.id
return Response.json({ ok: true })
})
OPTIONS preflight requests skip the JWT check, so CORS preflights don't fail with 401. If you send only an apikey header without Authorization, SupaHost's gateway copies it into Authorization: Bearer, so that works too.