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.)
Where your code goes
Section titled “Where your code goes”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/uiand the token color scales; you get both themes and consistent styling for free. - Talk to the background only through the
sendMessagehelper from@/utils/messaging(typed, so wrong payloads won’t compile), never rawchrome.runtime.sendMessage.
Unlike the demos (below), YourFeature.tsx is core: the wizard never
prunes it.
Make an action count toward the paywall
Section titled “Make an action count toward the paywall”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.
What happens automatically
Section titled “What happens automatically”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 * 24ingates.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/eventsevery minute, keeping uid-keyed counters in Firestore. Client-side counts can be wiped or forged.
Delete the demos when ready
Section titled “Delete the demos when ready”Two demos exist purely to show the paths above:
- GateDemo (
apps/extension/components/GateDemo.tsx): the “Try a premium feature” button. It ships with thegatemodule, so don’t prune that module to remove it; you’d delete your paywall engine too. Once your own feature callsgateFeature, delete the file and its twoGateDemolines inentrypoints/popup/main.tsx. - Highlighter: the content-script demo. This one the wizard can
prune: it’s the
content-demomodule. Rerunpnpm create extstartor see the module guide.
Next steps
Section titled “Next steps”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.
