---
title: "Cloudflare reference"
description: "Configuration, capabilities, endpoints, caching and troubleshooting."
requested_language: fr
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/fr/payload/cloudflare/reference
version: unreleased (main)
docs_index: https://docs.systhema.app/fr/llms.txt
---
> This page isn't translated yet. Showing English.


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

## Configuration reference

```ts
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.

## Capabilities

Registered only when Cloudflare is enabled:

| Capability                 | Default roles                          | Gates                                                                                                                                 |
| -------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `cloudflare.read`          | `dev`, `admin`, `editor`, `seoManager` | Dashboard widgets, `/timeseries`, `/totals`, `/breakdown`                                                                             |
| `cloudflare.purge`         | `dev`, `admin`, `editor`               | `/purge`, the admin-bar cache-clear buttons                                                                                           |
| `global.cloudflare.update` | `dev`, `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.

## Endpoints

Registered on the Payload config only when Cloudflare is enabled:

| Endpoint                                       | Method | Auth                                            | Purpose                                                                                                                                                                                                                                                                                                                                                                                                   |
| ---------------------------------------------- | ------ | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/api/systhema/cloudflare/settings`            | GET    | Authenticated only                              | Safe 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/timeseries`          | GET    | `cloudflare.read`                               | Zero-filled per-bucket HTTP metrics for `?preset=` or `?since=&until=[&tz=]`.                                                                                                                                                                                                                                                                                                                             |
| `/api/systhema/cloudflare/totals`              | GET    | `cloudflare.read`                               | Current + previous-period aggregate metrics (requests, bandwidth, cache, uniques, threats).                                                                                                                                                                                                                                                                                                               |
| `/api/systhema/cloudflare/breakdown`           | GET    | `cloudflare.read`                               | Top-N rows for one property — `?property=country\|status\|contentType&limit=`.                                                                                                                                                                                                                                                                                                                            |
| `/api/systhema/cloudflare/purge`               | POST   | `cloudflare.purge`                              | Body `{scope:'everything'}` \| `{urls:[...]}` \| `{paths:[...]}`. Purges the edge cache AND triggers the matching Next.js revalidation.                                                                                                                                                                                                                                                                   |
| `/api/systhema/cloudflare/zone-settings`       | GET    | `cloudflare.read` or `global.cloudflare.update` | Every zone-setting card state, plus the zone's plan (for plan-gating). Optional `?host=` selects a domain (omitted ⇒ primary).                                                                                                                                                                                                                                                                            |
| `/api/systhema/cloudflare/zone-settings`       | PATCH  | `global.cloudflare.update`                      | Body `{name, value, host?}`. Patches one zone setting — `name` must be one of the settings the admin page exposes (400 otherwise).                                                                                                                                                                                                                                                                        |
| `/api/systhema/cloudflare/optimize`            | POST   | `global.cloudflare.update`                      | Body `{host?}`. Applies the "Optimize for Systhema" bundle; returns per-setting applied/skipped/failed lists.                                                                                                                                                                                                                                                                                             |
| `/api/systhema/cloudflare/sync-zone-settings`  | POST   | `global.cloudflare.update`                      | Body `{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) | POST   | `cloudflare.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.

## Caching

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.

## Troubleshooting

- **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.
