Skip to content
Get the kit

Build your feature

This guide shows you where your code goes and the 2-file edit that makes a feature paid. Build here any time; nothing before this step depends on it.

The popup renders a card titled “Your feature goes here”. That is apps/extension/components/YourFeature.tsx, a small commented component that is yours to gut. Your first feature takes its place; every later feature is a new component next to it, modeled on the first (the add-feature recipe in .claude/commands/ walks both).

  • Rename it to your feature’s name in three places: the file, its import in apps/extension/components/HubPage.tsx, and every module.json that lists it (grep -rl YourFeature --include=module.json .). Until the manifests match, pnpm test fails and names the entry to change.
  • Replace its two buttons with your real UI. Keep the primitives from @extensionstart/ui and the color roles (bg-background, text-foreground, and the rest); both themes come free.
  • Talk to the background only through sendMessage from @/utils/messaging. It is typed, so wrong payloads won’t compile.

Unlike the demos, YourFeature.tsx is core: the wizard never prunes it.

The default freemium recipe lists the core action as { free: 10, per: "ever" }: ten free uses for good. The wall follows the tenth use and every click after it, with Pro removing the limit. Ask before the work, exactly like a Pro feature:

const { allowed } = await sendMessage("access", { feature: "your-free-action" });
if (!allowed) return; // the allowance is spent; the wall is up

This is the free button’s call in YourFeature.tsx, and <UsageMeter feature="your-free-action" /> inside it shows “7/10” with a progress line along the button’s edge (the tooltip and screen readers get “7 of 10 free uses left”). A feature listed as "free" runs every time; the same call just counts it, so fire and forget it after the work.

File 1. List the feature and what it costs:

apps/extension/paywall.config.ts
features: {
"your-free-action": { free: 10, per: "ever" },
"export-pdf": "pro", // Pro only
// or a free allowance: summarize: { free: 5, per: "day" }
// or credits: "ai-image": { credits: 5 }
},

File 2: your call site. Ask the engine before running the feature:

const { allowed } = await sendMessage("access", { feature: "export-pdf" });
if (!allowed) return; // the wall is already rendering; stop
// …run the feature…

The premium button in YourFeature.tsx does exactly this with the "premium-demo" id, so you can compare against a working example. Put <LockBadge feature="export-pdf" /> inside the control (a lock and the paid plan’s name, pricing.paidPlanName in site.config.ts), so users see it is paid before they click. For a limited feature, <UsageMeter feature="summarize" /> inside the control shows “3/5 today” with a progress line along its edge. Give a control that holds the meter relative overflow-hidden.

  • The wall renders itself in the popup, the side panel, and the on-page demo, with copy for the reason (Pro feature, limit reached, out of credits, sign in); you never build wall UI.
  • One call, one verdict. access checks, counts free uses, and consumes credits before the work. Feature ids are typed from the config.
  • Sign-in chains. Signed-out users see the sign-in wall first, then the paywall. Sign-in never fires standalone, which keeps you inside store policy.
  • Usage mirrors to the server. Accesses queue locally and flush to POST /gate/events once 25 are queued or the oldest is an hour old; anything with stakes, such as credits or a card trial, is enforced on the server, not by client counters.
  • YourFeature is the seed, not a demo: rename it and replace its two buttons with your real UI. The access calls it carries are the entire paywall integration.
  • The on-page status card (components/content/StatusCard.tsx, while your clone has it) calls the same two demo ids from every https:// page. Put your feature’s on-page work behind its access call, or remove the whole on-page demo if your product has no on-page UI: commit, then run pnpm create extstart --on-page no --yes. It deletes the content script, the card and their e2e spec, and the extension then asks for no access to websites (see On-page UI).
  • The demo ids (your-free-action, premium-demo) go from paywall.config.ts once nothing calls them. The e2e specs click the demo’s buttons through FREE_ACTION and PREMIUM_BUTTON in apps/extension/e2e/fixtures.ts: point them at your own core and paid controls, and rewrite or delete the specs that only show the demo. A wall in a spec is modals(page) from the same file, so a snackbar your feature shows never counts as one.

For recipes, trials, limits, and manual wall control, see the paywall guide.