Project structure
ExtensionStart is a pnpm workspace monorepo. Here’s the map, top to bottom.
apps/extension: the extension app
Section titled “apps/extension: the extension app”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 byindex.tsin a fixed order:migrations→ the backend stack (#backend: its auth strategy and thebroadcastsmirror) →auth→entitlements→billing→prices→paywall→uninstall. All event listeners are registered at the top level (why that matters).- Surfaces:
popupandsidepanelmount 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;offscreenis 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.
apps/site: your website
Section titled “apps/site: your website”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).
packages/: framework-free cores
Section titled “packages/: framework-free cores”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.
| package | what it is |
|---|---|
core-ext | defineStore / defineStorageView / defineMessaging / 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')) |
paywall | the three-question config (features / trial / prompt), the pure evaluator (limits, trials, prompt cooldown, sign-in chaining), and the Paywall / LockBadge / UsageMeter / TrialBanner UI (/react) |
ui | Material 3 primitives: Button/Card/TextField/Modal/ListItem/Badge/Skeleton/Toast (CVA variants over Base UI), plus the color palettes (palettes/) and fonts |
backend/functions: one Hono app
Section titled “backend/functions: 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: /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.
tooling/
Section titled “tooling/”create/: thecreate-extstartwizard (reference).config/: shared ESLint and tsconfig presets and themodule.schema.jsonthat validates module manifests.
module.json everywhere
Section titled “module.json everywhere”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.
Commands you’ll use
Section titled “Commands you’ll use”From the repo root:
pnpm build: build everythingpnpm typecheck: TypeScript, strict, workspace-widepnpm lint: ESLint, workspace-widepnpm format: Prettier over code, JSON, CSS and YAML (CI runspnpm 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 + watchpnpm e2e: Playwright against the built extensionpnpm zip: store-ready zip (alsozip: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.
The files at the root
Section titled “The files at the root”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 theprettierkey inpackage.json).eslint.config.js: lint rules forpackages/..nvmrc: the Node version..gitignore: what git never tracks, such as build output and.envfiles.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-submissionrecipe.
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.