This is an interesting issue that points to a state synchronization problem between the Supabase client and the Chrome extension's network layer after token refresh. Let me break down what's likely happening and provide solutions.
Root Cause Analysis
The behavior you're describing suggests that the Supabase client's internal fetch implementation isn't properly updating its authorization headers or cookies after a token refresh in the Chrome extension environment. The manual fetch "fixes" it because it likely triggers some internal state update or cookie sync.
Why This Happens in Chrome Extensions
Chrome extensions have unique characteristics that can cause this:
- Service Worker Context (MV3): Background scripts in MV3 run as service workers, which have different cookie/storage handling than regular web pages
- Cookie Partitioning: Chrome extensions handle cookies differently, especially with cross-origin requests
- Fetch Interceptors: The Supabase client may use fetch interceptors that don't properly update in the extension context
Solutions & Workarounds
Solution 1: Force Client Refresh After Token Update
javascript
// In your background script
supabase.auth.onAuthStateChange(async (event, session) => {
if (event === 'TOKEN_REFRESHED' && session) {
// Force update the client's auth headers
await supabase.auth.setSession({
access_token: session.access_token,
refresh_token: session.refresh_token
});
// Alternative: Recreate the client instance
// reinitializeSupabaseClient(session);
}
});
Solution 2: Custom Fetch Wrapper (Recommended for Chrome Extensions)
javascript
// Create a custom fetch wrapper that always uses fresh tokens
async function supabaseInvoke(functionName, options = {}) {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
throw new Error('No active session');
}
const response = await fetch(`${SUPABASE_URL}/functions/v1/${functionName}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
'apikey': SUPABASE_ANON_KEY,
},
body: JSON.stringify(options.body || {}),
});
if (!response.ok) {
throw new Error(`Function call failed: ${response.statusText}`);
}
return response.json();
}
Solution 3: Ensure Proper Cookie Configuration
javascript
// When initializing Supabase client
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
storage: customExtensionStorage, // Custom storage implementation
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false, // Important for extensions
flowType: 'pkce', // Use PKCE flow for better security
storageKey: 'supabase-auth', // Explicit storage key
},
global: {
headers: {
'X-Client-Info': 'chrome-extension'
},
fetch: (...args) => {
// Custom fetch that ensures proper header propagation
return fetch(...args);
}
}
});
Solution 4: Custom Storage Adapter for MV3
javascript
// Implement a proper storage adapter for Chrome extensions
const customExtensionStorage = {
getItem: async (key) => {
const result = await chrome.storage.local.get(key);
return result[key] || null;
},
setItem: async (key, value) => {
await chrome.storage.local.set({ [key]: value });
},
removeItem: async (key) => {
await chrome.storage.local.remove(key);
}
};
Answers to Your Specific Questions
- Has anyone experienced this?
Yes, this is a known issue with Supabase in Chrome extensions, particularly MV3. It's related to how service workers handle fetch and cookies differently.
- Proper way to refresh client state?
Either manually call setSession() with the new tokens or implement a custom fetch wrapper that always gets fresh tokens.
- Why does manual fetch fix it?
The manual fetch likely triggers Chrome's internal cookie sync mechanism or causes the Supabase client to re-read its stored session state.
- Is direct fetch recommended?
Yes, for Chrome extensions, direct fetch is often more reliable. The Supabase client wasn't specifically designed for the Chrome extension environment, so using direct fetch with manual token management gives you more control.
Recommended Approach for Production
For a production Chrome extension, I'd recommend:
javascript
class SupabaseExtensionClient {
constructor(url, anonKey) {
this.url = url;
this.anonKey = anonKey;
this.supabase = createClient(url, anonKey, {
auth: { storage: customExtensionStorage }
});
// Listen for auth changes
this.supabase.auth.onAuthStateChange(this.handleAuthChange.bind(this));
}
async handleAuthChange(event, session) {
if (event === 'TOKEN_REFRESHED') {
// Store the fresh session
await chrome.storage.local.set({
'supabase_session': session
});
}
}
async invokeFunction(functionName, body = {}) {
// Always get fresh session from storage
const { supabase_session } = await chrome.storage.local.get('supabase_session');
if (!supabase_session?.access_token) {
throw new Error('No valid session');
}
return fetch(`${this.url}/functions/v1/${functionName}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${supabase_session.access_token}`,
'apikey': this.anonKey,
},
body: JSON.stringify(body)
}).then(res => res.json());
}
}
This approach gives you full control over token management and avoids the state synchronization issues you're experiencing. It's more verbose but much more reliable in the Chrome extension context.