Skip to content
Get the kit

Modules

The kit ships as the full repo. The setup wizard removes what your answers rule out: the backend stack you did not pick, the payment providers you did not pick (Stripe always stays), billing and the paywall on a free extension, the side panel on a popup-only one. What makes that safe is a contract: every module carries a module.json manifest declaring everything it owns (code, npm dependencies, env vars, manifest permissions). One declaration removes all of it together.

Manifests live at the package root for workspace packages (packages/core-billing/module.json), under apps/extension/modules/<id>/module.json for app-level modules, and next to each payment provider’s seed (backend/payments/polar/module.json), validated against tooling/config/module.schema.json.

packages/core-billing/module.json
{
"$schema": "../../tooling/config/module.schema.json",
"id": "billing",
"title": "Billing & paywall",
"description": "Checkout and portal, webhook-written entitlements, and the paywall.",
"files": ["packages/core-billing/**", "packages/paywall/**", "apps/extension/paywall.config.ts"],
"dependsOn": ["auth"],
"npmDependencies": { "@extensionstart/core-billing": "workspace:*" },
"env": [{ "name": "WXT_API_URL", "description": "deployed backend API base URL" }],
"wiring": ["apps/extension/entrypoints/background/index.ts"]
}
  • files are repo-root-relative: exact files, or a whole directory as dir/**. A module can own files outside its package.
  • dependsOn is by module ID. The resolver keeps the dependencies of any kept module, even one an answer would drop, and says so. A module that depends on a backend stack (backend-firebase) is dropped when the other stack is chosen.
  • Modules the wizard never removes set "removable": false: the core packages, the website, broadcasts. One that depends on a backend stack still goes with that stack (the uninstall survey on Supabase).
  • Env vars and permissions listed by several modules are pruned only when no kept module lists them.
  • permissions is why the generated manifest shrinks when you prune: each module declares the chrome.* permissions it needs, and a smaller permission surface means a faster store review.

A module’s code often touches shared files it doesn’t own: the background import order, the messaging protocol, surface roots. Those touchpoints carry marker comments so the pruner can strip them:

  • Line marker: a // module:<id> suffix ({/* module:<id> */} in JSX, # module:<id> in YAML) removes that line when <id> is dropped.
  • Block marker: everything from module:<id>:start through module:<id>:end (inclusive) is removed. Blocks of different modules may nest.

Each manifest lists the shared files carrying its markers under wiring, and every wiring file must carry at least one of them (a test checks both directions). The pass criterion: any prune combination leaves pnpm typecheck and pnpm lint green with zero dangling imports.

Markers work line by line, and Prettier decides where lines break. A marker after a statement stays on its line, but one after a JSX opening or closing tag does not: Prettier moves it to a line of its own. To drop a wrapper and keep what it wraps, put each tag in its own block:

{/* module:billing:start */}
<PaidPlanNameProvider name={siteConfig.pricing.paidPlanName}>
{/* module:billing:end */}
<ToastProvider>{children}</ToastProvider>
{/* module:billing:start */}
</PaidPlanNameProvider>
{/* module:billing:end */}

The wizard formats every file it writes with the repo’s Prettier config once dependencies are installed, so a prune leaves no formatting diff.

The wizard strips all remaining markers at the end of its run (unless --keep-markers). A later run keeps any module whose wiring lost its markers, and says so: dropping it then would delete its files and leave its imports behind.

The CI workflow (.github/workflows/ci.yml) is wiring too: its backend steps sit in backend-firebase and backend-supabase blocks, so your clone runs only the checks of the stack it ships. After pruning, the wizard also refreshes pnpm-lock.yaml (CI installs with --frozen-lockfile), so your first push builds green.

A dropped feature module leaves its backend files in place: the Hono app is one self-contained function and unused routes are harmless. Delete them manually if you want a minimal backend. Two kinds of module are the exception. The stack you do not choose goes whole (backend/functions with backend-firebase, backend/supabase with backend-supabase), feature routes included. A payment provider you do not choose (payments-polar, payments-paddle) takes its adapter, its contract test, its seed script, and its Supabase copy along, and its lines in the provider switch carry its markers; the seed:* script that ran its seed leaves backend/functions/package.json with it.

A module of your own records what your feature owns. The wizard keeps it on every run; the manifest is there so each permission, env var, and test has an owner (CI checks that), and so your store listing’s permission justifications can say which feature needs what.

  1. Create the manifest (apps/extension/modules/<id>/module.json for an app-level feature) with id, title, description, and files globs for everything the module owns.
  2. Declare npmDependencies, env (a WXT_ name must exist in apps/extension/.env.example), permissions, and dependsOn.
  3. Keep the manifest in sync: adding a file, dependency, env var, or permission means updating module.json in the same change.

Wiring markers are optional for your own module: nothing prunes it.