Keep subscriber pages out of shared caches
Set up your CDN, proxy and page cache so subscribers reach your app, and their pages never reach anyone else.
In short
- When a request carries
Better-Web-Token, your cache must skip lookup and skip storage. - Forward the token and the public hostname to your application.
- Mark subscriber responses
private, no-store. - Test the same URL with and without a token, while the cache is warm.
Why it matters
A shared page cache can go wrong in two ways:
- It serves a subscriber the ordinary page. The request never reaches your app, so the token is never checked, and the subscriber sees ads.
- It stores a subscriber's page. Other visitors then get your paid content and a clean page for free.
The SDK's own cache only stores verification results. It doesn't protect your pages.
The rules
For every caching layer in front of your app:
- Skip the cache lookup when the request has a
Better-Web-Tokenheader. - Don't store the response to that request.
- Forward the
Better-Web-Tokenheader to your application. - Keep the public hostname in the
Hostheader, so the token checks against the right website.
A header being present must never grant access on its own. Only your app's verification decides.
Nginx
Add a map in the http block:
map $http_better_web_token $freedom_skip_cache {
"" 0;
default 1;
}
Then add the matching lines to the location that caches. Keep your other cache exclusions.
With proxy_cache:
proxy_cache_bypass $freedom_skip_cache;
proxy_no_cache $freedom_skip_cache;
proxy_set_header Better-Web-Token $http_better_web_token;
With fastcgi_cache (PHP-FPM):
fastcgi_cache_bypass $freedom_skip_cache;
fastcgi_no_cache $freedom_skip_cache;
fastcgi_param HTTP_BETTER_WEB_TOKEN $http_better_web_token;
You need both lines: *_cache_bypass skips lookup, and *_no_cache skips storage. The map makes any non-empty token bypass, even an invalid value such as 0. Don't configure Nginx to ignore private or no-store responses.
Varnish
In vcl_recv, before any cache lookup or early return:
if (req.http.Better-Web-Token) {
return (pass);
}
Make sure the header still reaches your backend.
Cloudflare
Create a Cache Rule with Cache eligibility: Bypass cache, and this expression:
http.request.headers.truncated or has_key(http.request.headers, "better-web-token")
Cloudflare's header map uses lowercase names, so this also matches mixed-case headers. Requests with truncated headers bypass too.
Check that no other rule or Worker overrides the bypass or strips the token.
Other caches
| Cache | What to do |
|---|---|
| Apache, CloudFront, managed hosting | Bypass both lookup and storage for token requests, and forward the header. |
| No bypass controls available | Turn off HTML caching on pages that participate. Static assets can stay cached. |
| WordPress page-cache plugins | See WordPress and server caches. |
Mark subscriber responses private
When a request carries a token, send:
Cache-Control: private, no-store
Adding Vary: Better-Web-Token to your public responses also stops browsers reusing the ordinary page once a token appears. The WordPress plugin sends both for you.
Check your caches
Clear all page caches first. Then test the same URL in this order:
- Without a token, load the page to warm the public cache.
- With a valid token, such as from Test in your browser, load it again. You should get the clean page on the first visit.
- Repeat the token request. It must still skip the cache, with a
private, no-storeresponse. - Remove the token. The ordinary page, including any paywall, must be unchanged.
- Try invalid, expired and wrong-hostname tokens. None may grant access.
Check each cache layer's logs or status headers. A MISS alone isn't enough, because a miss can still store the response.
Token expiry
- An expired token stops working, give or take the SDK's 60-second clock tolerance.
- Cancelling a membership or closing an account stops renewal. It can't revoke a token that was already signed, until that token expires.
- Cached verification results never outlive the token's expiry.