Docs
Systhema Design (opens in new tab)
Unreleased

Cloudflare reference

Configuration, capabilities, endpoints, caching and troubleshooting.

On this page

The full cloudflare option, the capabilities and endpoints it registers, how it caches, and fixes for common problems.

Configuration referenceLink to this section

export interface SysthemaCloudflareOptions {
  /** Disable without removing the option. @default true when an object is provided */
  enabled?: boolean
  /** Cloudflare API token (recommended) — sent as a Bearer credential. */
  apiToken?: string
  /** Legacy Global API Key (account-wide). Paired with `email`. */
  apiKey?: string
  /** Account email — required alongside a legacy `apiKey`. */
  email?: string
  /** Explicit zone id. Omit to auto-resolve from `siteUrl`. */
  zoneId?: string
  /** Canonical site origin (e.g. `https://example.com`). Defaults to the Payload config `serverURL`. */
  siteUrl?: string
  /** Mirror Next.js revalidations into Cloudflare edge-cache purges. A persisted `cloudflare` global's `autoPurge` field overrides this default. @default true */
  autoPurge?: boolean
  /** Enable the Cloudflare dashboard widgets. @default true */
  analytics?: boolean
  /** Widget-data cache TTL, in seconds. 0 disables persistence. @default 300 */
  cacheTtlSeconds?: number
}

All credentials are server-only: they live in the server-side config store and never reach the admin client. The settings endpoint exposes only: whether the module is enabled, the requester's canRead/canPurge/canManage capability flags, whether analytics widgets are on, whether a zone is currently connected, the auth mode, the zone name/plan (when connected), the plan-aware range block, and a dashboardUrl deep-link (built from the zone's account id + name) — never the credential or the raw account id itself.

CapabilitiesLink to this section

Registered only when Cloudflare is enabled:

CapabilityDefault rolesGates
cloudflare.readdev, admin, editor, seoManagerDashboard widgets, /timeseries, /totals, /breakdown
cloudflare.purgedev, admin, editor/purge, the admin-bar cache-clear buttons
global.cloudflare.updatedev, admin/zone-settings (PATCH), /optimize, /sync-zone-settings, the Cloudflare admin page's mutations (including its autoPurge field)

/settings isn't gated by a capability — any authenticated user can read it (it only returns safe client settings, including the requester's own capability flags, so the UI knows what to render). dev holds every capability via its * wildcard. Adjust via the existing roles API (roles.overrideRoles, custom roles) like any other Systhema capability.

EndpointsLink to this section

Registered on the Payload config only when Cloudflare is enabled:

EndpointMethodAuthPurpose
/api/systhema/cloudflare/settingsGETAuthenticated onlySafe client settings: capability flags, connection status, zone name/plan, auth mode, a dashboardUrl deep-link to the zone's Cloudflare dashboard (null when not connected), the plan-aware range block (presets + custom-range bounds), and a domains block (one entry per publishing domain, deduped by zone, with multi flagging whether more than one resolved). Never exposes credentials.
/api/systhema/cloudflare/timeseriesGETcloudflare.readZero-filled per-bucket HTTP metrics for ?preset= or ?since=&until=[&tz=].
/api/systhema/cloudflare/totalsGETcloudflare.readCurrent + previous-period aggregate metrics (requests, bandwidth, cache, uniques, threats).
/api/systhema/cloudflare/breakdownGETcloudflare.readTop-N rows for one property — ?property=country|status|contentType&limit=.
/api/systhema/cloudflare/purgePOSTcloudflare.purgeBody {scope:'everything'} | {urls:[...]} | {paths:[...]}. Purges the edge cache AND triggers the matching Next.js revalidation.
/api/systhema/cloudflare/zone-settingsGETcloudflare.read or global.cloudflare.updateEvery zone-setting card state, plus the zone's plan (for plan-gating). Optional ?host= selects a domain (omitted ⇒ primary).
/api/systhema/cloudflare/zone-settingsPATCHglobal.cloudflare.updateBody {name, value, host?}. Patches one zone setting — name must be one of the settings the admin page exposes (400 otherwise).
/api/systhema/cloudflare/optimizePOSTglobal.cloudflare.updateBody {host?}. Applies the "Optimize for Systhema" bundle; returns per-setting applied/skipped/failed lists.
/api/systhema/cloudflare/sync-zone-settingsPOSTglobal.cloudflare.updateBody {sourceHost, dryRun?}. Copies the source domain's comparable settings to every other publishing domain; dryRun returns the per-domain diff without writing. Unreadable domains come back in unavailable and are never written to.
/sys/cloudflare-purge (Next.js, not Payload)POSTcloudflare.purge (cookie auth + CSRF guard)The admin-bar's cache-clear buttons — same purge+revalidate engine as the Payload /purge endpoint.

Guard order on the data/mutation endpoints: 401 unauthenticated → 404 disabled → 403 missing capability → 400 invalid param. Beyond that, failure handling is endpoint-specific rather than a uniform opaque 502: a Cloudflare 4xx rejection carrying a user-actionable, non-sensitive message (e.g. "Security level of off is only available to Enterprise customers") is forwarded verbatim as a 400; a Cloudflare 5xx, a network failure, or an unexpected response shape stays opaque as a 502 (detail logged server-side only). The purge endpoints are a separate case: a disconnected zone doesn't 502 at all — the purge leg silently no-ops and the matching Next.js revalidation still succeeds, so purging while "not connected" returns a success response with purgedEverything/connected: false. Successful data responses are { success: true, data, stale? } — stale: true rides along only when an expired cache entry was served because the Cloudflare request failed.

The host parameter on the endpoints above is a closed list, not a free-form zone selector: it must be one of the hosts the site actually publishes through (the canonical site host plus the configured locale domains), and anything else 400s before any Cloudflare lookup happens. A raw zone id is never accepted — one credential typically reaches every zone on the account, so accepting an arbitrary zone would let a cloudflare.read user enumerate zones that have nothing to do with the site.

CachingLink to this section

Widget data and zone lookups are cached server-side in payload.kv under systhema-cloudflare:* keys, separate from the analytics module's systhema-analytics:* keys — the same read-through envelope pattern:

  • Serve-stale-on-error: an expired cache entry is served (flagged stale: true) instead of erroring when the Cloudflare refresh fails. A failed fetch is never persisted.
  • Request deduplication: concurrent requests for the same data share one in-flight Cloudflare call.
  • Widget data — configurable via cacheTtlSeconds (default 300 seconds).
  • Resolved zone — cached for 1 hour (zones rarely move between accounts).
  • Discovered dataset limits (range.presets/range.custom derivation — see "Plan-aware extended presets + custom ranges" above) — cached for 1 hour, same stale-on-error/dedup rules, falling back to a conservative "3 days hourly / 31 days daily" default on any failure.
  • The always_use_https setting (used to filter purge URLs) — cached for 5 minutes.
  • The zone-settings admin page reads are not cached — the admin page wants live state and reads are cheap. That includes the cross-domain reads behind the "differs" badges: Cloudflare's zone-settings reads are already eventually consistent, and stacking a cache envelope on top would show false drift for minutes after an apply.

TroubleshootingLink to this section

  • The cloudflare global page or dashboard widgets render blank after upgrading — the admin resolves Systhema's components through Payload's generated import map, and an import map generated before this version has no Cloudflare entries. Run payload generate:importmap (a next dev start also regenerates it automatically), then rebuild.
  • Monorepo/workspace development only: the dashboard 500s with Cannot destructure property 'config' of useConfig(...) — a stale incremental .next build can duplicate @payloadcms/ui in the SSR graph when @systhemaui/payload is consumed through a workspace symlink, splitting the admin's React context. Delete .next and rebuild clean. Published-package installs are not affected.
  • "Not connected" in the admin page / empty widgets — the configured credentials didn't resolve to an active zone. Check that apiToken (or apiKey+email) is correct, that the zone is active on the account those credentials belong to, and that siteUrl (or your Payload serverURL) matches the zone's registered domain (or a subdomain of it).
  • Today/Yesterday show flat or missing data on an older zone — Cloudflare's GraphQL Analytics API only retains hourly (httpRequests1hGroups) data for a limited window; very old zones or free-tier retention limits can leave the hourly presets sparse even though daily data further back remains populated. This module always uses daily buckets for anything past a 3-day span, so Last 7/30 days are unaffected.
  • WAF / Image optimization stay disabled — these are plan-gated (Pro+ for WAF, Business+ for Mirage/Polish). The toggle shows a "Currently not available. Upgrade needed." hint instead of hiding, so the control is discoverable even when unavailable.
  • "Optimize for Systhema" reports failures — some Cloudflare token types (e.g. Account Owned Tokens) reject specific settings endpoints even with the correct scope. The bundle keeps applying every other setting and reports failures individually rather than aborting.
  • Auto-purge silently does nothing — check, in order: is cloudflare enabled with valid credentials; is the effective autoPurge toggle on (the cloudflare global's autoPurge field wins over the plugin option's default when the global is readable); does siteUrl (or serverURL) resolve to an absolute origin. A missing siteUrl logs a warning and skips only the Cloudflare purge — Next.js revalidation still happens.
  • A Global API Key pasted into apiToken "just works" — that's intentional: the module sniffs the credential's format (Cloudflare's cfk_ prefix, or the legacy 37–45 character lowercase-hex shape) and sends it with the correct header pair when an email is also provided, rather than failing as a malformed Bearer token.