This is the abridged 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