This is the full developer documentation for ExtensionStart
# ExtensionStart Docs
> From purchase to a published extension. Sign-in, payments, paywalls, and store runbooks, documented end to end.
[Start here](/getting-started/what-you-get/)Run one wizard, then follow the 5-step journey from clone to a live, monetized listing.
[Publish to the Chrome Web Store](/publishing/first-submission/)The submission runbook: developer account, privacy tab, permissions, review expectations.
## Download docs for your AI agent
[Section titled “Download docs for your AI agent”](#download-docs-for-your-ai-agent)
If you’re working with Claude, Cursor, or another coding agent, give it the whole documentation set as context:
* [extensionstart-docs.md](/downloads/extensionstart-docs.md): every page in one concatenated Markdown file
* [extensionstart-docs.zip](/downloads/extensionstart-docs.zip): the individual Markdown files, zipped
* [llms.txt](/llms.txt) · [llms-full.txt](/llms-full.txt) · [llms-small.txt](/llms-small.txt): machine-readable indexes following the llms.txt convention
Every page is also available as raw Markdown by appending `.md` to its URL (for example [/guides/auth.md](/guides/auth.md)), or via the “Copy as Markdown” button next to each page title.
# Connect sign-in
> Step 2 of 5. Point the kit at your own Firebase project and sign in for real.
Step 2 of 5. Point the kit at your own Firebase project so Google and email sign-in work for real. Each step here is one command or one console screen; the [Sign-in guide](/guides/auth/) owns the detail.
## The checklist
[Section titled “The checklist”](#the-checklist)
1. **Install the Firebase CLI and log in**: `npm i -g firebase-tools`, then `firebase login`.
2. **Run the Firebase setup:**
```sh
pnpm create extstart --firebase
```
It creates or picks a project, writes the SDK config into every file that carries it, and prints deep links for the console steps below ([what it writes](/guides/auth/#the-automated-path-recommended)).
3. **Enable the sign-in providers**: Google and Email/Password (and Anonymous, if you use anonymous-first) in the console screen the wizard linked ([details](/guides/auth/#the-manual-path-what-the-script-automates)).
4. **Create the Google OAuth client** and paste its ID as `WXT_GOOGLE_OAUTH_CLIENT_ID` when the wizard offers ([redirect-URI steps](/guides/auth/#the-google-oauth-client-web-auth-flow-path)).
5. **Upgrade the project to the Blaze plan**: step 4 of the journey deploys a Cloud Function, which Spark can’t do. The free-tier quota covers development ([details](/guides/auth/#setting-up-your-firebase-project)).
6. **Rebuild and reload**: restart `pnpm dev` (env values are baked in at build time), then reload the extension on `chrome://extensions`.
## Verify
[Section titled “Verify”](#verify)
Open the popup and click **Sign in with Google**. The “Connect your Firebase project” notice is gone, the OAuth window completes, and your account shows in the Account tab.
If the sign-in window opens and closes without signing you in, the OAuth redirect URI doesn’t match your current extension ID; see [the fix](/guides/auth/#the-google-oauth-client-web-auth-flow-path). To skip the OAuth client entirely, use the [offscreen popup path](/guides/auth/#the-offscreen-popup-path) instead (Chromium only, zero OAuth config); the [sign-in guide](/guides/auth/#choose-your-google-sign-in-path) compares the two paths.
Once you’re signed in, continue to **[3. Take payments](/getting-started/take-payments/)**: seed Stripe and watch that account become a paying customer.
# Build your first feature
> Where your code goes, and the 2-file recipe that makes a feature paid.
Step 4 of 5. Setup is done: the extension runs, sign-in works, payments clear. This page is where *your* product starts: where your code goes, and the **2-file edit** that makes it paid. (Build here any time; nothing before this step depends on it.)
## Where your code goes
[Section titled “Where your code goes”](#where-your-code-goes)
The popup renders a card titled **“Your feature goes here”**. That’s `apps/extension/components/YourFeature.tsx`, a small, heavily commented component that is yours to gut:
* Rename it freely (update the import in `apps/extension/entrypoints/popup/main.tsx`).
* Replace its two buttons with your real UI. Keep the primitives from `@extensionstart/ui` and the token color scales; you get both themes and consistent styling for free.
* Talk to the background only through the `sendMessage` helper from `@/utils/messaging` (typed, so wrong payloads won’t compile), never raw `chrome.runtime.sendMessage`.
Unlike the demos (below), `YourFeature.tsx` is core: the wizard never prunes it.
## Make an action count toward the paywall
[Section titled “Make an action count toward the paywall”](#make-an-action-count-toward-the-paywall)
Free actions should still *count*: the default `value-first` preset raises the paywall on the 10th recorded action. Record one line, fire-and-forget, after your feature does its work:
```ts
sendMessage("gateAction", { name: "your-free-action" }).catch(console.error);
```
That’s the free button in `YourFeature.tsx`, verbatim minus the error message.
## Make a feature premium – the 2-file recipe
[Section titled “Make a feature premium – the 2-file recipe”](#make-a-feature-premium--the-2-file-recipe)
**File 1: `apps/extension/entrypoints/background/gates.ts`.** Add your feature id to the list at the top:
```ts
/** The demo premium feature (see GateDemo) — replace with your real ones. */
export const PREMIUM_FEATURES = ["premium-demo"];
```
becomes
```ts
export const PREMIUM_FEATURES = ["premium-demo", "export-pdf"];
```
**File 2: your call site.** Ask the gate engine *before* running the feature:
```ts
const decision = await sendMessage("gateFeature", { feature: "export-pdf" });
if (decision !== null) return; // the wall is already rendering — stop
// …run the premium feature…
```
Done. The premium button in `YourFeature.tsx` does exactly this with the `"premium-demo"` id, so it gates out of the box; swap in your own id once File 1 lists it.
## What happens automatically
[Section titled “What happens automatically”](#what-happens-automatically)
You never build wall UI. The engine handles the rest:
* **The wall renders itself.** The background publishes the decision to `storage.local.gateDecision`; the already-mounted `` shows the same wall in the popup, sidepanel, and content-script surfaces.
* **Dismissals cool down.** A dismissed paywall stays quiet for 24 hours (`cooldownMinutes: 60 * 24` in `gates.ts`; every timing number lives in that one file).
* **Sign-in chains.** Signed-out and anonymous users see the sign-in wall first, then the paywall. Sign-in never fires standalone, which is [CWS-policy-safe by design](/guides/gates/).
* **Usage mirrors to the server.** Recorded events flush to `POST /gate/events` every minute, keeping uid-keyed counters in Firestore. Client-side counts can be wiped or forged.
## Delete the demos when ready
[Section titled “Delete the demos when ready”](#delete-the-demos-when-ready)
Two demos exist purely to show the paths above:
* **GateDemo** (`apps/extension/components/GateDemo.tsx`): the “Try a premium feature” button. It ships with the `gate` module, so don’t prune that module to remove it; you’d delete your paywall engine too. Once your own feature calls `gateFeature`, delete the file and its two `GateDemo` lines in `entrypoints/popup/main.tsx`.
* **Highlighter**: the content-script demo. This one the wizard *can* prune: it’s the `content-demo` module. Rerun `pnpm create extstart` or see the [module guide](/guides/modules/).
## Next steps
[Section titled “Next steps”](#next-steps)
Your feature is gated, and sign-in and payments are already wired from steps 2–3, so the wall can actually convert. Next, [5. Ship it](/publishing/first-submission/) covers submitting to the Chrome Web Store.
For the gate engine’s full surface (presets, timing knobs, policy guardrails, manual wall control), see the [Paywalls guide](/guides/gates/).
# Repo tour
> The monorepo map: apps, packages, backend, and how they fit together.
ExtensionStart is a pnpm + Turborepo monorepo. Here’s the map, top to bottom.
## `apps/extension` – the extension app
[Section titled “apps/extension – the extension app”](#appsextension--the-extension-app)
WXT + React 19 + Tailwind 4 + Firebase. WXT **generates** `manifest.json` from `wxt.config.ts` per browser; never hand-edit a manifest.
* `entrypoints/background/`: one module per concern, imported by `index.ts` in a fixed order: `migrations` → `errors` → `logs` → `firebase` (auth) → `billing` → `gates` → `broadcasts` → `update-notice`. All event listeners are registered at the top level ([why that matters](/guides/background/)).
* Surfaces: `popup` and `sidepanel` mount the same account/settings tabs (no separate options page); `welcome` is the first-run tour; `content` holds the shadow-DOM UI: status bar, gate walls, highlighter demo; `offscreen` is the auth fallback. Demo surfaces `newtab` and `devtools` build only with `WXT_DEMO_SURFACES=true`.
* `utils/`: the typed messaging protocol, settings store, shadow-UI mount, DOM observer, highlight persistence, error reporting, page bridge.
* `hooks/`: `useAuth`, `useBilling`, `useTheme`.
* `site.config.ts`: every user-facing name, email, URL, and pricing-copy string. Rebranding starts (and mostly ends) here.
## `packages/` – framework-free cores
[Section titled “packages/ – framework-free cores”](#packages--framework-free-cores)
Each package is a framework-free core with a thin React subpath (`/react`) where UI bindings exist. Lint enforces importing only public entrypoints.
| package | what it is |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `core-ext` | `defineStore` / `defineStorageView` / `defineMessaging` / `defineProxyService` / `defineAlarm` / `defineMigrations` (the MV3 survival kit) plus a `/testing` chrome mock |
| `core-auth` | auth strategies (web-auth-flow default, offscreen fallback), anonymous-first linking, typed auth errors |
| `core-billing` | entitlement snapshot + states matrix, billing API client, storage-backed hooks (`useEntitlement('paid')`) |
| `gate` | trigger primitives and combinators, the gate evaluator (cooldowns/escalation/chaining), presets, and the `GateWall` UI (`/react`) |
| `ui` | Button/Card/Input/Badge/Skeleton/Dialog/Toast (CVA variants over Base UI) |
## `backend/functions` – one Hono app
[Section titled “backend/functions – one Hono app”](#backendfunctions--one-hono-app)
A single Hono app on Cloud Functions v2, deliberately self-contained: Firebase packs it standalone, so it can’t import workspace TypeScript. Routes: `/gateConfig`, `/billing/checkout`, `/billing/portal`, `/billing/webhook`, `/gate/events`, `/auth/revoke`, `/errors`. The billing core is port-based (StripeGateway / EntitlementStore / ClaimsWriter) so alternative payment providers can implement the same contract.
## `tooling/`
[Section titled “tooling/”](#tooling)
* `create/`: the `create-extstart` wizard ([reference](/reference/cli/)).
* `config/`: shared ESLint/Prettier/tsconfig presets and the `module.schema.json` that validates module manifests.
## `module.json` everywhere
[Section titled “module.json everywhere”](#modulejson-everywhere)
Every prunable feature module carries a `module.json` manifest declaring its files, dependencies, env vars, and manifest permissions. The wizard’s pruner consumes these: dropping a module removes its code, deps, env entries, and permissions together. Details in the [module system guide](/guides/modules/).
## Commands you’ll use
[Section titled “Commands you’ll use”](#commands-youll-use)
From the repo root:
```sh
pnpm build # build everything
pnpm typecheck # TypeScript, strict, workspace-wide
pnpm lint # ESLint, workspace-wide
pnpm turbo run test # unit tests
```
Extension-specific root aliases (the long form `pnpm --filter @extensionstart/extension