DEV Community

Cover image for Why we removed Google sign-in from our web app to support a browser extension
Tommaso Musetti
Tommaso Musetti

Posted on

Why we removed Google sign-in from our web app to support a browser extension

MyPrompt is a prompt manager: a web dashboard where you write and organize prompts, and a browser extension that injects them into ChatGPT, Claude, Gemini, and Perplexity. Both halves talk to the same Firestore database, and both need the same user to be signed in.

The web app shipped first, with the sign-in options you would expect: email and password, plus "Continue with Google." The extension came later. By the time it was working, Google sign-in was gone — not just missing from the extension, but deleted from the web app too.

That sounds backwards. Removing a feature from a working product to accommodate a secondary client usually is. This is the reasoning that got us there, and what MV3 forced on us regardless.

The problem: the extension is a separate client

A browser extension is not a tab of your website. It has its own origin, its own storage, its own everything. Whatever session the user has in the web app is invisible to it. So the extension has to authenticate independently, and it has to end up holding a Firebase Auth session for the same user, or Firestore security rules will correctly refuse to hand over that user's prompts.

If a user signed up with Google, there is no password to type into the extension. Their only path in is Google. Which means the extension needs a working Google OAuth flow, or those users are locked out of half the product.

So: how hard is Google sign-in inside a Manifest V3 extension?

Hard enough, and for an unusually annoying reason

The obvious tool is chrome.identity. Two problems stack up.

First, the extension ID. A Google OAuth client of type "Chrome Extension" is registered against a specific extension ID. That ID is derived from the extension's signing key. During development, an unpacked extension gets an ID generated from its path — a different one on your machine than on a colleague's, and a different one again from the ID the Chrome Web Store assigns on publish. To pin it, you extract the public key from your .pem and paste it into manifest.json as the key field, which is why our README still tells you what happens when that field is absent:

If you plan to publish your own build to the Chrome Web Store, generate your own signing key (Chrome assigns a new extension ID automatically once manifest.json's key field is absent).

None of this is undocumented. It is just a circular dependency you have to unwind by hand before you can write a line of auth code: the OAuth client needs the ID, the ID comes from the key, the key has to be generated and pinned before the ID means anything. Coming at it fresh, the IDs and redirect URLs feel arbitrary, because at that point they are — you have not yet done the step that makes them stable.

Second, and decisive: we ship on Firefox too. Our manifest carries a browser_specific_settings.gecko block, and the web app links to both stores depending on the browser:

const CHROME_URL = 'https://chromewebstore.google.com'
const FIREFOX_URL = 'https://addons.mozilla.org/en-US/firefox/addon/myprompt/'
Enter fullscreen mode Exit fullscreen mode

chrome.identity.getAuthToken is Chrome-only, so the simplest path was off the table immediately. launchWebAuthFlow is available in both, but it comes with its own redirect-URL setup that differs between the two browsers — which means the identity work above is not something you do once, it is something you do per browser.

So the real cost was never "set up OAuth." It was "set up and maintain two different OAuth paths, on two browsers, against a third-party console that wants stable identifiers, for a product with one developer."

The decision: one auth method, everywhere

We removed Google sign-in from the web app and standardized on email and password on both sides.

This is the part worth being honest about: it is a downgrade in sign-up conversion. "Continue with Google" is one click and no password to invent. Email and password is a form, and for a free tool that friction costs you users at the top of the funnel.

We took it anyway, for two reasons that turned out to matter more than the click:

  • The identity setup never stopped costing. Getting the OAuth client, the signing key, the extension ID, and the redirect URL to line up is not conceptually hard, but it is a chain where every link has to be right and the whole thing has to be redone per browser. For a two-person side project, that work has no natural end and produces nothing a user can see.
  • The accounts drifted apart. A Google user and an email user are different Firebase Auth records unless you explicitly link credentials. Without account linking, someone who signed up with Google on the web had no way into the extension at all — and the failure is silent from our side. They do not file a bug. They uninstall.

One auth method means the extension's login is four lines, and it cannot drift out of sync with the web app:

export async function login(email: string, password: string): Promise<void> {
  await signInWithEmailAndPassword(auth, email, password)
}
Enter fullscreen mode Exit fullscreen mode

If the product ever justifies the cost — real users, real demand — Google sign-in goes back in, on both sides at once, with account linking designed in from the start. It is not a door we nailed shut.

What MV3 forced on us anyway

Dropping OAuth did not make Firebase Auth in an extension free. Two things still needed handling.

Use the web-extension entry point. Manifest V3 forbids loading remote code, and the standard Firebase Auth build reaches for remotely hosted helpers. The SDK ships a separate entry point for exactly this:

import { getAuth, connectAuthEmulator } from 'firebase/auth/web-extension'
Enter fullscreen mode Exit fullscreen mode

It is MV3-compliant, loads nothing remotely, and persists to IndexedDB by default. This is a one-line change that is easy to miss and produces confusing failures if you do.

Name your Firebase app instance. The extension's content scripts run on pages that may themselves be running Firebase. An unnamed default app can collide with the host page's:

export const firebaseApp = initializeApp(firebaseConfig, 'myprompt-extension')
Enter fullscreen mode Exit fullscreen mode

The bug nobody writes about: the popup flash

This one is specific to extensions and took longer to notice than to fix.

A popup is destroyed and recreated every single time the user clicks the toolbar icon. There is no warm process holding UI state. Meanwhile onAuthStateChanged is asynchronous: on a cold start, Firebase has not restored the session from IndexedDB yet, so for a few hundred milliseconds the extension believes nobody is signed in.

Naively rendered, that means a signed-in user sees the login screen flash on every open. It is a small thing that makes a product feel broken.

The fix is a synchronous-ish fast path: cache a minimal user record in chrome.storage.local and read it before Firebase resolves, then let onAuthStateChanged correct it.

// Fast path: read cached state from storage before Firebase resolves async
chrome.storage.local.get('mp_auth_user').then((result) => {
  currentUser.value = (result['mp_auth_user'] as StoredUser | null) ?? null
  if (!authReady.value) authReady.value = true
})

// Subscribe to Firebase auth for live updates and token refresh
onAuthStateChanged(auth, (user) => {
  currentUser.value = user
    ? { uid: user.uid, email: user.email ?? '', displayName: user.displayName ?? '' }
    : null
  authReady.value = true
})
Enter fullscreen mode Exit fullscreen mode

The popup then waits on a single authReady flag before deciding what to render, so the first paint is either the real screen or a deliberate loading state — never the wrong screen.

The cached copy is a convenience, not a credential. It holds a uid, an email, and a display name. Firestore rules trust the Firebase ID token and nothing else, so a tampered cache buys an attacker a misleading popup and zero data.

Takeaway

If you are adding a browser extension to an existing web app, audit your sign-in methods before you write any extension code. Every auth provider you support on the web is a provider you now have to support in an environment with a different security model, on every browser you ship to, forever.

Sometimes the right move is to support fewer.


MyPrompt is MIT-licensed and runs against the Firebase emulator with no account required: github.com/It-Codes-Two/my-prompt

Top comments (0)