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 just reads who’s signed in.

Both paths are fully wired. You pick one with a single env var:

Web auth flow Offscreen popup
Browsers Chrome, Edge, and Firefox Chrome and Edge only
Setup Create a Google OAuth client (about 5 console minutes) Deploy the bundled sign-in page to your Firebase Hosting
How to pick it Set WXT_GOOGLE_OAUTH_CLIENT_ID Leave WXT_GOOGLE_OAUTH_CLIENT_ID empty
Sign-in UX Browser account chooser A small popup window
Watch out for The OAuth redirect URI embeds your extension ID, which changes if you load unpacked from a new path One extra Hosting deploy; no Firefox
Best for Shipping to real users, multi-browser products Getting started fast, Chromium-only products

We recommend the web auth flow for production (it covers Firefox and skips popup UX), but the choice is yours; many Chromium-only products ship the offscreen path permanently. Under the hood: web auth flow uses chrome.identity.launchWebAuthFlow with the implicit OAuth flow (plus a getAuthToken fast path on Chrome, never an OAuth client secret); the offscreen path opens your Firebase Hosting page (VITE_FIREBASE_HOSTING_URL) in an offscreen document and completes sign-in there.

Independent of that choice:

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

The setup wizard does the whole CLI-automatable half for you:

Terminal window
pnpm create extstart --firebase

It creates (or picks) a Firebase project, creates a web app, fetches its SDK config, and writes it everywhere it lives: apps/extension/utils/firebase.ts, backend/firebase-hosting/public/signInWithPopup.js, both .firebaserc files, and VITE_FIREBASE_HOSTING_URL / WXT_API_URL in apps/extension/.env. It then prints a deep-linked checklist of the steps no CLI can do (enable the sign-in providers, upgrade to Blaze, create the OAuth client). Paste the client id when offered and it writes WXT_GOOGLE_OAUTH_CLIENT_ID. Safe to re-run; see the CLI reference for --firebase-project / --firebase-create (headless).

The manual path (what the script automates)

Section titled “The manual path (what the script automates)”
  1. Firebase console → create a project.
  2. Add a Web App and copy its config into apps/extension/utils/firebase.ts (the TODO marker). The checked-in config is PASTE_YOUR_… placeholders; the extension boots without them but shows “Connect your Firebase project” in every surface until they’re replaced.
  3. Authentication → Sign-in method: enable Google and Email/Password (and Anonymous if you use anonymous-first). (This step is manual even on the automated path; the wizard deep-links you to the right console screen.)
  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. Put the client ID in .env: WXT_GOOGLE_OAUTH_CLIENT_ID=<client-id>.apps.googleusercontent.com.

If you chose the offscreen path (WXT_GOOGLE_OAUTH_CLIENT_ID left empty), the background opens an offscreen document that loads the page at VITE_FIREBASE_HOSTING_URL and completes sign-in there. That page lives at backend/firebase-hosting/public/signInWithPopup.js and needs your config too:

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

  2. Deploy it to your project’s Hosting, 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, which is why the page can’t just be opened locally.

Failure symptom if you skip this: clicking “Sign in with Google” opens a popup that closes again silently and you stay signed out. The iframe is still pointing at a page with placeholder config (or a Firebase project that isn’t yours), so the auth result never reaches your extension.

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

  • Gates, usage counters, and even purchases attribute to that uid from minute one, with no forced sign-up.
  • Interactive sign-in upgrades in place: linkWithCredential keeps the uid, so entitlements (customers/{uid}) and counters (usage/{uid}) survive untouched.
  • Conflicts: if the Google credential or email already belongs to an account, linking fails and the strategy signs into the existing account instead. The guest session’s server-side data stays behind under the old uid; accounts are never merged silently. Email sign-up surfaces “email in use” and the UI steers to sign-in.
  • Signing out returns to a fresh guest session.
  • Anonymous users see the sign-in surface and count as signed out for gate identity. Converting them is the sign-in wall’s job.

These are enforced by lint and architecture, not convention:

  1. ID tokens never leave the background. UI and content scripts have no getIdToken; backend calls go through background messages (billingCheckout, gateFeature, …) and the background attaches the token.
  2. Content scripts get proxied state only: storage.local snapshots (user, entitlements) 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, never Firebase directly, 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, so sessions on other devices end when their current ID tokens expire (≤1 hour). It’s best-effort: local sign-out proceeds even if the network call fails.
  5. Ephemeral/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.