.phaze: Edge data (Cloudflare)
---platform and ---cloudflare are edge-data boundaries — a subsystem distinct from the four core boundaries (---page, ---data, ---state, ---props). The core four compile to exports, a loader, state, or params; these two compile to instance-scope edgeSignals whose values come from a built-in resolver reading Cloudflare’s per-request intelligence.
Unlike ---data (where you write name: value), these list bare field names — one per line — and the value comes from the resolver, not from you. ---platform answers “how is the user connecting?” (device + connection tech); ---cloudflare answers “who / where is the request from?” (geo / identity / bot).
What it exposes
Section titled “What it exposes”The resolvers read two Cloudflare-specific request sources during SSR:
-
request.cf— theIncomingRequestCfPropertiesobject Cloudflare attaches to every Worker request. Geo (country,city,timezone,regionCode,isEUCountry), connection (clientTcpRtt,clientQuicRtt,httpProtocol,edgeL4.deliveryRate), TLS (tlsVersion,tlsCipher), network (asn,asOrganization,colo), andbotManagementall live here. -
Cloudflare request headers — for the few fields not on
request.cf:Header Used for CF-Device-Typedevice— not delivered to Workers; theUser-Agentsniff is the real path (see below)CF-Connecting-IPconnectingIpTrue-Client-IPtrueClientIp(Enterprise)CF-RayrayCF-WorkerworkerCF-Visitorvisitor.scheme
---platformdeviceperf---cloudflarecountrycitytimezone---Compiles to
import { edgeSignal, __platform, __cloudflare } from '@madenowhere/phaze-cloudflare/runtime'
const device = edgeSignal(() => __platform().device)const perf = edgeSignal(() => __platform().perf)const country = edgeSignal(() => __cloudflare().country)const city = edgeSignal(() => __cloudflare().city)const timezone = edgeSignal(() => __cloudflare().timezone)The edgeSignal / __platform / __cloudflare imports are injected by phaze-compile’s babel pass (same merge-or-prepend as revalidate / @global) — you never write them.
Each field becomes an instance-scope edgeSignal — a real phaze Signal<T>. It resolves once on the edge during SSR (from request.cf + CF request headers), serializes into the page payload, and rehydrates on the client with no fetch — so it reads the same on both sides. Read it like any signal:
<p>{city} · {timezone}</p> {/* signal-as-child auto-binds */}watch(console.log(device(), city())) {/* or read explicitly */}---platform — how the user connects
Section titled “---platform — how the user connects”| Field | Type | Derived from |
|---|---|---|
device | 'mobile' | 'tablet' | 'desktop' | a coarse User-Agent sniff. The CF-Device-Type header is preferred when present, but Cloudflare does not send it to Workers — see the caching caveat below. Not request.cf.deviceType — no such field exists. |
mobile | boolean | device !== 'desktop' — a convenience flag |
perf | { latency, protocol, throughput } | latency ← cf.clientTcpRtt ?? cf.clientQuicRtt; protocol ← cf.httpProtocol; throughput ← cf.edgeL4.deliveryRate |
tls | { version, cipher } | version ← cf.tlsVersion; cipher ← cf.tlsCipher |
network | { asn, asOrganization, colo } | cf.asn / cf.asOrganization / cf.colo |
⚠️ device and the edge cache
Section titled “⚠️ device and the edge cache”A page that renders differently per device cannot be cached under a URL-only key. That
includes any page using device-variant fences —
---mobile / ---tablet / ---desktop — because the matched variant is rendered into the
HTML and the resolved device is inlined into the hydration payload, which the client reads
once to decide which chunk to import. Serve that document to a second device and the browser
renders the wrong variant and fetches the wrong chunks, never requesting its own.
Cloudflare has a setting for this — Cache by device type — and it does not apply to
Workers. It is an Automatic Platform Optimization
feature that sends CF-Device-Type to an origin, and a Worker on a custom domain has none.
Cloudflare’s Workers Cache docs state it
plainly: “No zone configuration for caching applies to Workers Caching.” Measured on a live
deployment with the rule enabled, only cf-connecting-ip, cf-ray and cf-visitor were
delivered — no cf-device-type. Cache Rules, Edge TTL, the device cache key and Purge
Everything each changed nothing, while the Worker’s own CDN-Cache-Control controlled the
cache to the second.
What Workers Caching does honour is Vary. Declare it on the cache rule:
cloudflare({ cache: { rules: { '/**': { maxAge: minutes(1), swr: days(1), headers: { Vary: 'User-Agent' } }, }, },})User-Agent rather than client hints: Sec-CH-UA-Mobile is two values where a UA string is
thousands, but Safari and Firefox send no client hints, so a hint-keyed cache leaves every
Safari visitor sharing one entry. The UA is also exactly what resolveDevice reads, so the
cache key and the rendered variant cannot disagree. The cost is one cached copy per distinct
UA string instead of one per URL.
---cloudflare — who / where the request is from
Section titled “---cloudflare — who / where the request is from”| Field | Type | Derived from |
|---|---|---|
country | string | undefined | cf.country (ISO 3166-1 alpha-2; same value as the CF-IPCountry header) |
eu | boolean | cf.isEUCountry === '1' |
region | string | undefined | cf.regionCode |
city | string | undefined | cf.city |
timezone | string | undefined | cf.timezone (IANA, e.g. America/New_York) |
connectingIp | string | null | the CF-Connecting-IP header |
trueClientIp | string | null | the True-Client-IP header (Enterprise) |
ray | string | null | the CF-Ray header |
worker | string | null | the CF-Worker header — the owning Worker zone’s hostname, stamped on Worker subrequests (loop detection) |
visitor | { scheme } | scheme ← the CF-Visitor header’s JSON (e.g. {"scheme":"https"}), parsed defensively — undefined if absent/malformed |
bot | { score, verified } | cf.botManagement.score / cf.botManagement.verifiedBot (requires Bot Management) |
Flat vs grouped. device / mobile and every ---cloudflare scalar carry a single value — device() is the string. perf / tls / network / visitor / bot carry an object — read a member off the signal: perf().latency, tls().version, visitor().scheme, bot().score. The compile shape is identical; only the resolver’s return type differs.
- Bare field names only — one identifier per line (same trailing-whitespace +
// commenttolerance as the other fences). Noname: value; the value is the resolver’s, not yours. - Unknown or duplicate fields surface a diagnostic and are skipped. The allowed sets are exactly the fields tabled above.
- Instance-scope, not module-scope —
edgeSignalmust run during render, so the declarations hoist into the component body (like Local---state), not above it. They’re in scope in the trailing region AND in every---mobile/---tablet/---desktopvariant body. - Initial state only — after hydration each is a regular signal; you may
.set()it, but the edge value is just the initial value (it never re-resolves client-side).