Paywalls & free limits
Pick a preset and list your premium features; the same wall then renders
in every surface, and you never build wall UI. Every timing number lives
in one file: apps/extension/entrypoints/background/gates.ts.
Pick a preset
Section titled “Pick a preset”| preset | paywall appears | use when |
|---|---|---|
value-first (default) | premium-feature moment, or after 10 actions / 3 days | most products |
day-zero | first session (dismissible) | strong day-0 conversion focus |
metered | credit balance exhausted (+ premium moments) | usage products with credit billing |
silent | only when you call open("paywall") | maximum review safety |
The wizard writes your preset into background/gates.ts; switching later
is a one-line edit:
defineGates({ gates: valueFirst({ premiumFeatures: ["export-pdf"], paywallAfterActions: 10 }),})Every preset except silent accepts: premiumFeatures (feature IDs that
raise the paywall immediately), paywallAfterActions (default 10),
paywallAfterDays (default 3), cooldownMinutes (quiet period after a
dismissal, default 24 h), and escalateAfterDismissals (default 0 =
never; leave it that way for core features). The sign-in wall never fires
on its own in any preset; it appears only chained, when an action needs an
account.
Gate a feature
Section titled “Gate a feature”Two message types instrument your product. Both return null for
“proceed”; anything else means the wall is up and the action should stop:
import { sendMessage } from "@/utils/messaging";
// Before running a premium feature:const decision = await sendMessage("gateFeature", { feature: "export-pdf" });if (decision) return; // wall is already rendering everywhere
// Counting free usage toward the actions threshold:await sendMessage("gateAction", { name: "highlight" });Then add the feature ID to premiumFeatures in background/gates.ts.
The engine handles the wall UI, cooldowns, and the sign-in chain.
Metered (credit-billed) features use creditsConsume instead:
consume first, then work:
const result = await sendMessage("creditsConsume", { feature: "summarize" });if (!result.ok) return; // out of credits; the top-up wall is upThe metered preset reads the real balance: the wall fires the instant a
consume hits 0 and clears when a purchase or allowance reset raises it.
Enforcement stays server-side; the wall is conversion UX. See
Credits.
For manual control (the silent preset), use
sendMessage("gateOpen", { gateId: "paywall" }). The dev tools in the
Settings tab (dev builds) reset all local gate state.
Why did this wall appear?
Section titled “Why did this wall appear?”Read background/gates.ts top to bottom; it is the complete explanation:
PREMIUM_FEATURES: anygateFeaturecall with a listed ID raises the paywall immediately.- The
valueFirst({ ... })call expands to two gates: signin (triggermanual(), so it can never fire on its own) and paywall (premium moment, 10th action, or day 3, withrequires: "signin"anddismissible: true). - A wall did not appear because the user is paid, the gate is inside its post-dismissal cooldown, or the requirement is already satisfied. Anonymous guests count as signed out.
Stay inside Chrome Web Store policy
Section titled “Stay inside Chrome Web Store policy”CWS’s single-purpose policy expects your listed functionality to work on a fresh install. Review accounts are fresh installs; a wall they can’t pass reads as bait-and-switch, a rejection class. The kit’s guardrails:
- Core value stays usable pre-wall. Gate premium extras; never put the action from your listing’s first sentence behind a paywall. If the whole product is paid, say so in the listing; that is allowed, hiding it is not.
- Walls are dismissible by default.
escalateAfterDismissalsis opt-in and belongs only on non-core features. - No wall at install. No preset triggers a non-dismissible wall on day 0. Don’t hand-write one.
- Sign-in never fires standalone; chained only.
- For a review-sensitive product, use
silent: nothing fires unless you call it.
The server-side mirror
Section titled “The server-side mirror”Walls are conversion UX; the features they gate are enforced by
entitlements and credits on the server. Gate events flush (via a 1-minute
alarm) to POST /gate/events, which increments usage/{uid} in
Firestore. Anything with stakes (trial-once, credit balances, abuse caps)
reads those server counters, never client numbers.
Remote config
Section titled “Remote config”GET /gateConfig serves flags and values as remote data, which store
policy allows; remote code is not.
Wire those values into gate thresholds with a config transform before
defineGates.