# 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

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.

## Setting up your Firebase project

:::note[Before you deploy anything]
Install the Firebase CLI and sign in: `npm i -g firebase-tools`, then `firebase login`. Deploys (Functions, Hosting) also require your project on the **Blaze plan**; Cloud Functions v2 won't deploy on Spark. The free-tier quota covers development.
:::

### The automated path (recommended)

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

```sh
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](/reference/cli/) for `--firebase-project` / `--firebase-create` (headless).

### The manual path (what the script automates)

1. [Firebase console](https://console.firebase.google.com) → 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)

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`.

:::caution
The redirect URI embeds your extension ID, which changes if you load unpacked from a different path. If sign-in suddenly opens and closes with an error, re-check that the URI matches the *current* ID.
:::

### 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:

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/`:

   ```sh
   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.

## 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**: `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.

## The token rules

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.
