Skip to content

Sign-in

The kit ships two Google sign-in paths, plus email/password and anonymous sign-in, all built for extensions. Every flow runs in the background service worker; your UI never touches Firebase, it reads who is signed in from storage.

Both paths are fully wired. One env var picks between them:

Web auth flowOffscreen popup
BrowsersChrome, Edge, FirefoxChrome and Edge only
SetupCreate a Google OAuth client (about 5 console minutes)Deploy the bundled sign-in page to Firebase Hosting
How to pick itSet WXT_GOOGLE_OAUTH_CLIENT_IDLeave WXT_GOOGLE_OAUTH_CLIENT_ID empty
Sign-in UXBrowser account chooserA small popup window
Watch out forThe redirect URI embeds your extension ID, which changes if you load unpacked from a new pathOne extra Hosting deploy; no Firefox

If unsure, use the web auth flow: it covers Firefox and is the production default. Independent of the choice:

  • Email/password always works alongside, no OAuth client needed (emailSignIn / emailSignUp / emailPasswordReset messages).
  • Anonymous-first (WXT_ANONYMOUS_AUTH=true): every install starts as a guest uid, and sign-in upgrades that uid in place, so purchases and counters survive. Turn it on when gates, counters, or purchases should work before sign-up.

Install the Firebase CLI and sign in first: npm i -g firebase-tools, then firebase login. Deploys also require the Blaze plan; the free-tier quota covers development.

Terminal window
pnpm create extstart --firebase

The wizard creates or picks a project, creates a web app, and writes its SDK config everywhere it lives: apps/extension/utils/firebase.ts, backend/firebase-hosting/public/signInWithPopup.js, both .firebaserc files, and the URLs in apps/extension/.env. It then prints a deep-linked checklist of the console steps no CLI can do (sign-in providers, Blaze plan, OAuth client). Paste the OAuth client ID when offered and it writes WXT_GOOGLE_OAUTH_CLIENT_ID too. Safe to re-run; headless flags are in the CLI reference.

  1. Firebase console: create a project.
  2. Add a Web App and copy its config into apps/extension/utils/firebase.ts (the TODO marker). Until then the extension shows “Connect your Firebase project” in every surface.
  3. Authentication → Sign-in method: enable Google and Email/Password (and Anonymous for anonymous-first). This step is manual even on the automated path.
  4. Set VITE_FIREBASE_HOSTING_URL=https://<project>.firebaseapp.com in apps/extension/.env.

The Google OAuth client (web-auth-flow path)

Section titled “The Google OAuth client (web-auth-flow path)”
  1. Load the extension once and copy its ID from chrome://extensions.
  2. Google Cloud console (same project) → Credentials → Create OAuth client → Web application → authorized redirect URI: https://<extension-id>.chromiumapp.org/.
  3. Set WXT_GOOGLE_OAUTH_CLIENT_ID=<client-id>.apps.googleusercontent.com in .env.

If WXT_GOOGLE_OAUTH_CLIENT_ID is empty, the background completes sign-in in an offscreen document that loads a page from your Firebase Hosting. That page needs your config too:

  1. Paste your Firebase web config into backend/firebase-hosting/public/signInWithPopup.js (the TODO marker). pnpm create extstart --firebase writes it for you.

  2. Deploy it, from backend/firebase-hosting/:

    Terminal window
    firebase use <your-project-id>
    firebase deploy --only hosting
  3. Set VITE_FIREBASE_HOSTING_URL=https://<your-project-id>.firebaseapp.com in apps/extension/.env. Firebase Hosting serves the reserved /__/auth/* helpers on that origin, so the page can’t run locally.

If you skip the deploy, clicking “Sign in with Google” opens a popup that closes again silently: the page still carries placeholder config, so the auth result never reaches your extension.

With WXT_ANONYMOUS_AUTH=true, every install gets a guest uid at startup:

  • Gates, counters, and purchases attribute to that uid from minute one.
  • Interactive sign-in upgrades in place: linkWithCredential keeps the uid, so entitlements and counters survive untouched.
  • Conflicts: if the credential already belongs to an account, linking fails and the strategy signs into the existing account instead. Accounts are never merged silently; the guest’s server-side data stays under the old uid.
  • Signing out returns to a fresh guest session.
  • Anonymous users count as signed out for gate identity; converting them is the sign-in wall’s job.

Enforced by lint and architecture, not convention:

  1. ID tokens never leave the background. UI and content scripts send messages (billingCheckout, gateFeature, …); the background attaches the token to the API call.
  2. Content scripts get proxied state only: storage.local snapshots and the message bus. They never import Firebase.
  3. Single writer: the background’s onAuthStateChanged is the only writer of storage.local.user. UI reads storage, so every surface shows the same state and survives service-worker restarts.
  4. Logout everywhere: sign-out calls POST /auth/revoke (revokeRefreshTokens) before clearing local state. Best-effort: local sign-out proceeds even if the network call fails.
  5. Ephemeral or token-ish data belongs in storage.session, never storage.sync.

The kit imports firebase/auth/web-extension, not firebase/auth; the standard build assumes DOM APIs a service worker doesn’t have. Every gated read awaits authStateReady(). The strategy handles both for you.