| 
 | 1 | +# Token Refresh Debug App  | 
 | 2 | + | 
 | 3 | +This is a diagnostic application designed to help reproduce and debug the intermittent token refresh issue reported in [Issue #1158](https://github.com/supabase/supabase-flutter/issues/1158).  | 
 | 4 | + | 
 | 5 | +## Problem Description  | 
 | 6 | + | 
 | 7 | +Users are experiencing inconsistent token refresh behavior where:  | 
 | 8 | +- Sometimes auto-refresh works correctly  | 
 | 9 | +- Sometimes the SDK emits a `signedOut` event instead of refreshing expired tokens  | 
 | 10 | +- This results in 403 errors and unexpected user logouts  | 
 | 11 | +- The issue is intermittent and difficult to reproduce  | 
 | 12 | + | 
 | 13 | +## Purpose  | 
 | 14 | + | 
 | 15 | +This app provides:  | 
 | 16 | +1. **Real-time session monitoring** - View token expiry status, time remaining, and session details  | 
 | 17 | +2. **Comprehensive logging** - All instrumentation logs from the SDK are displayed in the console  | 
 | 18 | +3. **App lifecycle tracking** - Monitor app state changes (paused/resumed)  | 
 | 19 | +4. **Manual testing tools** - Trigger API calls and manual token refreshes  | 
 | 20 | +5. **Reproduction environment** - Controlled conditions to reproduce the issue  | 
 | 21 | + | 
 | 22 | +## Setup  | 
 | 23 | + | 
 | 24 | +### Prerequisites  | 
 | 25 | + | 
 | 26 | +1. A Supabase project with authentication enabled  | 
 | 27 | +2. Configure your project with a short token expiry for easier testing:  | 
 | 28 | +   - Go to your Supabase Dashboard  | 
 | 29 | +   - Navigate to Authentication > Settings  | 
 | 30 | +   - Set "JWT expiry limit" to a short duration (e.g., 300 seconds = 5 minutes)  | 
 | 31 | +   - This allows you to reproduce the issue faster without waiting hours  | 
 | 32 | + | 
 | 33 | +3. Create a test user in your project  | 
 | 34 | +4. Optional: Create a `test_table` for API testing (not required)  | 
 | 35 | + | 
 | 36 | +### Installation  | 
 | 37 | + | 
 | 38 | +```bash  | 
 | 39 | +cd examples/token_refresh_debug_app  | 
 | 40 | +flutter pub get  | 
 | 41 | +flutter run  | 
 | 42 | +```  | 
 | 43 | + | 
 | 44 | +## Usage  | 
 | 45 | + | 
 | 46 | +### Step 1: Configure Supabase  | 
 | 47 | + | 
 | 48 | +1. Launch the app  | 
 | 49 | +2. Enter your Supabase URL (e.g., `https://your-project.supabase.co`)  | 
 | 50 | +3. Enter your Supabase Anon Key  | 
 | 51 | +4. Click "Initialize"  | 
 | 52 | + | 
 | 53 | +### Step 2: Sign In  | 
 | 54 | + | 
 | 55 | +1. Enter your test user email  | 
 | 56 | +2. Enter password  | 
 | 57 | +3. Click "Sign In"  | 
 | 58 | + | 
 | 59 | +### Step 3: Monitor Session  | 
 | 60 | + | 
 | 61 | +Once signed in, you'll see the Debug Dashboard with:  | 
 | 62 | + | 
 | 63 | +- **Session Status Card** (Green/Red)  | 
 | 64 | +  - Current session state (Active or EXPIRED)  | 
 | 65 | +  - User ID and email  | 
 | 66 | +  - Token expiry time  | 
 | 67 | +  - Time remaining until expiry  | 
 | 68 | +  - Access token preview  | 
 | 69 | +  - Refresh token availability  | 
 | 70 | + | 
 | 71 | +- **Controls**  | 
 | 72 | +  - Test API Call - Makes a query to test if token is valid  | 
 | 73 | +  - Manual Token Refresh - Manually triggers a refresh  | 
 | 74 | +  - Sign Out - Logs out the user  | 
 | 75 | +  - App State indicator  | 
 | 76 | + | 
 | 77 | +- **Event Log**  | 
 | 78 | +  - Shows auth state changes (signedIn, tokenRefreshed, signedOut)  | 
 | 79 | +  - Shows app lifecycle changes (resumed, paused, inactive)  | 
 | 80 | +  - Timestamped for correlation with console logs  | 
 | 81 | + | 
 | 82 | +### Step 4: Reproduce the Issue  | 
 | 83 | + | 
 | 84 | +#### Method 1: App Pause/Resume with Expired Token  | 
 | 85 | + | 
 | 86 | +1. **Note the expiry time** - Check "Time Until Expiry"  | 
 | 87 | +2. **Minimize the app** - Use your device/simulator to background the app  | 
 | 88 | +3. **Wait for token to expire** - Wait longer than the expiry time  | 
 | 89 | +4. **Resume the app** - Bring the app back to foreground  | 
 | 90 | +5. **Observe behavior**:  | 
 | 91 | +   - ✅ **Expected**: Session status stays green, "Time Until Expiry" resets (token was refreshed)  | 
 | 92 | +   - ❌ **Bug**: Session disappears or shows "No active session" (signedOut event was emitted)  | 
 | 93 | + | 
 | 94 | +#### Method 2: Network Interruption  | 
 | 95 | + | 
 | 96 | +1. **Enable airplane mode** while token is about to expire  | 
 | 97 | +2. **Wait for auto-refresh to trigger**  | 
 | 98 | +3. **Re-enable network**  | 
 | 99 | +4. **Observe** if session is preserved or user is signed out  | 
 | 100 | + | 
 | 101 | +#### Method 3: Rapid Lifecycle Changes  | 
 | 102 | + | 
 | 103 | +1. **Rapidly pause and resume** the app multiple times  | 
 | 104 | +2. **Check** if session remains stable  | 
 | 105 | +3. **Look for** race conditions in the logs  | 
 | 106 | + | 
 | 107 | +### Step 5: Analyze Logs  | 
 | 108 | + | 
 | 109 | +The app outputs comprehensive logs to the console showing:  | 
 | 110 | + | 
 | 111 | +```  | 
 | 112 | +INFO: 14:23:45.123: supabase.supabase_flutter: App lifecycle state changed to: resumed  | 
 | 113 | +FINE: 14:23:45.124: supabase.auth: Starting auto refresh with session state: expiresAt=2024-01-20T14:28:45.000Z, isExpired=false, hasRefreshToken=true  | 
 | 114 | +FINE: 14:23:45.125: supabase.auth: Auto-refresh tick: expires in 58 ticks (583s), threshold=3  | 
 | 115 | +INFO: 14:23:45.126: supabase.supabase_flutter: Starting session recovery from local storage  | 
 | 116 | +```  | 
 | 117 | + | 
 | 118 | +Key things to look for:  | 
 | 119 | +- **Session recovery timing** - Does it complete before auto-refresh starts?  | 
 | 120 | +- **Auto-refresh tick calculations** - Are expiry times calculated correctly?  | 
 | 121 | +- **Error messages** - What type of errors occur during refresh?  | 
 | 122 | +- **SignedOut events** - When do they occur and why?  | 
 | 123 | +- **App lifecycle timing** - Do rapid state changes cause issues?  | 
 | 124 | + | 
 | 125 | +## Expected Log Flow (Successful Refresh)  | 
 | 126 | + | 
 | 127 | +```  | 
 | 128 | +1. App resumed  | 
 | 129 | +2. Auto-refresh timer starts  | 
 | 130 | +3. Session recovery starts  | 
 | 131 | +4. Session recovery completes  | 
 | 132 | +5. Auto-refresh tick checks expiry  | 
 | 133 | +6. Token refresh triggered (when threshold reached)  | 
 | 134 | +7. Token refresh successful  | 
 | 135 | +8. tokenRefreshed event emitted  | 
 | 136 | +9. Session persisted to storage  | 
 | 137 | +```  | 
 | 138 | + | 
 | 139 | +## Problematic Log Flow (Issue Reproduces)  | 
 | 140 | + | 
 | 141 | +```  | 
 | 142 | +1. App resumed  | 
 | 143 | +2. Auto-refresh timer starts  | 
 | 144 | +3. Auto-refresh tick runs immediately  | 
 | 145 | +4. Session recovery still in progress (race condition)  | 
 | 146 | +5. Token refresh fails (various reasons)  | 
 | 147 | +6. signedOut event emitted  | 
 | 148 | +7. Session cleared  | 
 | 149 | +8. User unexpectedly logged out  | 
 | 150 | +```  | 
 | 151 | + | 
 | 152 | +## Key Scenarios to Test  | 
 | 153 | + | 
 | 154 | +### Scenario 1: Clean Resume After Expiry  | 
 | 155 | +- Start: Valid session  | 
 | 156 | +- Action: Pause app for >expiry duration  | 
 | 157 | +- Resume: Should auto-refresh  | 
 | 158 | +- Check: Session stays active  | 
 | 159 | + | 
 | 160 | +### Scenario 2: Network Error During Refresh  | 
 | 161 | +- Start: Token about to expire  | 
 | 162 | +- Action: Enable airplane mode  | 
 | 163 | +- Wait: Trigger auto-refresh attempt  | 
 | 164 | +- Resume: Re-enable network  | 
 | 165 | +- Check: Session preserved, retries refresh  | 
 | 166 | + | 
 | 167 | +### Scenario 3: Concurrent Refresh Attempts  | 
 | 168 | +- Start: Token about to expire  | 
 | 169 | +- Action: Rapidly open/close app  | 
 | 170 | +- Check: Only one refresh call made  | 
 | 171 | +- Check: No race conditions  | 
 | 172 | + | 
 | 173 | +### Scenario 4: Custom Storage Implementation  | 
 | 174 | +- Configure: Use FlutterSecureStorage instead of SharedPreferences  | 
 | 175 | +- Run: All above scenarios  | 
 | 176 | +- Check: Same behavior as default storage  | 
 | 177 | + | 
 | 178 | +## Troubleshooting  | 
 | 179 | + | 
 | 180 | +### Issue: "No active session" shows immediately after resume  | 
 | 181 | +- This indicates the session was not properly restored from storage  | 
 | 182 | +- Check logs for storage read errors  | 
 | 183 | +- Verify permissions for SharedPreferences/FlutterSecureStorage  | 
 | 184 | + | 
 | 185 | +### Issue: Token refresh fails with 401  | 
 | 186 | +- Check if refresh token is still valid  | 
 | 187 | +- Verify Supabase project settings allow token refresh  | 
 | 188 | +- Check if user was deleted/disabled  | 
 | 189 | + | 
 | 190 | +### Issue: Token refresh fails with network error  | 
 | 191 | +- Verify internet connectivity  | 
 | 192 | +- Check Supabase project status  | 
 | 193 | +- Look for retryable vs non-retryable errors in logs  | 
 | 194 | + | 
 | 195 | +### Issue: App doesn't respond to lifecycle changes  | 
 | 196 | +- Verify WidgetsBindingObserver is properly registered  | 
 | 197 | +- Check if auto-refresh is enabled in configuration  | 
 | 198 | +- Look for timer start/stop logs  | 
 | 199 | + | 
 | 200 | +## Configuration Options  | 
 | 201 | + | 
 | 202 | +You can modify the app to test different scenarios:  | 
 | 203 | + | 
 | 204 | +### Change Log Level  | 
 | 205 | +In `main.dart`, adjust logging level:  | 
 | 206 | +```dart  | 
 | 207 | +Logger.root.level = Level.ALL;     // Most verbose  | 
 | 208 | +Logger.root.level = Level.FINE;    // Debug info  | 
 | 209 | +Logger.root.level = Level.INFO;    // Important events only  | 
 | 210 | +```  | 
 | 211 | + | 
 | 212 | +### Test with Different Storage  | 
 | 213 | +Swap SharedPreferences for FlutterSecureStorage:  | 
 | 214 | +```dart  | 
 | 215 | +await Supabase.initialize(  | 
 | 216 | +  url: url,  | 
 | 217 | +  anonKey: anonKey,  | 
 | 218 | +  authOptions: FlutterAuthClientOptions(  | 
 | 219 | +    localStorage: MyCustomSecureStorage(), // Your implementation  | 
 | 220 | +  ),  | 
 | 221 | +);  | 
 | 222 | +```  | 
 | 223 | + | 
 | 224 | +### Adjust Auto-Refresh Timing  | 
 | 225 | +The timing constants are in `packages/gotrue/lib/src/constants.dart`:  | 
 | 226 | +- `autoRefreshTickDuration` - How often to check (default: 10 seconds)  | 
 | 227 | +- `autoRefreshTickThreshold` - When to refresh (default: 3 ticks before expiry)  | 
 | 228 | +- `expiryMargin` - Safety buffer (default: 30 seconds)  | 
 | 229 | + | 
 | 230 | +## Contributing Findings  | 
 | 231 | + | 
 | 232 | +When reporting results:  | 
 | 233 | + | 
 | 234 | +1. **Include console logs** - Copy relevant log sections  | 
 | 235 | +2. **Describe the scenario** - Which test case you ran  | 
 | 236 | +3. **Note timing** - How long after pause did you resume?  | 
 | 237 | +4. **Environment details** - iOS/Android version, Flutter version  | 
 | 238 | +5. **Storage type** - Default or custom implementation  | 
 | 239 | +6. **Consistency** - How often does it reproduce (1/10, 5/10, always)?  | 
 | 240 | + | 
 | 241 | +## Technical Details  | 
 | 242 | + | 
 | 243 | +### Instrumentation Added  | 
 | 244 | + | 
 | 245 | +This app uses the instrumentation added to the SDK:  | 
 | 246 | +- **gotrue_client.dart** - Token refresh lifecycle  | 
 | 247 | +- **supabase_auth.dart** - App lifecycle and recovery  | 
 | 248 | +- **session.dart** - Expiry calculations  | 
 | 249 | +- **local_storage.dart** - Storage operations  | 
 | 250 | +- **supabase.dart** - Initialization flow  | 
 | 251 | + | 
 | 252 | +### Dependencies  | 
 | 253 | + | 
 | 254 | +- `supabase_flutter` - Local path to repository version with instrumentation  | 
 | 255 | +- `logging` - For structured log output  | 
 | 256 | +- `intl` - For timestamp formatting  | 
 | 257 | + | 
 | 258 | +## Next Steps  | 
 | 259 | + | 
 | 260 | +After reproducing the issue with this app:  | 
 | 261 | + | 
 | 262 | +1. Share logs with the Supabase team  | 
 | 263 | +2. Help identify the specific timing conditions that trigger the bug  | 
 | 264 | +3. Test proposed fixes  | 
 | 265 | +4. Verify the fix resolves the issue in your production app  | 
 | 266 | + | 
 | 267 | +## Related Issues  | 
 | 268 | + | 
 | 269 | +- [Issue #1158](https://github.com/supabase/supabase-flutter/issues/1158) - Original bug report  | 
0 commit comments