3.8 KiB
JWT 401 "Invalid JWT" Fix
Date: January 11, 2026
Issue: Edge Functions returning 401 "Invalid JWT" errors for feed endpoints
Status: Fixed
Problem
The feed-personal and feed-sojorn Edge Functions were returning 401 "Invalid JWT" errors, causing feeds to fail to load despite the user having a valid session.
Symptoms
feed-personalandfeed-sojornfunctions returned 401 with{code: 401, message: "Invalid JWT"}profilefunction worked fine (no 401 errors)- Session refresh didn't resolve the issue
- Multiple concurrent requests caused repeated refresh attempts
Root Cause
The feed functions were explicitly passing the JWT to supabase.auth.getUser(jwt):
// BEFORE (broken):
const { data: { user }, error: authError } = await supabase.auth.getUser(jwt);
The JWT was extracted from the request's Authorization header. Even after the Flutter client refreshed its session and obtained a new token, subsequent API calls would still send the old/stale JWT in the header because:
- The request was already in flight when refresh happened
- Cached/old tokens weren't being properly invalidated
- The Edge Function validated the stale JWT and rejected it
Meanwhile, the profile function worked because it called supabase.auth.getUser() without passing the JWT explicitly:
// AFTER (fixed):
const { data: { user }, error: authError } = await supabase.auth.getUser();
This lets the Supabase SDK use its internal session state (which gets updated after refresh) rather than trusting the potentially stale header token.
Solution
Edge Functions Changes
Changed both feed-personal and feed-sojorn to NOT pass the JWT to getUser():
// AFTER (fixed):
const { data: { user }, error: authError } = await supabase.auth.getUser();
Flutter App Changes (api_service.dart)
Added proper 401 retry logic:
_callFunctionnow re-throwsFunctionException- Allows callers to catch 401 errors- 401 retry with session refresh in
getPersonalFeed,getSojornFeed, andgetProfile - Concurrent refresh handling - Multiple simultaneous 401s share a single refresh future via
_refreshInFlight - Removed artificial delays - No more unnecessary 500ms/1000ms delays after refresh
Files Modified
supabase/functions/feed-personal/index.ts- Removed JWT parameter fromgetUser()supabase/functions/feed-sojorn/index.ts- Removed JWT parameter fromgetUser()sojorn_app/lib/services/api_service.dart- Added 401 retry logic with session refresh
How to Deploy
Run the deployment script from the project root:
.\deploy_all_functions.ps1
This uses --no-verify-jwt flag which is required for the supabase-js v2 SDK that supports ES256 JWTs.
Lessons Learned
- Don't explicitly pass JWTs to
getUser()- Let the SDK handle authentication automatically - The Supabase SDK handles auth internally - Trust its internal session state
- Profile function was the clue - It worked because it didn't pass the JWT explicitly
- Check how similar functions work - When one function works and another doesn't, compare their implementations
Prevention
When creating new Edge Functions:
- Always use
supabase.auth.getUser()without passing the JWT parameter - Trust the Supabase SDK's internal session handling
- If you need the user's ID, get it from
user.idafter callinggetUser()without parameters - Don't extract and pass JWTs from request headers manually