Skip to content

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.

presetpaywall appearsuse when
value-first (default)premium-feature moment, or after 10 actions / 3 daysmost products
day-zerofirst session (dismissible)strong day-0 conversion focus
meteredcredit balance exhausted (+ premium moments)usage products with credit billing
silentonly 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.

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 up

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

Read background/gates.ts top to bottom; it is the complete explanation:

  1. PREMIUM_FEATURES: any gateFeature call with a listed ID raises the paywall immediately.
  2. The valueFirst({ ... }) call expands to two gates: signin (trigger manual(), so it can never fire on its own) and paywall (premium moment, 10th action, or day 3, with requires: "signin" and dismissible: true).
  3. 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.

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:

  1. 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.
  2. Walls are dismissible by default. escalateAfterDismissals is opt-in and belongs only on non-core features.
  3. No wall at install. No preset triggers a non-dismissible wall on day 0. Don’t hand-write one.
  4. Sign-in never fires standalone; chained only.
  5. For a review-sensitive product, use silent: nothing fires unless you call it.

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.

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.