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