Supabase security · 5 minute read
Invalid JWT when testing Supabase Edge Functions locally: the three traps
Your Deno function works when you deploy it, but every local curl comes back {"msg":"Invalid JWT"}. Nine times out of ten the problem is not the key you think it is — it is a missing colon in the header, a cloud key sent to a local verifier, or an env var name that never gets injected. Here is the 30-second triage for each.
Trap 1: the Authorization header has no colon
This one hides in plain sight. Compare:
# broken — curl does not treat this as a header, no credential is sent
curl -X POST "http://127.0.0.1:54321/functions/v1/my-func" \
-H "Authorization Bearer <key>"
# working
curl -X POST "http://127.0.0.1:54321/functions/v1/my-func" \
-H "Authorization: Bearer <key>"Without the colon, -H receives a string curl cannot parse into a header, so no Authorization reaches the gateway — and the gateway answers with exactly the error you are seeing. Before debugging JWT contents, confirm a credential is being sent at all.
Trap 2: cloud keys do not verify against the local secret
supabase functions serve verifies JWTs against the local stack's JWT secret. A service_rolekey copied from the cloud dashboard is signed by your cloud project's secret, so it will never verify locally no matter how correct it looks. Local keys come from the CLI itself:
npx supabase status
# ... prints:
# API URL: http://127.0.0.1:54321
# anon key: eyJ... <- use this for JWT-verified local calls
# service_role key: eyJ... <- local service key, for RLS-bypassing pathsThe same asymmetry in reverse: the local anon key does nothing against your cloud project. Rule of thumb — keys are per-stack. If you move between local and cloud, re-read them from supabase status or the dashboard each time.
Trap 3: custom env var names are never auto-injected
Only the reserved names are injected automatically inside a function: SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_DB_URL. Code that reads something like MY_SUPABASE_SERVICE_ROLE_KEY needs it set as a custom secret, and locally custom env vars must be present in your shell or an env file before functions serve:
npx supabase secrets set MY_SUPABASE_SERVICE_ROLE_KEY=eyJ... # deployed functions
MY_SUPABASE_SERVICE_ROLE_KEY=eyJ... npx supabase functions serve # local runA typo'd or missing name shows up as undefined inside the function — and clients constructed with an undefined key fail in ways that can look like auth errors. Log the first characters of the key at the top of the function while debugging; never log the whole key.
When you actually want no JWT verification
For cron jobs and webhook receivers with no user context, skip verification instead of fighting it — either per function:
# supabase/config.toml
[functions.stock-news-sync]
verify_jwt = falseor for a local session: npx supabase functions serve --no-verify-jwt.
Treat that switch carefully: with verify_jwt = false the function itself becomes the access control. If it then reads a user identity from a cookie or header, you own validating it. And remember the client inside your function built with the service key bypasses RLS by design — every policy on the tables it writes to is skipped, which is convenient for ingestion and dangerous for anything user-scoped.
The 30-second triage script
- Fix the header colon, call locally with the anon key from
supabase status. Works? The cloud key was the problem all along. - Still
Invalid JWT? Confirmfunctions serveis running on the port you are curling (it prints the exact URL and port). - Different error now (401, 500, undefined client)? You are past auth — check env names next.
The public RLS leak demo runs a read-only harness that queries as anon and as an authenticated user, so you can see which role a request actually runs as before you touch any policy or key.
Check every policy and role in one pass
Supabase RLS Audit Kit · $29 one-time
Seven commented SQL audits and 60 checks show every table's RLS status, the role each policy actually targets, grants and bypass paths, plus a role-simulation harness that proves isolation — the same harness that tells you whether your service-key client touches data it should not.
Get the kit - $29 ↗Review the files, scope and license →Related reading: Supabase auth on Vercel: the double-login fix and how to test Supabase RLS policies. For a full launch pass, a fixed-price Supabase RLS audit from $99 returns prioritized findings with reproducible evidence.