---
title: "Zones and domains"
description: "Zone resolution and multiple locale domains."
requested_language: cs
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/cs/next/payload/cloudflare/zones
version: unreleased (main)
docs_index: https://docs.systhema.app/cs/next/llms.txt
---
> This page isn't translated yet. Showing English.


Every Cloudflare call goes to a zone. This page covers how Systhema finds the zone for your site, and what changes when the site publishes through several domains.

## Zone resolution

An explicit `zoneId` is used directly. Otherwise the zone is resolved from `siteUrl`'s host by querying Cloudflare for an active zone matching that host, walking up subdomains until a match is found — `www.blog.example.com` → `blog.example.com` → `example.com` (a bare TLD is never tried). A resolved zone (id, name, plan, account id — the latter only ever used server-side to build the `dashboardUrl` deep-link) is cached for an hour in `payload.kv`. Not every failure to resolve is treated the same: a genuine "no matching active zone" result (Cloudflare responded, nothing matched) is a real `null` and is cached for only 60 seconds — not the full hour — so fixing DNS/zone activation is reflected within a minute instead of staying "Not connected" for up to an hour. An API/network failure (unreachable credentials, a timeout) is never persisted as that `null` — it falls back to serving a still-fresh cached zone if one exists, or otherwise surfaces as "not connected" without writing a new cache entry, so a transient outage can't get stuck caching a false negative.

## Multiple domains (locale domains)

A [domain-routed multilingual site](https://docs.systhema.app/cs/next/nextjs/locales.md) can publish through more than one registrable domain — say `example.eu` carrying `en`, `fr` and `nl` under path prefixes, and `example.hu` carrying `hu` on its own domain. **Each registrable domain is a separate Cloudflare zone**, and Cloudflare rejects a purge for a URL outside the zone it was sent to.

Purging handles that for you, with no extra configuration:

- **Per-URL purges** (publishing a page, editing a component or form, redirects, uploads) group the affected URLs by host and issue one purge per owning zone. A Hungarian page on `example.hu` is purged against the `example.hu` zone, not dropped because the primary zone is `example.eu`.
- **Whole-site purges** (general settings, header/footer, "Revalidate site", the admin bar's "Clear all caches") purge _every_ zone the site publishes through — the canonical site host plus every configured locale domain.
- Hosts sharing a zone (`example.eu` and `shop.example.eu`) are deduplicated, so one zone is never purged twice.
- A locale domain that isn't on Cloudflare at all is a supported setup, not an error: those pages are still revalidated in Next.js, and a warning names the hosts whose edge cache was left alone — the purge is never silently skipped.

An explicit `zoneId` pins the **primary** domain's zone only; the other domains are still resolved by name, so pinning one zone never sends another domain's purge to the wrong place.

**One credential, many zones.** There is a single `apiToken` (or key + email) for the whole module — every zone is reached with the same credential, so it must be able to see and purge all of them.

A scoped API token handles this fine; the only thing to get right is the token's **Zone Resources**, which defaults to a single zone:

| Your domains                   | What to create                                                                          |
| ------------------------------ | --------------------------------------------------------------------------------------- |
| All in one Cloudflare account  | API token, Zone Resources → **Include → All zones from an account** (or list each zone) |
| Spread across several accounts | A **user** API token (Zone Resources → _All zones_), or the legacy Global API Key       |

The Global API Key works in every case because it carries full account access — which is also why it is the fallback rather than the recommendation. Prefer a token whenever one can cover your zones, so the credential your CMS holds can be revoked and re-scoped without touching anything else.

When a domain's zone can't be resolved — a too-narrow token, a domain not on Cloudflare, a zone in an account the token can't see — those pages are still revalidated in Next.js and a warning names the hosts whose edge cache was skipped:

```text
[systhema cloudflare] auto-purge skipped for hosts with no active Cloudflare zone
  { hosts: ['example.hu'] }
```

### Managing every domain from the admin page

When more than one domain resolves to a Cloudflare zone, the [Cloudflare admin page](https://docs.systhema.app/cs/next/payload/cloudflare/admin-page.md) grows a **domain selector** above the connection-status panel, and everything below it — the Cache, Security and Speed tabs, and "Optimize for Systhema" — reads and writes the **selected** domain's zone rather than the primary one. Optimize is labelled with the domain it will act on, so a one-click bundle is never ambiguous about where it lands.

The selector renders **only** when at least two distinct zones resolve. A single-domain site — however many locales it serves — gets exactly the page it had before: no selector, no badges, no apply-to-all button. The selection is deliberately **not** remembered between visits: the page always opens on the primary domain, because a remembered write target is how someone returns weeks later, recognises the toggles, and edits the wrong domain believing it is the primary one.

The connection-status panel expands to list every publishing host with its own zone name and plan, or "Not on Cloudflare" for a domain that never resolved — so a too-narrow token surfaces as a named domain rather than one that silently isn't there.

**"Differs" badges.** Every setting row is compared across the resolved domains and carries a badge when they disagree, so drift is visible without opening each domain in turn; the selector shows a per-domain count ("2 differ") for the same reason.

Badges cover the settings this page renders a control for — `security_level`, `waf`, `always_online`, `automatic_https_rewrites`, `mirage` and `polish` — so a count always matches the number of badges you can actually find and act on. Apply-to-all syncs a **wider** set, adding the performance settings the Optimize bundle owns (`cache_level`, `browser_cache_ttl`, `ipv6`, `websockets`, `ip_geolocation`, `email_obfuscation`, `server_side_exclude`, `hotlink_protection`, `rocket_loader`); those have no toggle here, so they are disclosed in the apply-to-all diff rather than badged. Neither set is the raw zone-settings response — Cloudflare exposes around fifty settings, most of which this page does not manage and whose per-domain differences would be noise.

A setting another domain reports as not editable, or that its plan doesn't offer (Mirage/Polish below Business), reads as **incomparable** rather than as drift. Those are permanent states, and a "differs" badge that can never be cleared is noise, not information. A domain whose settings couldn't be read at all makes the comparison **unknown**, not "matches": no badges appear on the rows, and that domain is flagged with an error marker in the selector and the connection panel so the gap is attributable.

The selected domain's settings load first — the single-domain fast path is unchanged — and the other domains load in the background. Badges simply appear as those reads land; nothing on the page waits for them.

**Apply to all domains.** A whole-page action (not per-tab) that copies the selected domain's comparable settings onto every other resolved domain. It always shows a diff first: clicking it runs a dry run and opens a confirmation listing, per domain, exactly what would change and from what to what (`example.hu: always_online off → on`). If nothing differs, it says so instead of offering a no-op write.

A domain whose settings **could not be read is excluded, not applied to blind** — it's named in the confirmation as excluded, and the apply skips it. Systhema will not overwrite a zone whose current state it doesn't know.

Confirming runs the real apply and reports a per-domain applied / skipped / failed breakdown, the same shape as the Optimize button's results. **Partial success is normal**: domains on different Cloudflare plans genuinely can't hold the same settings, so a Business-only value skipped on a Free zone (or a setting the token can't edit there) is reported as skipped while everything else applies.

**Development mode is never synced.** `development_mode` is deliberately outside the compared and synced set. It's a temporary, per-zone debugging state that Cloudflare reverts on its own after three hours — and it's the one setting where copying the source domain's value could switch a live production domain into development mode, bypassing its cache entirely, as a side effect of an action the user understood to be about cache levels and security headers. Turn it on per domain, from the Cache tab, with that domain selected.

**The dashboard widgets stay primary-zone only.** This is an explicit non-goal, not an oversight. The four Cloudflare widgets always report the zone resolved from `siteUrl`; there is no per-domain analytics view, since that would mean threading a domain dimension through every widget and the shared range selector for a number that is rarely compared side by side. For a second domain's traffic, use Cloudflare's own dashboard — the external-link icon on each widget card deep-links straight into it.
