Supabase returns "flow_state_not_found: PKCE flow state could not be found" when the OAuth authorization flow state is missing, expired, or corrupted. This typically happens when users take too long to complete social login, when browser sessions are cleared mid-flow, or when PKCE code verifiers don't match. The fix involves adjusting flow timeouts, preserving session storage, and ensuring consistent PKCE implementation.
The "flow_state_not_found" error occurs during Supabase OAuth authentication when the server cannot locate the Proof Key for Code Exchange (PKCE) flow state that was created at the beginning of the authorization process. PKCE is a security mechanism that prevents authorization code interception attacks by binding the authorization request to the token exchange. When a user initiates OAuth login (e.g., with GitHub, Google, etc.), Supabase creates a unique "flow state" record containing the PKCE code verifier and other session details. This state is stored server-side with a limited lifespan (typically 5-10 minutes). If the user takes too long to complete the OAuth flow, clears browser storage, or if there's a mismatch between the code verifier sent to the provider and the one stored, Supabase cannot retrieve the flow state and returns this error. The error indicates a broken authentication flow rather than invalid credentials, requiring attention to session management, timeout settings, and PKCE implementation consistency.
By default, Supabase PKCE flow states expire after 5-10 minutes. If users are taking longer (e.g., reading permissions screens), increase the timeout:
// Server-side configuration (if self-hosting Supabase)
// In your Supabase Auth configuration or environment variables
AUTH_FLOW_STATE_EXPIRY=900 // 15 minutes in seconds
// For managed Supabase, you may need to contact support
// or check project settings for timeout adjustmentsIf you cannot modify server timeouts, implement client-side warnings when the flow is taking too long.
Ensure the PKCE code verifier survives browser redirects and page reloads:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(supabaseUrl, supabaseAnonKey)
// Store code verifier in localStorage (survives page reloads)
// instead of sessionStorage (cleared on page reload)
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
// Supabase automatically handles PKCE, but ensure storage is consistent
redirectTo: 'https://yourapp.com/auth/callback',
// For custom implementations, store the code verifier:
// codeVerifier: localStorage.getItem('pkce_code_verifier')
}
})
// Clean up stored verifier after successful authentication
window.addEventListener('beforeunload', () => {
localStorage.removeItem('pkce_code_verifier')
})Consider using IndexedDB for more robust storage if localStorage is being cleared.
Add error handling to detect flow_state_not_found and restart the OAuth flow:
async function handleOAuthCallback() {
try {
const { data, error } = await supabase.auth.getSession()
if (error?.message.includes('flow_state_not_found')) {
console.warn('PKCE flow state expired or lost, restarting OAuth flow')
// Clear any stale auth state
await supabase.auth.signOut()
// Restart OAuth flow with fresh state
const { error: oauthError } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: window.location.href,
skipBrowserRedirect: false
}
})
if (oauthError) throw oauthError
return
}
// Normal session handling...
} catch (error) {
console.error('Auth flow error:', error)
// Show user-friendly message and retry button
}
}Provide users with a clear "Try again" button when this error occurs.
Prevent multiple simultaneous OAuth attempts that can corrupt flow state:
let oauthInProgress = false
async function startOAuthFlow(provider) {
if (oauthInProgress) {
console.warn('OAuth flow already in progress, waiting...')
return
}
oauthInProgress = true
try {
const { error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: 'https://yourapp.com/auth/callback'
}
})
if (error) throw error
} finally {
// Reset flag after timeout to handle abandoned flows
setTimeout(() => {
oauthInProgress = false
}, 30000) // 30 seconds
}
}
// Also prevent multiple tabs from initiating OAuth simultaneously
window.addEventListener('storage', (event) => {
if (event.key === 'oauth_flow_active') {
if (event.newValue === 'true') {
console.warn('OAuth flow active in another tab')
// Show warning or disable login button
}
}
})Store OAuth state in shared storage (localStorage) to detect cross-tab conflicts.
If using custom OAuth providers, ensure proper PKCE implementation:
// Correct PKCE flow for custom OAuth
import { generatePKCE } from '@supabase/auth-js'
// 1. Generate code verifier and challenge
const { codeVerifier, codeChallenge } = await generatePKCE()
// 2. Store code verifier securely (survives redirects)
localStorage.setItem('pkce_code_verifier', codeVerifier)
// 3. Include code_challenge in authorization request
const authUrl = `https://custom-provider.com/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/auth/callback&response_type=code&code_challenge=${codeChallenge}&code_challenge_method=S256`
// 4. Exchange code for token with stored verifier
async function exchangeCode(code) {
const codeVerifier = localStorage.getItem('pkce_code_verifier')
const response = await fetch('https://custom-provider.com/oauth/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'https://yourapp.com/auth/callback',
client_id: 'YOUR_CLIENT_ID',
code_verifier: codeVerifier
})
})
// Clean up after successful exchange
localStorage.removeItem('pkce_code_verifier')
return response.json()
}Ensure code_verifier matches the original code_challenge sent to the provider.
Add detailed logging to diagnose flow_state_not_found occurrences:
// Client-side logging
supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth event:', event, 'Session:', session)
if (event === 'SIGNED_IN') {
console.log('PKCE flow completed successfully')
// Clear any debugging flags
localStorage.removeItem('oauth_flow_started')
}
})
// Log OAuth initiation
localStorage.setItem('oauth_flow_started', Date.now())
// Server-side (if you have access to Supabase logs)
// Monitor these log patterns:
// - "flow_state_not_found" errors in auth logs
// - PKCE flow creation vs completion timestamps
// - User agent and IP patterns for failing flows
// Implement alerting for elevated failure rates
const FLOW_STATE_FAILURE_THRESHOLD = 0.1 // 10%
if (flowStateFailures / totalOAuthAttempts > FLOW_STATE_FAILURE_THRESHOLD) {
console.error('Elevated PKCE flow state failure rate detected')
// Send alert to monitoring system
}Track failure rates by provider, user agent, and time of day to identify patterns.
### PKCE Flow State Storage in Supabase
Supabase stores PKCE flow states in memory or Redis (depending on configuration) with a default TTL of 5-10 minutes. Each flow state contains:
- Code verifier (the secret)
- Code challenge (hashed verifier sent to OAuth provider)
- Provider configuration
- Redirect URL
- Creation timestamp
When the user returns from the OAuth provider, Supabase looks up the flow state using a combination of provider, code verifier hash, and sometimes a stored session ID. If any component is missing or mismatched, the lookup fails.
### Browser Storage Considerations
Different storage mechanisms have different behaviors:
- sessionStorage: Cleared when tab closes, not shared across tabs
- localStorage: Persists until explicitly cleared, shared across tabs
- IndexedDB: Most robust, survives service worker restarts
For OAuth flows that may involve multiple redirects or app switching (common in mobile), consider using IndexedDB or a combination of localStorage with fallback logic.
### Mobile and Deep Link Considerations
When using Supabase Auth in mobile apps with deep links:
1. The OAuth flow starts in system browser
2. Returns to app via deep link
3. App must preserve PKCE state across this context switch
Implement platform-specific storage that survives the browser-to-app transition, such as secure enclave storage on iOS or encrypted SharedPreferences on Android.
### Scaling Considerations
At high traffic volumes:
- Ensure Supabase Auth service has sufficient memory for flow state storage
- Consider Redis persistence if self-hosting
- Monitor memory usage and flow state eviction rates
- Implement exponential backoff for retries during peak loads
email_conflict_identity_not_deletable: Cannot delete identity because of email conflict
How to fix "Cannot delete identity because of email conflict" in Supabase
mfa_challenge_expired: MFA challenge has expired
How to fix "mfa_challenge_expired: MFA challenge has expired" in Supabase
conflict: Database conflict, usually related to concurrent requests
How to fix "database conflict usually related to concurrent requests" in Supabase
phone_exists: Phone number already exists
How to fix "phone_exists" in Supabase
StorageApiError: resource_already_exists
StorageApiError: Resource already exists