Spaces:
Sleeping
Sleeping
| # Token Refresh & Session Management Guide | |
| ## Overview | |
| This guide explains why users get auto-logged out and how to implement robust token refresh logic in your frontend application. | |
| **TL;DR:** Refresh tokens expire after 30 days. You need proactive refresh logic in your frontend to prevent auto-logout. | |
| --- | |
| ## Table of Contents | |
| 1. [Understanding the Problem](#understanding-the-problem) | |
| 2. [How Supabase Tokens Work](#how-supabase-tokens-work) | |
| 3. [Why Users Get Logged Out](#why-users-get-logged-out) | |
| 4. [The Solution](#the-solution) | |
| 5. [Frontend Implementation](#frontend-implementation) | |
| 6. [Backend Behavior](#backend-behavior) | |
| 7. [Testing & Validation](#testing--validation) | |
| 8. [Troubleshooting](#troubleshooting) | |
| --- | |
| ## Understanding the Problem | |
| ### The Error | |
| ``` | |
| ERROR: invalid JWT: unable to parse or verify signature, token has invalid claims: token is expired | |
| ``` | |
| This error appears when a **refresh token has expired**, not the access token. | |
| ### What Happens | |
| 1. User logs in β Receives access token (1 hour) + refresh token (30 days) | |
| 2. Access token expires after 1 hour | |
| 3. Frontend tries to refresh using refresh token | |
| 4. **If refresh token is also expired (30+ days old)** β 401 Error β Auto-logout | |
| --- | |
| ## How Supabase Tokens Work | |
| ### Token Types | |
| | Token Type | Lifespan | Purpose | Storage | | |
| |------------|----------|---------|---------| | |
| | **Access Token** | 1 hour | Authenticate API requests | Memory/LocalStorage | | |
| | **Refresh Token** | 30 days | Get new access tokens | LocalStorage (HttpOnly cookies recommended) | | |
| ### Token Rotation | |
| Supabase uses **rotating refresh tokens** for security: | |
| - Each time you refresh, you get a **new refresh token** | |
| - The **old refresh token becomes invalid** | |
| - This prevents replay attacks | |
| - If rotation fails (network issues, race conditions), tokens can become invalid | |
| ### Storage | |
| After successful login, you receive: | |
| ```json | |
| { | |
| "access_token": "eyJhbG...", | |
| "refresh_token": "v1.MKh...", | |
| "expires_in": 3600, | |
| "token_type": "bearer", | |
| "user": { ... } | |
| } | |
| ``` | |
| Store in localStorage: | |
| ```javascript | |
| localStorage.setItem('access_token', data.access_token); | |
| localStorage.setItem('refresh_token', data.refresh_token); | |
| localStorage.setItem('expires_at', new Date(Date.now() + data.expires_in * 1000).toISOString()); | |
| ``` | |
| --- | |
| ## Why Users Get Logged Out | |
| ### Scenario 1: Inactivity (Most Common) | |
| - User doesn't use app for 30+ days | |
| - Refresh token expires | |
| - Next time they open app β Auto-logout | |
| **Solution:** Display "Your session expired due to inactivity" message | |
| ### Scenario 2: Token Rotation Failure | |
| - Network interruption during refresh | |
| - Race condition (multiple tabs refreshing simultaneously) | |
| - Old refresh token stored, new one discarded | |
| **Solution:** Implement retry logic and synchronize across tabs | |
| ### Scenario 3: No Proactive Refresh | |
| - Frontend waits for 401 error before refreshing | |
| - User makes request with expired access token | |
| - Backend returns 401 | |
| - Frontend tries to refresh, but refresh token also expired | |
| **Solution:** Refresh tokens **before** they expire (5 minutes buffer) | |
| ### Scenario 4: Multi-Tab/Device Issues | |
| - User opens app in multiple tabs/devices | |
| - Each tab/device tries to refresh independently | |
| - Token rotation causes conflicts | |
| - Tokens become invalid across sessions | |
| **Solution:** Use BroadcastChannel API or shared worker for token sync | |
| --- | |
| ## The Solution | |
| ### Architecture Overview | |
| ``` | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β FRONTEND β | |
| β β | |
| β 1. Login β Store tokens + Schedule refresh β | |
| β 2. Auto-refresh 5 min before expiry β | |
| β 3. Retry on 401 errors β | |
| β 4. Sync tokens across tabs β | |
| β 5. Graceful logout on refresh token expiry β | |
| β β | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β | |
| βΌ | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β BACKEND β | |
| β β | |
| β POST /api/v1/auth/refresh-token β | |
| β β’ Validates refresh token with Supabase β | |
| β β’ Returns new access + refresh tokens (rotated) β | |
| β β’ Returns clear error if refresh token expired β | |
| β β | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ``` | |
| ### Key Principles | |
| 1. **Proactive Refresh** - Don't wait for 401 errors | |
| 2. **Token Rotation Handling** - Always store new refresh token | |
| 3. **Cross-Tab Sync** - Coordinate token updates across tabs | |
| 4. **Graceful Degradation** - Clear messaging when re-login required | |
| 5. **Retry Logic** - Handle transient failures | |
| --- | |
| ## Frontend Implementation | |
| ### 1. Token Refresh Function | |
| ```javascript | |
| // utils/auth.js | |
| const API_BASE = process.env.REACT_APP_API_URL; | |
| /** | |
| * Refresh access token using refresh token | |
| * Returns new tokens or null if refresh failed | |
| */ | |
| export const refreshTokens = async () => { | |
| const refreshToken = localStorage.getItem('refresh_token'); | |
| if (!refreshToken) { | |
| console.warn('No refresh token found'); | |
| handleLogout(); | |
| return null; | |
| } | |
| try { | |
| const res = await fetch(`${API_BASE}/auth/refresh-token`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ refresh_token: refreshToken }), | |
| }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| // Store new tokens (refresh token is rotated by Supabase) | |
| localStorage.setItem('access_token', data.access_token); | |
| localStorage.setItem('refresh_token', data.refresh_token); | |
| const expiresAt = new Date(Date.now() + data.expires_in * 1000); | |
| localStorage.setItem('expires_at', expiresAt.toISOString()); | |
| console.log('β Tokens refreshed successfully'); | |
| // Broadcast to other tabs | |
| broadcastTokenUpdate(data); | |
| // Schedule next refresh | |
| scheduleTokenRefresh(expiresAt); | |
| return data; | |
| } else if (res.status === 401) { | |
| // Refresh token expired | |
| const error = await res.json(); | |
| console.error('Refresh token expired:', error.detail); | |
| // Show user-friendly message | |
| showToast('Your session has expired. Please log in again.', 'warning'); | |
| handleLogout(); | |
| return null; | |
| } else { | |
| throw new Error(`Refresh failed: ${res.status}`); | |
| } | |
| } catch (error) { | |
| console.error('Token refresh error:', error); | |
| handleLogout(); | |
| return null; | |
| } | |
| }; | |
| /** | |
| * Logout user and clear all tokens | |
| */ | |
| const handleLogout = () => { | |
| localStorage.removeItem('access_token'); | |
| localStorage.removeItem('refresh_token'); | |
| localStorage.removeItem('expires_at'); | |
| // Redirect to login | |
| window.location.href = '/login'; | |
| }; | |
| ``` | |
| ### 2. Proactive Token Refresh Scheduler | |
| ```javascript | |
| // utils/auth.js | |
| let refreshTimeout = null; | |
| /** | |
| * Schedule automatic token refresh before expiration | |
| * @param {Date|string} expiresAt - Token expiration timestamp | |
| */ | |
| export const scheduleTokenRefresh = (expiresAt) => { | |
| // Clear any existing timeout | |
| if (refreshTimeout) { | |
| clearTimeout(refreshTimeout); | |
| } | |
| const now = Date.now(); | |
| const expiresAtMs = new Date(expiresAt).getTime(); | |
| const timeUntilExpiry = expiresAtMs - now; | |
| // Refresh 5 minutes (300000ms) before expiration | |
| const BUFFER_TIME = 5 * 60 * 1000; | |
| const refreshTime = timeUntilExpiry - BUFFER_TIME; | |
| if (refreshTime > 0) { | |
| console.log(`π Token refresh scheduled in ${Math.round(refreshTime / 1000 / 60)} minutes`); | |
| refreshTimeout = setTimeout(async () => { | |
| console.log('β° Auto-refreshing token...'); | |
| await refreshTokens(); | |
| }, refreshTime); | |
| } else { | |
| // Token already expired or expires very soon - refresh immediately | |
| console.log('β οΈ Token expired or expiring soon, refreshing now...'); | |
| refreshTokens(); | |
| } | |
| }; | |
| /** | |
| * Initialize token refresh on app startup | |
| */ | |
| export const initializeTokenRefresh = () => { | |
| const expiresAt = localStorage.getItem('expires_at'); | |
| if (expiresAt) { | |
| scheduleTokenRefresh(expiresAt); | |
| } else { | |
| console.warn('No token expiration found'); | |
| } | |
| }; | |
| ``` | |
| ### 3. Cross-Tab Token Synchronization | |
| ```javascript | |
| // utils/auth.js | |
| /** | |
| * Broadcast token updates to other tabs/windows | |
| */ | |
| const broadcastTokenUpdate = (tokenData) => { | |
| // Use BroadcastChannel API (modern browsers) | |
| if ('BroadcastChannel' in window) { | |
| const channel = new BroadcastChannel('auth_channel'); | |
| channel.postMessage({ | |
| type: 'TOKEN_UPDATED', | |
| data: tokenData | |
| }); | |
| } | |
| // Fallback: localStorage events | |
| localStorage.setItem('token_update_event', JSON.stringify({ | |
| timestamp: Date.now(), | |
| data: tokenData | |
| })); | |
| }; | |
| /** | |
| * Listen for token updates from other tabs | |
| */ | |
| export const setupTokenSyncListener = () => { | |
| // BroadcastChannel listener | |
| if ('BroadcastChannel' in window) { | |
| const channel = new BroadcastChannel('auth_channel'); | |
| channel.onmessage = (event) => { | |
| if (event.data.type === 'TOKEN_UPDATED') { | |
| console.log('π‘ Received token update from another tab'); | |
| scheduleTokenRefresh(localStorage.getItem('expires_at')); | |
| } | |
| }; | |
| } | |
| // localStorage listener (fallback) | |
| window.addEventListener('storage', (event) => { | |
| if (event.key === 'token_update_event' && event.newValue) { | |
| console.log('π‘ Received token update via storage event'); | |
| scheduleTokenRefresh(localStorage.getItem('expires_at')); | |
| } | |
| }); | |
| }; | |
| ``` | |
| ### 4. API Interceptor for 401 Errors | |
| ```javascript | |
| // utils/api.js | |
| import axios from 'axios'; | |
| import { refreshTokens } from './auth'; | |
| const api = axios.create({ | |
| baseURL: process.env.REACT_APP_API_URL, | |
| }); | |
| // Request interceptor - Add access token to all requests | |
| api.interceptors.request.use( | |
| (config) => { | |
| const accessToken = localStorage.getItem('access_token'); | |
| if (accessToken) { | |
| config.headers.Authorization = `Bearer ${accessToken}`; | |
| } | |
| return config; | |
| }, | |
| (error) => Promise.reject(error) | |
| ); | |
| // Response interceptor - Auto-retry on 401 with token refresh | |
| api.interceptors.response.use( | |
| (response) => response, | |
| async (error) => { | |
| const originalRequest = error.config; | |
| // If 401 and not already retried | |
| if (error.response?.status === 401 && !originalRequest._retry) { | |
| originalRequest._retry = true; | |
| console.log('π 401 error detected, attempting token refresh...'); | |
| const newTokens = await refreshTokens(); | |
| if (newTokens) { | |
| // Update authorization header with new token | |
| originalRequest.headers.Authorization = `Bearer ${newTokens.access_token}`; | |
| // Retry original request | |
| return api(originalRequest); | |
| } else { | |
| // Refresh failed - user will be logged out by refreshTokens() | |
| return Promise.reject(error); | |
| } | |
| } | |
| return Promise.reject(error); | |
| } | |
| ); | |
| export default api; | |
| ``` | |
| ### 5. Login Flow Integration | |
| ```javascript | |
| // pages/Login.jsx | |
| import { refreshTokens, scheduleTokenRefresh, setupTokenSyncListener } from '../utils/auth'; | |
| const handleLogin = async (email, password) => { | |
| try { | |
| const res = await fetch(`${API_BASE}/auth/login`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email, password }), | |
| }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| // Store tokens | |
| localStorage.setItem('access_token', data.access_token); | |
| localStorage.setItem('refresh_token', data.refresh_token); | |
| const expiresAt = new Date(Date.now() + data.expires_in * 1000); | |
| localStorage.setItem('expires_at', expiresAt.toISOString()); | |
| // Schedule automatic refresh | |
| scheduleTokenRefresh(expiresAt); | |
| // Setup cross-tab sync | |
| setupTokenSyncListener(); | |
| // Navigate to dashboard | |
| navigate('/dashboard'); | |
| } else { | |
| // Handle error | |
| const error = await res.json(); | |
| showToast(error.detail, 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Login error:', error); | |
| showToast('Login failed. Please try again.', 'error'); | |
| } | |
| }; | |
| ``` | |
| ### 6. App Initialization | |
| ```javascript | |
| // App.jsx or main.jsx | |
| import { initializeTokenRefresh, setupTokenSyncListener } from './utils/auth'; | |
| function App() { | |
| useEffect(() => { | |
| // Check if user is logged in | |
| const accessToken = localStorage.getItem('access_token'); | |
| if (accessToken) { | |
| // Initialize automatic token refresh | |
| initializeTokenRefresh(); | |
| // Setup cross-tab synchronization | |
| setupTokenSyncListener(); | |
| } | |
| }, []); | |
| return <Router>...</Router>; | |
| } | |
| ``` | |
| --- | |
| ## Backend Behavior | |
| ### Endpoint: POST /api/v1/auth/refresh-token | |
| **Request:** | |
| ```json | |
| { | |
| "refresh_token": "v1.MKh..." | |
| } | |
| ``` | |
| **Success Response (200):** | |
| ```json | |
| { | |
| "access_token": "eyJhbG...", | |
| "refresh_token": "v1.XYz...", | |
| "expires_in": 3600, | |
| "token_type": "bearer", | |
| "user": { | |
| "id": "uuid", | |
| "email": "user@example.com", | |
| "first_name": "John", | |
| "last_name": "Doe", | |
| "full_name": "John Doe", | |
| "role": "platform_admin", | |
| "is_active": true | |
| } | |
| } | |
| ``` | |
| **Error Responses:** | |
| | Status Code | Error | Meaning | Frontend Action | | |
| |-------------|-------|---------|-----------------| | |
| | 401 | `Refresh token expired. Please log in again.` | Refresh token is expired (30+ days old) | Logout user, redirect to login with message | | |
| | 401 | `Token refresh failed: <error>` | Network or Supabase error | Retry once, then logout if still fails | | |
| | 404 | `User account no longer exists` | User deleted from database | Logout user, show account deletion message | | |
| | 403 | `Account is inactive. Contact support.` | User account deactivated | Logout user, show contact support message | | |
| ### Backend Implementation Details | |
| The backend: | |
| 1. Validates refresh token with Supabase | |
| 2. Returns **rotated tokens** (new refresh token on each call) | |
| 3. Verifies user still exists and is active | |
| 4. Provides clear, actionable error messages | |
| 5. Logs all refresh attempts for security monitoring | |
| **Important:** The backend does NOT store tokens in a database. Supabase manages all token storage and validation. The backend simply validates tokens and returns new ones. | |
| --- | |
| ## Testing & Validation | |
| ### Test Case 1: Normal Token Refresh | |
| **Steps:** | |
| 1. Login to application | |
| 2. Wait 55 minutes (or modify expiry for testing) | |
| 3. Make API request | |
| 4. Verify auto-refresh happens | |
| 5. Verify new tokens stored in localStorage | |
| **Expected:** No logout, seamless refresh | |
| ### Test Case 2: Expired Refresh Token | |
| **Steps:** | |
| 1. Login to application | |
| 2. Manually expire refresh token (modify timestamp or wait 30 days) | |
| 3. Try to make API request | |
| 4. Verify 401 error from backend | |
| 5. Verify user redirected to login with message | |
| **Expected:** Graceful logout with "Session expired" message | |
| ### Test Case 3: Multi-Tab Sync | |
| **Steps:** | |
| 1. Open app in Tab A and Tab B | |
| 2. Login in Tab A | |
| 3. Verify Tab B receives token update | |
| 4. Let token expire in Tab A | |
| 5. Verify Tab B also logs out | |
| **Expected:** Both tabs stay synchronized | |
| ### Test Case 4: Network Failure During Refresh | |
| **Steps:** | |
| 1. Login to application | |
| 2. Disable network | |
| 3. Wait for auto-refresh time | |
| 4. Verify retry logic activates | |
| 5. Re-enable network | |
| 6. Verify refresh succeeds | |
| **Expected:** Retry successful, no logout | |
| ### Test Case 5: Race Condition (Multiple Refreshes) | |
| **Steps:** | |
| 1. Login to application | |
| 2. Trigger multiple API calls simultaneously when token is about to expire | |
| 3. Verify only one refresh request sent | |
| 4. Verify all pending requests use new token | |
| **Expected:** No duplicate refresh calls, all requests succeed | |
| --- | |
| ## Troubleshooting | |
| ### Issue: Users Still Getting Auto-Logged Out | |
| **Diagnosis Steps:** | |
| 1. **Check localStorage:** | |
| ```javascript | |
| console.log('Access Token:', localStorage.getItem('access_token')); | |
| console.log('Refresh Token:', localStorage.getItem('refresh_token')); | |
| console.log('Expires At:', localStorage.getItem('expires_at')); | |
| ``` | |
| 2. **Check if refresh is scheduled:** | |
| ```javascript | |
| // Add to scheduleTokenRefresh function | |
| console.log('Refresh scheduled for:', new Date(expiresAt).toLocaleString()); | |
| ``` | |
| 3. **Check backend logs:** | |
| - Look for `Token refreshed successfully` (success) | |
| - Look for `Token refresh error` (failure) | |
| - Check error details for root cause | |
| 4. **Verify token rotation:** | |
| - After refresh, refresh_token should be different | |
| - If same token returned, rotation may be disabled | |
| 5. **Check Supabase settings:** | |
| - Verify JWT expiry settings in Supabase dashboard | |
| - Confirm refresh token rotation is enabled | |
| ### Common Mistakes | |
| β **Mistake 1:** Waiting for 401 before refreshing | |
| ```javascript | |
| // BAD - Reactive | |
| api.interceptors.response.use(null, async (error) => { | |
| if (error.response.status === 401) { | |
| await refreshTokens(); | |
| } | |
| }); | |
| ``` | |
| β **Fix:** Proactive refresh before expiry | |
| ```javascript | |
| // GOOD - Proactive | |
| scheduleTokenRefresh(expiresAt); | |
| ``` | |
| β **Mistake 2:** Not storing new refresh token | |
| ```javascript | |
| // BAD - Old token reused | |
| localStorage.setItem('access_token', data.access_token); | |
| // Missing: localStorage.setItem('refresh_token', data.refresh_token); | |
| ``` | |
| β **Fix:** Always update both tokens | |
| ```javascript | |
| // GOOD - Both tokens updated | |
| localStorage.setItem('access_token', data.access_token); | |
| localStorage.setItem('refresh_token', data.refresh_token); | |
| ``` | |
| β **Mistake 3:** Not handling 401 on refresh endpoint | |
| ```javascript | |
| // BAD - Infinite loop | |
| if (res.status === 401) { | |
| await refreshTokens(); // Calls itself | |
| } | |
| ``` | |
| β **Fix:** Logout on refresh failure | |
| ```javascript | |
| // GOOD - Break loop | |
| if (res.status === 401) { | |
| handleLogout(); | |
| } | |
| ``` | |
| ### Debug Checklist | |
| - [ ] Refresh token is stored in localStorage | |
| - [ ] scheduleTokenRefresh is called after login | |
| - [ ] Timeout is set (check browser console) | |
| - [ ] New tokens stored after refresh | |
| - [ ] API interceptor configured correctly | |
| - [ ] 401 errors trigger refresh attempt | |
| - [ ] Failed refresh triggers logout | |
| - [ ] Cross-tab sync working (optional but recommended) | |
| - [ ] Error messages displayed to user | |
| - [ ] Backend logs show refresh attempts | |
| --- | |
| ## Best Practices Summary | |
| ### DO β | |
| - **Refresh proactively** 5 minutes before expiry | |
| - **Store new refresh token** after each refresh | |
| - **Schedule next refresh** after successful refresh | |
| - **Synchronize across tabs** using BroadcastChannel | |
| - **Handle 401 gracefully** with retry logic | |
| - **Show clear messages** when re-login required | |
| - **Log all token operations** for debugging | |
| - **Test token expiry scenarios** thoroughly | |
| ### DON'T β | |
| - **Don't wait for 401** to refresh tokens | |
| - **Don't ignore new refresh token** from response | |
| - **Don't refresh on every 401** (check if not already refreshing) | |
| - **Don't store tokens in cookies** unless using HttpOnly | |
| - **Don't create database table** for tokens (Supabase handles this) | |
| - **Don't show technical errors** to users | |
| - **Don't refresh more than once** for same expiry | |
| - **Don't forget to clear tokens** on logout | |
| --- | |
| ## Security Considerations | |
| ### Token Storage | |
| **LocalStorage (Current Implementation):** | |
| - β Simple to implement | |
| - β Persists across page refreshes | |
| - β Vulnerable to XSS attacks | |
| - β Accessible to all scripts | |
| **HttpOnly Cookies (Recommended for Production):** | |
| - β Not accessible to JavaScript (XSS protection) | |
| - β Automatically sent with requests | |
| - β Can set secure and sameSite flags | |
| - β Requires backend cookie handling | |
| **In-Memory Storage (Most Secure):** | |
| - β Not vulnerable to XSS | |
| - β Cleared on page refresh | |
| - β Poor UX (users logged out on refresh) | |
| - β Doesn't work with multi-tab sync | |
| ### Token Rotation | |
| Supabase automatically rotates refresh tokens for security: | |
| - Each refresh returns a **new refresh token** | |
| - Old refresh token becomes **invalid** | |
| - Prevents token replay attacks | |
| - Reduces impact of stolen tokens | |
| ### Monitoring | |
| Log these events for security monitoring: | |
| - Login attempts (success/failure) | |
| - Token refresh attempts (success/failure) | |
| - Logout events (user-initiated vs. auto-logout) | |
| - Multiple failed refresh attempts (potential attack) | |
| - Token expiry patterns (identify inactive users) | |
| --- | |
| ## Migration Guide | |
| If you're currently experiencing auto-logout issues: | |
| ### Phase 1: Immediate Fix (Frontend) | |
| 1. Add proactive token refresh scheduler | |
| 2. Update token after each refresh | |
| 3. Add 401 error interceptor | |
| 4. Test with short expiry times | |
| ### Phase 2: Enhanced UX | |
| 1. Add cross-tab synchronization | |
| 2. Show clear expiry messages | |
| 3. Add retry logic for network failures | |
| 4. Implement token refresh progress indicator | |
| ### Phase 3: Production Hardening | |
| 1. Switch to HttpOnly cookies (optional) | |
| 2. Add security monitoring/logging | |
| 3. Implement token refresh rate limiting | |
| 4. Add user session management dashboard | |
| --- | |
| ## Additional Resources | |
| - **Supabase Auth Docs:** https://supabase.com/docs/guides/auth | |
| - **JWT Best Practices:** https://tools.ietf.org/html/rfc8725 | |
| - **Token Refresh Patterns:** https://auth0.com/docs/secure/tokens/refresh-tokens | |
| - **BroadcastChannel API:** https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel | |
| --- | |
| ## Support | |
| If you continue experiencing issues: | |
| 1. Enable debug logging in frontend | |
| 2. Check backend logs for refresh attempts | |
| 3. Verify Supabase project settings | |
| 4. Test with curl/Postman to isolate frontend vs. backend issues | |
| 5. Check for browser extensions blocking requests | |
| --- | |
| **Last Updated:** November 18, 2025 | |
| **Maintained By:** SwiftOps Backend Team | |
| **Related Docs:** AUTH_API_COMPLETE.md, GETTING_STARTED_AUTH.md | |