back

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.

Supabase vs Chrome Extension Architecture Clash
Supabase OAuth flow comparison between a regular web app and a Manifest V3 Chrome extension showing PKCE, chrome.identity.launchWebAuthFlow, and chrome.storage.local session handling.

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.

Supabase PKCE Flow
Manifest V3 Chrome extension Supabase authentication architecture using PKCE flow, chrome.identity.launchWebAuthFlow, offscreen document, and chrome.storage.local for session persistence.

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.

Hello