Skip to content

Build your first feature

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.)

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.

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:

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”

File 1: apps/extension/entrypoints/background/gates.ts. Add your feature id to the list at the top:

/** The demo premium feature (see GateDemo) — replace with your real ones. */
export const PREMIUM_FEATURES = ["premium-demo"];

becomes

export const PREMIUM_FEATURES = ["premium-demo", "export-pdf"];

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

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.

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 <GateOverlay> 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.
  • 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.

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.

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 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.