This error occurs when Supabase cannot find a valid refresh token during authentication. Refresh tokens are used to obtain new access tokens without requiring users to re-enter credentials. The fix typically involves checking token storage, session management, and authentication flow configuration.
The "refresh_token_not_found" error in Supabase indicates that the authentication system cannot locate a valid refresh token when attempting to refresh an access token. In Supabase's authentication flow: 1. **Access tokens** are short-lived (typically 1 hour) and used for API authorization 2. **Refresh tokens** are long-lived and used to obtain new access tokens without user interaction 3. **Session management** relies on both tokens being properly stored and accessible When a user's access token expires, Supabase automatically attempts to use the refresh token to get a new access token. If the refresh token is missing, invalid, expired, or cannot be retrieved from storage, this error occurs. This issue commonly appears when: - Local storage or cookies are cleared - Multiple tabs/windows cause session conflicts - Authentication state isn't properly persisted - Refresh token rotation is misconfigured - The user logs out from another device/session
First, verify what's happening with the current session:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
// Check current session
const { data: { session }, error } = await supabase.auth.getSession()
console.log('Session:', session)
console.log('Error:', error)
// Check if user is authenticated
const { data: { user } } = await supabase.auth.getUser()
console.log('User:', user)This helps identify if the session exists but the refresh token is missing.
Ensure you're using the correct storage method for your use case:
// For web apps that need persistence across browser restarts
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
storage: localStorage, // Default - persists across browser restarts
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true
}
})
// For mobile apps or secure contexts
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
storage: AsyncStorage, // React Native
autoRefreshToken: true,
persistSession: true
}
})
// For server-side rendering (Next.js, Nuxt)
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
storage: {
getItem: (key) => {
// Implement secure server-side storage
},
setItem: (key, value) => {
// Implement secure server-side storage
},
removeItem: (key) => {
// Implement secure server-side storage
}
}
}
})Implement proper session state management:
// Subscribe to auth state changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
console.log('Auth event:', event)
console.log('Session:', session)
// Handle specific events
switch (event) {
case 'SIGNED_IN':
// Session created, store tokens
break
case 'TOKEN_REFRESHED':
// Tokens refreshed successfully
break
case 'SIGNED_OUT':
// Clear local storage
localStorage.removeItem('supabase.auth.token')
break
case 'USER_UPDATED':
// User data updated
break
}
}
)
// Clean up subscription when component unmounts
subscription.unsubscribe()Handle multiple tabs/windows with broadcast channel:
// Create a broadcast channel for auth state
const authChannel = new BroadcastChannel('supabase-auth')
// Listen for auth events from other tabs
authChannel.onmessage = (event) => {
if (event.data.type === 'SIGNED_IN' || event.data.type === 'SIGNED_OUT') {
// Sync auth state across tabs
window.location.reload()
}
}
// Broadcast auth events
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
authChannel.postMessage({ type: event, session })
}
)
// Or use localStorage events as fallback
window.addEventListener('storage', (event) => {
if (event.key === 'supabase.auth.token') {
// Auth state changed in another tab
window.location.reload()
}
})Add comprehensive error handling:
async function handleAuthError(error) {
console.error('Auth error:', error)
if (error.message.includes('refresh_token_not_found')) {
// Refresh token is missing
console.log('Refresh token not found, attempting recovery...')
// Option 1: Redirect to login
// window.location.href = '/login'
// Option 2: Try to get new session
const { error: refreshError } = await supabase.auth.refreshSession()
if (refreshError) {
// Refresh failed, clear storage and redirect
await supabase.auth.signOut()
localStorage.clear()
window.location.href = '/login?error=session_expired'
}
}
}
// Use in your auth calls
try {
const { data, error } = await supabase.auth.getSession()
if (error) await handleAuthError(error)
} catch (err) {
await handleAuthError(err)
}Verify backend configuration:
1. Check auth.sessions table:
-- Check if sessions exist for user
SELECT * FROM auth.sessions
WHERE user_id = 'user-uuid-here'
ORDER BY created_at DESC
LIMIT 5;
-- Check for session limits
SELECT count(*) as session_count FROM auth.sessions
WHERE user_id = 'user-uuid-here';2. Verify Supabase project settings:
- Go to Supabase Dashboard → Authentication → Settings
- Check "Session Length" (default: 3600 seconds for access token, 604800 for refresh token)
- Verify "Refresh Token Rotation" is enabled for security
- Check "URL Configuration" matches your app's domain
3. Review RLS policies if using custom session handling
### Refresh Token Rotation
Supabase supports refresh token rotation for enhanced security. When enabled:
- Each refresh token can only be used once
- New refresh tokens are issued with each rotation
- Compromised tokens become useless after first use
Enable in Supabase Dashboard → Authentication → Settings → Refresh Token Rotation.
### Mobile App Considerations
For React Native/Expo apps:
- Use expo-secure-store or @react-native-async-storage/async-storage
- Handle app state changes (background/foreground)
- Implement deep linking for OAuth callbacks
### Server-Side Rendering (Next.js)
For Next.js App Router:
// app/layout.tsx
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export default async function RootLayout() {
const cookieStore = cookies()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name) {
return cookieStore.get(name)?.value
},
set(name, value, options) {
cookieStore.set({ name, value, ...options })
},
remove(name, options) {
cookieStore.set({ name, value: '', ...options })
},
},
}
)
const { data: { session } } = await supabase.auth.getSession()
}### Security Best Practices
1. Never store tokens in insecure locations
2. Use HTTP-only cookies for server-side apps
3. Implement proper CORS configuration
4. Regularly rotate refresh tokens
5. Monitor auth.sessions table for anomalies
6. Set appropriate session timeouts based on app sensitivity
### Debugging Tips
1. Check browser DevTools → Application → Storage
2. Monitor Network tab for auth requests
3. Enable Supabase client debugging:
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: { debug: true }
})4. Check Supabase Logs in dashboard for server-side errors
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