Skip to content
Get the kit

Project structure

ExtensionStart is a pnpm workspace monorepo. Here’s the map, top to bottom.

WXT + React 19 + Tailwind 4 + Firebase or Supabase. 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 → the backend stack (#backend: its auth strategy and the broadcasts mirror) → auth → entitlements → billing → prices → paywall → uninstall. All event listeners are registered at the top level (why that matters).
  • Surfaces: popup and sidepanel mount the same Home | Account | Settings hub (no separate options page); content, while the clone keeps the on-page demo, holds the shadow-DOM UI: the status card and the paywall on every https page; offscreen is the auth fallback. Each page is described in Extension pages.
  • utils/: the typed messaging protocol, settings store, shadow-UI mount, DOM observer, 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.

A static Astro site: the landing page, the help center, privacy and terms templates, the changelog, and the /welcome/ page the extension opens on install (urls.welcome). You rebrand it in src/site.ts (Website and help center).

Each package is a framework-free core with a thin React subpath (/react) where UI bindings exist; ui is the React component kit itself. Lint enforces importing only public entrypoints.

packagewhat it is
core-extdefineStore / defineStorageView / defineMessaging / defineAlarm / defineMigrations (the MV3 survival kit) plus a /testing chrome mock
core-authauth strategies (web-auth-flow default, offscreen fallback), anonymous-first linking, typed auth errors
core-billingentitlement snapshot + states matrix, billing API client, storage-backed hooks (useEntitlement('paid'))
paywallthe three-question config (features / trial / prompt), the pure evaluator (limits, trials, prompt cooldown, sign-in chaining), and the Paywall / LockBadge / UsageMeter / TrialBanner UI (/react)
uiMaterial 3 primitives: Button/Card/TextField/Modal/ListItem/Badge/Skeleton/Toast (CVA variants over Base UI), plus the color palettes (palettes/) and fonts

A single Hono app on Cloud Functions v2, deliberately self-contained: Firebase packs it standalone, so it can’t import workspace TypeScript. Routes: /health, /billing/checkout, /billing/portal, /billing/prices, /billing/webhook, /credits/consume, /credits/balance, /gate/events (paywall usage mirror), /auth/revoke, and the Firebase-only /uninstall survey (in src/index.ts). The shared routes live in src/routes.ts, over small ports (token check, entitlement store, credit spend, usage, sign-out) that src/index.ts wires to Firestore and Firebase Auth; the Supabase backend (backend/supabase) serves a generated copy of that file on its own ports. The billing core is port-based too (BillingAdapter / EntitlementStore / ClaimsWriter), so Stripe, Polar, and Paddle implement the same contract.

  • create/: the create-extstart wizard (reference).
  • config/: shared ESLint and tsconfig presets and the module.schema.json that validates module manifests.

Every feature module carries a module.json manifest declaring its files, dependencies, env vars, and manifest permissions. The wizard’s pruner consumes these: removing a module (the other backend stack, billing on a free extension, the side panel) removes its code, deps, env entries, and permissions together. Details in the module system guide.

From the repo root:

  • pnpm build: build everything
  • pnpm typecheck: TypeScript, strict, workspace-wide
  • pnpm lint: ESLint, workspace-wide
  • pnpm format: Prettier over code, JSON, CSS and YAML (CI runs pnpm format:check)
  • pnpm test: unit tests

Each runs that script in every workspace package (pnpm -r).

Extension-specific root aliases (the long form pnpm --filter @extensionstart/extension <script> always works too):

  • pnpm dev: dev build + watch
  • pnpm e2e: Playwright against the built extension
  • pnpm zip: store-ready zip (also zip:firefox, zip:edge)
  • pnpm audit:remote-code: scan the built output

Backend (from backend/functions/): pnpm serve (emulators), pnpm firebase:deploy, pnpm seed:stripe, pnpm stripe:webhook, pnpm run doctor, pnpm test, pnpm test:rules, pnpm test:lifecycle. On the Supabase stack, the function’s tests run with Deno from the repo root: DENO_NO_PACKAGE_JSON=1 deno test backend/supabase/tests.

You rarely edit these. Each has one job:

  • package.json, pnpm-workspace.yaml, pnpm-lock.yaml: the pnpm workspace (root scripts, package folders, pinned versions, install safety settings).
  • .prettierignore, .editorconfig: formatting (the settings are the prettier key in package.json).
  • eslint.config.js: lint rules for packages/.
  • .nvmrc: the Node version.
  • .gitignore: what git never tracks, such as build output and .env files.
  • AGENTS.md, CLAUDE.md, .claude/commands/, .mcp.json: instructions and recipes for coding agents (AI agents guide).
  • .github/workflows/: CI on every push (ci.yml), the store release on a version tag (release.yml), and the store upload you start by hand with that tag (publish-to-stores.yml). The release setup and its secrets are in the /prep-store-submission recipe.

The setup wizard removes what your answers rule out, so a configured clone carries only its own backend stack, and billing and the side panel only when you chose them. AGENTS.md and the recipes are pruned the same way, so they describe the files you have.