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:

  1. Skip the cache lookup when the request has a Better-Web-Token header.
  2. Don't store the response to that request.
  3. Forward the Better-Web-Token header to your application.
  4. Keep the public hostname in the Host header, 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:

  1. Without a token, load the page to warm the public cache.
  2. With a valid token, such as from Test in your browser, load it again. You should get the clean page on the first visit.
  3. Repeat the token request. It must still skip the cache, with a private, no-store response.
  4. Remove the token. The ordinary page, including any paywall, must be unchanged.
  5. 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.