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.
When sign-in is asked
Section titled “When sign-in is asked”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:
| moment | decided by | what happens |
|---|---|---|
| The first screen of the popup and side panel | auth.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 purchase | always | Any 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 account | signin: true on that feature in paywall.config.ts | The 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.
The sign-in form
Section titled “The sign-in form”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.
Choose your Google sign-in path
Section titled “Choose your Google sign-in path”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 flow | Offscreen popup | |
|---|---|---|
| Browsers | Chrome, Edge, Firefox | Chrome and Edge only |
| Setup | Create a Google OAuth client (about 5 console minutes) | Deploy the bundled sign-in page to 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 redirect URI embeds your extension ID, which changes if you load unpacked from a new path without the committed key file | One 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/emailPasswordResetmessages). - 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.
Set up
Section titled “Set up”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.
The manual path (no wizard)
Section titled “The manual path (no wizard)”- Firebase console: create a project.
- Add a Web App and copy its config into
apps/extension/utils/firebase.ts(theTODOmarker). Until then the extension shows “Connect your Firebase project” in place of the sign-in form. - 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.
- Set
WXT_FIREBASE_HOSTING_URL=https://<project>.firebaseapp.comandWXT_API_URL=https://us-central1-<project>.cloudfunctions.net/apiinapps/extension/.env. Sign-out calls the API’s/auth/revoke, and skips it whileWXT_API_URLis empty.
The Google OAuth client (web-auth-flow path)
Section titled “The Google OAuth client (web-auth-flow path)”- Find your extension ID. The setup wizard writes a public key to
apps/extension/extension-public-key.txtand 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://extensionsshows the same ID on the extension’s card. - 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. - 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.comin.env.
The offscreen popup path
Section titled “The offscreen popup path”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:
-
Paste your Firebase web config into
backend/firebase-hosting/public/signInWithPopup.js(theTODOmarker).pnpm create extstart --firebasewrites it for you. -
Deploy it. The wizard’s Firebase step deploys Hosting, and
pnpm firebase:deployfrombackend/functions/ships it again with the API, so after either it is already live. On its own, frombackend/firebase-hosting/:Terminal window firebase use <your-project-id>Terminal window firebase deploy --only hosting -
Set
WXT_FIREBASE_HOSTING_URL=https://<your-project-id>.firebaseapp.cominapps/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.
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:
- 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
(
linkWithCredentialon Firebase,linkIdentityon 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.
The token rules
Section titled “The token rules”Enforced by lint and architecture, not convention:
- ID tokens never leave the background. UI and content scripts send
messages (
billingCheckout,access, …); the background attaches the token to the API call. - Content scripts get proxied state only:
storage.localsnapshots and the message bus. They never import Firebase. - Single writer:
background/auth.tsis the only writer ofstorage.local.user. UI reads storage, 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. Best-effort: local sign-out proceeds even if the network call fails. - Ephemeral or 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.