Skip to content

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

The resolvers read two Cloudflare-specific request sources during SSR:

  • request.cf — the IncomingRequestCfProperties object 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), and botManagement all live here.

  • Cloudflare request headers — for the few fields not on request.cf:

    HeaderUsed for
    CF-Device-Typedevice (when the cache_by_device_type Cache Rule is on)
    CF-Connecting-IPconnectingIp
    True-Client-IPtrueClientIp (Enterprise)
    CF-Rayray
    CF-Workerworker
    CF-Visitorvisitor.scheme
---platform
device
perf
---cloudflare
country
city
timezone
---
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 */}
FieldTypeDerived from
device'mobile' | 'tablet' | 'desktop'the CF-Device-Type header (set when the cache_by_device_type Cache Rule is on — it also partitions the edge cache per device), else a coarse User-Agent sniff. Not request.cf.deviceType — no such field exists.
mobilebooleandevice !== 'desktop' — a convenience flag
perf{ latency, protocol, throughput }latencycf.clientTcpRtt ?? cf.clientQuicRtt; protocolcf.httpProtocol; throughputcf.edgeL4.deliveryRate
tls{ version, cipher }versioncf.tlsVersion; ciphercf.tlsCipher
network{ asn, asOrganization, colo }cf.asn / cf.asOrganization / cf.colo

---cloudflare — who / where the request is from

Section titled “---cloudflare — who / where the request is from”
FieldTypeDerived from
countrystring | undefinedcf.country (ISO 3166-1 alpha-2; same value as the CF-IPCountry header)
eubooleancf.isEUCountry === '1'
regionstring | undefinedcf.regionCode
citystring | undefinedcf.city
timezonestring | undefinedcf.timezone (IANA, e.g. America/New_York)
connectingIpstring | nullthe CF-Connecting-IP header
trueClientIpstring | nullthe True-Client-IP header (Enterprise)
raystring | nullthe CF-Ray header
workerstring | nullthe 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 + // comment tolerance as the other fences). No name: 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-scopeedgeSignal must 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 / ---desktop variant 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).