# 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-personal` and `feed-sojorn` functions returned 401 with `{code: 401, message: "Invalid JWT"}` - `profile` function 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)`: ```typescript // 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: 1. The request was already in flight when refresh happened 2. Cached/old tokens weren't being properly invalidated 3. 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: ```typescript // 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()`: ```typescript // AFTER (fixed): const { data: { user }, error: authError } = await supabase.auth.getUser(); ``` ### Flutter App Changes (api_service.dart) Added proper 401 retry logic: 1. **`_callFunction` now re-throws `FunctionException`** - Allows callers to catch 401 errors 2. **401 retry with session refresh** in `getPersonalFeed`, `getSojornFeed`, and `getProfile` 3. **Concurrent refresh handling** - Multiple simultaneous 401s share a single refresh future via `_refreshInFlight` 4. **Removed artificial delays** - No more unnecessary 500ms/1000ms delays after refresh ## Files Modified 1. `supabase/functions/feed-personal/index.ts` - Removed JWT parameter from `getUser()` 2. `supabase/functions/feed-sojorn/index.ts` - Removed JWT parameter from `getUser()` 3. `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: ```powershell .\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 1. **Don't explicitly pass JWTs to `getUser()`** - Let the SDK handle authentication automatically 2. **The Supabase SDK handles auth internally** - Trust its internal session state 3. **Profile function was the clue** - It worked because it didn't pass the JWT explicitly 4. **Check how similar functions work** - When one function works and another doesn't, compare their implementations ## Prevention When creating new Edge Functions: 1. Always use `supabase.auth.getUser()` without passing the JWT parameter 2. Trust the Supabase SDK's internal session handling 3. If you need the user's ID, get it from `user.id` after calling `getUser()` without parameters 4. Don't extract and pass JWTs from request headers manually ## References - [Supabase Edge Functions Auth](https://supabase.com/docs/guides/functions/auth) - [supabase-js SDK v2](https://supabase.com/docs/reference/javascript/v2) - [ES256 JWT Support](https://supabase.com/docs/guides/functions/supported-jwt-algorithms)