<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tommaso Musetti</title>
    <description>The latest articles on DEV Community by Tommaso Musetti (@tommaso_musetti).</description>
    <link>https://dev.to/tommaso_musetti</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4054800%2Fda231ecb-e982-40aa-88fd-c924a5d78cde.png</url>
      <title>DEV Community: Tommaso Musetti</title>
      <link>https://dev.to/tommaso_musetti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tommaso_musetti"/>
    <language>en</language>
    <item>
      <title>Why we removed Google sign-in from our web app to support a browser extension</title>
      <dc:creator>Tommaso Musetti</dc:creator>
      <pubDate>Thu, 30 Jul 2026 10:22:20 +0000</pubDate>
      <link>https://dev.to/tommaso_musetti/why-we-removed-google-sign-in-from-our-web-app-to-support-a-browser-extension-3e15</link>
      <guid>https://dev.to/tommaso_musetti/why-we-removed-google-sign-in-from-our-web-app-to-support-a-browser-extension-3e15</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: the extension is a separate client
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;same&lt;/em&gt; user, or Firestore security rules will correctly refuse to hand over that user's prompts.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So: how hard is Google sign-in inside a Manifest V3 extension?&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard enough, and for an unusually annoying reason
&lt;/h2&gt;

&lt;p&gt;The obvious tool is &lt;code&gt;chrome.identity&lt;/code&gt;. Two problems stack up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, the extension ID.&lt;/strong&gt; 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 &lt;code&gt;.pem&lt;/code&gt; and paste it into &lt;code&gt;manifest.json&lt;/code&gt; as the &lt;code&gt;key&lt;/code&gt; field, which is why our README still tells you what happens when that field is absent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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 &lt;code&gt;manifest.json&lt;/code&gt;'s &lt;code&gt;key&lt;/code&gt; field is absent).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, and decisive: we ship on Firefox too.&lt;/strong&gt; Our manifest carries a &lt;code&gt;browser_specific_settings.gecko&lt;/code&gt; block, and the web app links to both stores depending on the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHROME_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://chromewebstore.google.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FIREFOX_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://addons.mozilla.org/en-US/firefox/addon/myprompt/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chrome.identity.getAuthToken&lt;/code&gt; is Chrome-only, so the simplest path was off the table immediately. &lt;code&gt;launchWebAuthFlow&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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."&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision: one auth method, everywhere
&lt;/h2&gt;

&lt;p&gt;We removed Google sign-in from the web app and standardized on email and password on both sides.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;We took it anyway, for two reasons that turned out to matter more than the click:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The identity setup never stopped costing.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The accounts drifted apart.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One auth method means the extension's login is four lines, and it cannot drift out of sync with the web app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signInWithEmailAndPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MV3 forced on us anyway
&lt;/h2&gt;

&lt;p&gt;Dropping OAuth did not make Firebase Auth in an extension free. Two things still needed handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the web-extension entry point.&lt;/strong&gt; 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;connectAuthEmulator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase/auth/web-extension&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name your Firebase app instance.&lt;/strong&gt; 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firebaseApp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;initializeApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firebaseConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myprompt-extension&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bug nobody writes about: the popup flash
&lt;/h2&gt;

&lt;p&gt;This one is specific to extensions and took longer to notice than to fix.&lt;/p&gt;

&lt;p&gt;A popup is destroyed and recreated every single time the user clicks the toolbar icon. There is no warm process holding UI state. Meanwhile &lt;code&gt;onAuthStateChanged&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The fix is a synchronous-ish fast path: cache a minimal user record in &lt;code&gt;chrome.storage.local&lt;/code&gt; and read it before Firebase resolves, then let &lt;code&gt;onAuthStateChanged&lt;/code&gt; correct it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fast path: read cached state from storage before Firebase resolves async&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mp_auth_user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mp_auth_user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;StoredUser&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;authReady&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;authReady&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Subscribe to Firebase auth for live updates and token refresh&lt;/span&gt;
&lt;span class="nf"&gt;onAuthStateChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;authReady&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Sometimes the right move is to support fewer.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;MyPrompt is MIT-licensed and runs against the Firebase emulator with no account required: &lt;a href="https://github.com/It-Codes-Two/my-prompt" rel="noopener noreferrer"&gt;github.com/It-Codes-Two/my-prompt&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>firebase</category>
      <category>vue</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
