ekumanov/flarum-ext-edge-cache

Cookieless guest page views + CSRF retry shim, enabling safe Cloudflare edge caching of guest HTML.

Maintainers

Package info

github.com/ekumanov/flarum-ext-edge-cache

Type:flarum-extension

pkg:composer/ekumanov/flarum-ext-edge-cache

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.1 2026-07-02 15:30 UTC

This package is auto-updated.

Last update: 2026-07-02 15:35:28 UTC


README

Makes guest page views cookieless so Cloudflare can safely cache guest HTML at the edge, plus a client-side CSRF retry shim so auth flows survive landing on a cached page. It also preloads the discussion page's boot-critical JS chunks, and can purge Cloudflare on every post change so a long edge TTL never serves stale content. Requires Flarum 2.0.

Installation

composer require ekumanov/flarum-ext-edge-cache

Components

  1. EdgeCacheMiddleware (forum frontend, inserted before StartSession — StartSession attaches cookies on the response's way OUT, so only an outer middleware can strip them): credential-less GET/HEAD on allowlisted paths → strip ALL Set-Cookie + X-CSRF-Token, emit a Server-Timing: origin header and Cache-Control: public, s-maxage=<ttl>, max-age=0, must-revalidate. The edge TTL is 3600s when Cloudflare purge credentials are configured (see below) — purge-on-write keeps the long tail warm without stale content — and a conservative 300s otherwise. All other forum HTML → explicit Cache-Control: private, no-store.
  2. JS retry shim: on a 400 whose JSON:API body carries code: csrf_token_mismatch, single-flight GET /api (refreshes session cookie + token via core's response-header update), then retry the original request once. This covers /api/* writes as well as login and register: although those POST to forum routes, Flarum 2.0's forum error handler content-negotiates, so an XHR (default catch-all Accept) receives the same JSON:API error the shim matches.
  3. CSRF exemption for forum-widgets.guest-heartbeat, the guest presence beacon of ekumanov/flarum-ext-forum-widgets (spoofable anyway, and the highest-frequency 400 source on cached pages). A no-op when that extension isn't installed.
  4. Discussion chunk preload: on /d/*, emits <link rel="preload" as="script"> for core's PostStream.js + PostStreamScrubber.js (hashes read from rev-manifest.json at runtime, so the preload URL always matches what the webpack runtime fetches — no double download). These chunks are otherwise fetched serially after boot; preloading lets them download in parallel with forum.js, collapsing that render-delay tail. Helps guests and logged-in members alike.
  5. Cloudflare purge-on-write: when a discussion's content changes (post added/edited/deleted/hidden/restored, discussion renamed/deleted/hidden/ restored), queues a purge of that discussion's canonical landing URL. Lets the edge TTL stay long without guests seeing stale pages. Best-effort and queued, so a Cloudflare hiccup never blocks the user's action; a no-op (with a log line) when no credentials are configured.

The matching Cloudflare Cache Rule (v1)

Expression: host eq "example.com" AND starts_with(path, "/d/") AND method GET AND NOT (cookie contains "flarum_session" OR cookie contains "flarum_remember" OR cookie contains "locale") → Eligible for cache, Edge TTL: respect origin, Browser TTL: respect origin. Adjust the host and the path prefix to your install (e.g. /forum/d/ when Flarum is mounted under /forum). The locale clause keeps a language-switched guest render off the shared cache — it matches both the bare locale cookie and prefixed variants like flarum_locale, in lockstep with the origin (see Invariants).

Cloudflare credentials (for purge-on-write + the long TTL)

Add to config.php (kept server-side, never exposed to the frontend):

'cloudflare' => [
    'zone_id'   => '...',
    'api_token' => '...', // a token scoped to Zone → Cache Purge for this zone
],

Create a scoped token in the Cloudflare dashboard (My Profile → API Tokens → Create Custom Token → permission Zone : Cache Purge, restricted to this zone) — do not use a Global API Key. Without these keys the extension keeps the conservative 300s edge TTL and the purge listener is a logged no-op, so it is safe to install before (or without) Cloudflare.

Known staleness

Purge-on-write fires on discussion content events only (post added/edited/deleted/hidden/restored, discussion renamed/deleted/hidden/ restored). Anything else embedded in a cached guest payload refreshes only within the edge TTL, not instantly — e.g. sticky/lock state, tag moves, poll votes, and like counts. Likewise, after a rename the old-slug URL keeps serving its cached page (then 301s) until it ages out under the TTL. All of this is bounded by the edge TTL by design; the TTL is the freshness floor for everything the purge list doesn't enumerate.

Invariants — read before changing anything

  • The middleware path allowlist and the CF rule scope move in lockstep, in the same deploy.
  • API responses must keep their Set-Cookie forever (heartbeat session-dedupe and the shim's refresh GET depend on it). This middleware is forum-only.
  • /reset, /confirm etc. are server-rendered Blade forms needing their session cookie — permanently denylisted.
  • A guest-facing language switcher would poison the shared cache (CF ignores Vary). Origin-side this is now enforced — a request carrying a locale cookie is served private, no-store — but keep the CF rule in lockstep (it must also exclude the locale cookie, as above).
  • CSRF 400s never reach flarum.log (KnownError) — monitor nginx access-log double-400s instead.

Rollback order

Disable this extension → clear the Flarum cache and purge the Cloudflare cache immediately (cached HTML referencing a rebuilt forum.js without the shim would otherwise strand guests until TTL expiry). Deleting the CF rule is safe at any point, in any order.

Build

cd js && npm install && npm run build