
Supabase Auth in a Chrome Extension: What You Won't Find in the Docs
Supabase Auth in a Chrome Extension: What You Won't Find in the Docs
Supabase calls itself the easy, open-source alternative to Firebase, and for web apps, that's true. Setting up Auth takes about ten minutes. But as soon as you try to use Supabase in a Chrome extension, especially with Manifest V3, you'll find yourself in areas the official docs don't explain.

This post explains what goes wrong, why it happens, and how you can fix it.
Why this is harder than it looks
Setting up Supabase OAuth on a regular website is straightforward. The user clicks "Sign in with Google," gets redirected to Google, approves, and then returns to your https://yourapp.com/auth/callback URL. Supabase reads the token from the URL hash, and that's it.

Chrome extensions can't do that. Here's why:
1. There's no meaningful "page" to redirect back to
An extension popup isn't a normal webpage. It runs atchrome-extension://<id>/popup.html. Even if you managed to get Supabase to redirect there, the popup would close as soon as the user clicks away, and the auth state would be lost.
2. The background script has no localStorage
By default, Supabase's JS client saves session data in localStorage. But background service workers in MV3 don't have localStorage or even a window object. If you try to set up the Supabase client directly in a service worker, the session won't persist after restarts.
3. Service workers are ephemeral
Chrome shuts down MV3 service workers when they're idle and restarts them as needed. Each restart wipes everything in memory. The Supabase client creates and stores its PKCE state (the code_verifier) right before the OAuth flow, but if the service worker restarts before the code exchange, the verifier is lost and the exchange fails.
4. chrome.identity doesn't play nicely with Supabase's default flow
The right way to open an OAuth popup in an extension is through chrome.identity.launchWebAuthFlow. But this API intercepts the final redirect. Supabase's implicit flow puts the auth token in the URL hash, which chrome.identity strips before returning the URL. PKCE flow avoids this by putting a code in the query string instead.
The solution: PKCE + chrome.identity + chrome.storage.local
Once you know these limitations, the solution is straightforward:
- Use PKCE flow → get a code in the redirect URL instead of a token in the hash.
- Use chrome.identity → manages the OAuth popup and intercepts the redirect.
- chrome.storage.local → Persistent, async, works in service workers.
The Supabase client → with a chrome.storage.local adapter
// supabaseClient.js
let _cache = {};
const chromeStorageAdapter = {
getItem: (key) => _cache[key] ?? null,
setItem: (key, value) => {
_cache[key] = value;
chrome.storage.local.set({ [key]: value });
},
removeItem: (key) => {
delete _cache[key];
chrome.storage.local.remove(key);
},
};
export async function hydrateStorageCache() {
return new Promise((resolve) => {
chrome.storage.local.get(null, (items) => {
_cache = { ..._cache, ...items };
resolve();
});
});
}
export function getSupabaseClient() {
return createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
storage: chromeStorageAdapter,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
flowType: "pkce",
},
});
}The auth flow
export async function loginWithSupabase({ provider }) {
const supabase = getSupabaseClient();
const { data } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: REDIRECT_URL,
skipBrowserRedirect: true,
},
});
const redirectResult = await new Promise((resolve, reject) => {
chrome.identity.launchWebAuthFlow(
{ url: data.url, interactive: true },
(responseUrl) => {
if (chrome.runtime.lastError) reject(new Error(chrome.runtime.lastError.message));
else resolve(responseUrl);
}
);
});
const code = new URL(redirectResult).searchParams.get("code");
const { data: sessionData } = await supabase.auth.exchangeCodeForSession(code);
return { session: sessionData.session, user: sessionData.session?.user };
}Conclusion: By switching to PKCE and using chrome.storage.local, you can create a robust authentication system for your Chrome extensions that survives service worker restarts and follows MV3 best practices.