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.
Choose your Google sign-in path
Section titled “Choose your Google sign-in path”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/emailPasswordResetmessages). - 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.
Setting up your Firebase project
Section titled “Setting up your Firebase project”The automated path (recommended)
Section titled “The automated path (recommended)”The setup wizard does the whole CLI-automatable half for you:
pnpm create extstart --firebaseIt 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)”- Firebase console → create a project.
- Add a Web App and copy its config into
apps/extension/utils/firebase.ts(theTODOmarker). The checked-in config isPASTE_YOUR_…placeholders; the extension boots without them but shows “Connect your Firebase project” in every surface until they’re replaced. - 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.)
- Set
VITE_FIREBASE_HOSTING_URL=https://<project>.firebaseapp.cominapps/extension/.env.
The Google OAuth client (web-auth-flow path)
Section titled “The Google OAuth client (web-auth-flow path)”- Load the extension once and copy its ID from
chrome://extensions. - Google Cloud console (same project) → Credentials → Create OAuth client → Web application → authorized redirect URI:
https://<extension-id>.chromiumapp.org/. - Put the client ID in
.env:WXT_GOOGLE_OAUTH_CLIENT_ID=<client-id>.apps.googleusercontent.com.
The offscreen popup path
Section titled “The offscreen popup path”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:
-
Paste your Firebase web config into
backend/firebase-hosting/public/signInWithPopup.js(theTODOmarker; same config as step 2 above).pnpm create extstart --firebasewrites it for you. -
Deploy it to your project’s Hosting, from
backend/firebase-hosting/:Terminal window firebase use <your-project-id>firebase deploy --only hosting -
Set
VITE_FIREBASE_HOSTING_URL=https://<your-project-id>.firebaseapp.cominapps/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.
Anonymous-first: how linking behaves
Section titled “Anonymous-first: how linking behaves”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:
linkWithCredentialkeeps 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.
The token rules
Section titled “The token rules”These are enforced by lint and architecture, not convention:
- 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. - Content scripts get proxied state only:
storage.localsnapshots (user,entitlements) and the message bus. They never import Firebase. - Single writer: the background’s
onAuthStateChangedis the only writer ofstorage.local.user. UI reads storage, never Firebase directly, so every surface shows the same state and survives service-worker restarts. - 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. - Ephemeral/token-ish data belongs in
storage.session, neverstorage.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.
