Skip to content
Get the kit

Authentication

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.

Sign-in is never required to use a free feature. The kit asks for an account at three moments, and you decide the first one:

momentdecided bywhat happens
The first screen of the popup and side panelauth.signIn in site.config.ts (the wizard’s “Sign-in” question, --sign-in)"when-needed" (default): the hub opens on Home with its tabs, and the Account tab is the sign-in form. "first-screen": the sign-in form is the whole surface until there is an account, your feature below it.
In front of a purchasealwaysAny wall shown to a signed-out user chains to the sign-in wall first, because checkout needs an account to attach the entitlement to. Dismissing it also dismisses the wall behind it.
A feature that needs an accountsignin: true on that feature in paywall.config.tsThe click raises the sign-in wall; after sign-in the feature runs. Sync and anything stored per user belong here.

Anonymous guests (WXT_ANONYMOUS_AUTH=true) count as signed out at all three moments; signing in links the guest into the account, so nothing is lost. There is no fourth place: do not add a standalone sign-in gate in front of a free feature, because store reviewers must be able to exercise the listed functionality on a fresh install.

components/account/SignInView.tsx offers Google first, then email and password. It opens on Create account, because whoever meets it in a freshly installed extension is almost always new; Sign in is one click away, and Continue with Google works for both. Chrome’s password manager neither saves nor fills passwords on extension pages (the popup and the side panel), so Google is the one-click way back in. The fields still carry the standard autocomplete values.

Forgot password? works on both stacks, in two different ways:

  • Firebase emails a link to a reset page Firebase hosts; the user sets the new password there and signs in again.
  • Supabase has no hosted page, so its reset email carries a 6-digit code instead of a link. A second step (components/account/ResetCodeForm.tsx, Supabase clones only) asks for the code and the new password, and the user is signed in when it is saved. The wizard’s Supabase step sets the email up (manual path). The form never says whether an email has an account.

On Firebase both paths are fully wired, and one env var picks between them (Supabase has one path, the web auth flow through your project’s Google provider):

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 path without the committed key fileOne 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 the usage the server recorded for the guest stays with the account. Turn it on when usage should belong to an account before sign-up. Checkout still asks a guest to sign in first.

The wizard’s Firebase step does the setup and prints the console checklist: the sign-in providers, the Blaze plan, and the Google OAuth client, each with a link into the right screen (what it asks and prints). On Supabase, the wizard’s Supabase step does the same and leaves one item, the Google OAuth client (what it asks and prints). Two alternatives live here.

  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 place of the sign-in form.
  3. Authentication: click Get started the first time (a new project shows it instead of the provider list), then on the Sign-in method tab enable Google and Email/Password (and Anonymous for anonymous-first). On the automated path the wizard switches on Email/Password (and Anonymous) itself; Google stays a console step.
  4. Set WXT_FIREBASE_HOSTING_URL=https://<project>.firebaseapp.com and WXT_API_URL=https://us-central1-<project>.cloudfunctions.net/api in apps/extension/.env. Sign-out calls the API’s /auth/revoke, and skips it while WXT_API_URL is empty.

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

Section titled “The Google OAuth client (web-auth-flow path)”
  1. Find your extension ID. The setup wizard writes a public key to apps/extension/extension-public-key.txt and prints the ID with the full redirect URI. The key fixes the ID, so every clone, teammate, and unpacked build shares it; commit the file. chrome://extensions shows the same ID on the extension’s card.
  2. Google Cloud console (same project) → Credentials. Enabling Google sign-in in Firebase already created Web client (auto created by Google Service) there. Open it and add an authorized redirect URI: https://<extension-id>.chromiumapp.org/. If no web client exists, create one (Create OAuth client → Web application) with that URI.
  3. Copy the client ID (top of the client’s page, or in Firebase under Authentication → Sign-in method → Google → Web SDK configuration) and 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 (/auth; your website takes the root). This path is what the offscreen permission is for: a build with the client ID set never uses it, so wxt.config.ts leaves the permission out of that build’s manifest, and the store asks you to justify one permission less. The 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. The wizard’s Firebase step deploys Hosting, and pnpm firebase:deploy from backend/functions/ ships it again with the API, so after either it is already live. On its own, from backend/firebase-hosting/:

    Terminal window
    firebase use <your-project-id>
    Terminal window
    firebase deploy --only hosting
  3. Set WXT_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 “Continue 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:

  • Paywall usage attributes to that uid from minute one. A purchase needs sign-in first, and sign-in keeps the same uid.
  • Interactive sign-in upgrades in place: linking keeps the uid (linkWithCredential on Firebase, linkIdentity on Supabase), 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 the paywall; 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, access, …); 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: background/auth.ts 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.