A paywall or entitlement check that runs in the browser is expensive to find and expensive to patch only if you make it so. Minified output makes it neither.
Why a paywall gate is a one-line patch
In plain or minified output, a paywall gate is usually a named function that returns a boolean. Anyone reading the bundle can search for the word that names it: isSubscribed, hasAccess, checkEntitlement. Once found, the cheapest attack is a one-line patch that flips the return value.
That patch works on every user's copy of the file, and it keeps working on your next release if the file has not changed shape. The work of finding the gate gets paid for once and reused on every build after it.
What the shipped file looks like after a build
A default build renames identifiers and encodes string literals, so the function that used to be called isSubscribed no longer carries that name and the strings around it are no longer readable text. Searching the shipped file for the name or the message finds nothing to patch.
The property that matters most here is per-build polymorphism. The seed is new on every build unless you pin it, so a patch written against one release does not line up with the next one. An attacker who works out where the gate lives pays that cost again every time you ship.
Raise protection on the gate
The default preset already renames and encodes at a low complexity target. To raise the whole bundle a step:
$ npx afterpack@latest dist/ --preset=hardTo raise just the gating function without touching the rest of the bundle, mark it with a directive:
/* @afterpack preset=hard */
function checkEntitlement(token: string): boolean {
return validateSubscription(token);
}
/* @afterpack end */Directives take effect on Pro builds.
Check the gate came out protected
Open the Protection Map after a build and find the gating function. It should be coloured more strongly than the code around it, reflecting the higher complexity target you set for that region. If it looks the same as the rest of the bundle, read the build's diagnostics: a directive that was read but had no effect is reported there.
Where a client-side gate ends
Per-build rotation is the durable part of this. A new seed every release means the work of locating and patching the gate never carries forward, so a patch, script or userscript written against one build stops applying to the next one. A static reader working through a single build can still recover an equivalent computation, since the gate has to run correctly in the browser, so pair the client-side check with a server-side entitlement call for anything that gates revenue.
Next
- Per-region obfuscation directives: the full directive grammar and what else is worth marking.
- Stop userscripts and extensions from patching your web app: what structural change and a new seed do to a text-matching patch.
- Threat model: what a static reader can and cannot recover.