sojorn/sojorn_docs/troubleshooting/JWT_401_FIX_2026-01-11.md
Patrick Britton 38653f5854 Sojorn Backend Finalization & Cleanup - Complete Migration from Supabase
##  Phase 1: Critical Feature Completion (Beacon Voting)
- Add VouchBeacon, ReportBeacon, RemoveBeaconVote methods to PostRepository
- Implement beacon voting HTTP handlers with confidence score calculations
- Register new beacon routes: /beacons/:id/vouch, /beacons/:id/report, /beacons/:id/vouch (DELETE)
- Auto-flag beacons at 5+ reports, confidence scoring (0.5 base + 0.1 per vouch)

##  Phase 2: Feed Logic & Post Distribution Integrity
- Verify unified feed logic supports all content types (Standard, Quips, Beacons)
- Ensure proper distribution: Profile Feed + Main/Home Feed for followers
- Beacon Map integration for location-based content
- Video content filtering for Quips feed

##  Phase 3: The Notification System
- Create comprehensive NotificationService with FCM integration
- Add CreateNotification method to NotificationRepository
- Implement smart deep linking: beacon_map, quip_feed, main_feed
- Trigger notifications for beacon interactions and cross-post comments
- Push notification logic with proper content type detection

##  Phase 4: The Great Supabase Purge
- Delete function_proxy.go and remove /functions/:name route
- Remove SupabaseURL, SupabaseKey from config.go
- Remove SupabaseID field from User model
- Clean all Supabase imports and dependencies
- Sanitize codebase of legacy Supabase references

##  Phase 5: Flutter Frontend Integration
- Implement vouchBeacon(), reportBeacon(), removeBeaconVote() in ApiService
- Replace TODO delay in video_comments_sheet.dart with actual publishComment call
- Fix compilation errors (named parameters, orphaned child properties)
- Complete frontend integration with Go API endpoints

##  Additional Improvements
- Fix compilation errors in threaded_comment_widget.dart (orphaned child property)
- Update video_comments_sheet.dart to use proper named parameters
- Comprehensive error handling and validation
- Production-ready notification system with deep linking

##  Migration Status: 100% Complete
- Backend: Fully migrated from Supabase to custom Go/Gin API
- Frontend: Integrated with new Go endpoints
- Notifications: Complete FCM integration with smart routing
- Database: Clean of all Supabase dependencies
- Features: All functionality preserved and enhanced

Ready for VPS deployment and production testing!
2026-01-30 09:24:31 -06:00

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

// 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:

// 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:

  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:

.\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