# 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)
