Router: Flow
Flow is shared state across domains.
Declare the sibling origins at build time — subdomains that behave as one product rather than as separate sites — and each surface can detect a document loaded from one of them, then read state the previous document left behind: a cursor position, a half-typed draft, anything that should survive the boundary.
Flow adds no state machinery of its own. Flow() is published once as a @global signal in your Phaze App and read as flow() anywhere in the app. Wrapping it in c() is what makes that work: the computed evaluates on demand, so the read resolves after the router has run rather than at module scope, and memoised, so a component reading flow() in seven places evaluates it once. The handoff itself is an ordinary short-lived cookie.
What Flow contributes is the origin list and two moments: Flow.in() on load, Flow.out() on page-hide. Everything reactive about it is phaze’s: one signal, declared once, read everywhere.
Declaring the flow
Section titled “Declaring the flow”List every origin in the group, development and production together. Each surface skips its own origin at runtime, so all of them can share one array:
cloudflare({ router: true, flow: [ 'http://localhost:5173', 'http://localhost:5174', 'https://example.com', 'https://portal.example.com', ],})startRouter compares document.referrer against that list once, when the page loads. The answer never changes while the document lives.
The API
Section titled “The API”Four calls, from the /flow subpath. Flow is callable and also has two methods — the same shape as s and s.async:
import { Flow } from '@madenowhere/phaze-cloudflare/flow'
Flow() // true when this document was loaded from a sibling originFlow(key, read, apply) // the one-line init — wires .in on load, .out on page-hideFlow.out(key, value) // hand a value forward, on the way outFlow.in(key) // take the value the sibling left, or nullFlow.in reads the value and removes it — one hop, one take. It also returns null when the document wasn’t loaded from a sibling origin, so a page reached directly can’t restore itself from its own leftovers.
Values travel on a short-lived cookie: thirty seconds, enough for a click. In production the cookie is set on the shared parent domain (domain=.your-site.tld) so every subdomain in the group can read it. On localhost it stays host-only, which works because cookies ignore port numbers — so :5173 and :5174 share one just as two subdomains would.
Asking once, reading everywhere
Section titled “Asking once, reading everywhere”Import Flow in the Phaze App and publish the answer as a @global. Components then read a signal rather than reaching for the router:
import { Flow } from '@madenowhere/phaze-cloudflare/flow'
---state@global flow : c(Flow())Use c(), not s(). The App’s module scope runs before startRouter does, so calling Flow() eagerly would capture the answer too early and freeze it at false. A computed doesn’t run its body until something reads it, and the first read happens when components mount — by which point the router has run. It also caches, so a component reading flow() in seven places evaluates it once.
Any .phaze file can then read it with no import at all:
---state@global flow---
<CenterMark draw={!flow()} … /> {/* plays when Flow() is false, stays still on a hop */}Moving state across the hop
Section titled “Moving state across the hop”State the whole handoff in one line, wherever the state belongs:
Flow('camera', getCameraState, setCameraState)That call takes whatever the sibling left, applies it, and arranges for the current value to be handed forward when this page goes away — using onPageHide, so it fires reliably across browsers. If Flow.in() finds nothing — no value left, or the document wasn’t loaded from a sibling — nothing is applied.
The value is applied before the call returns. That’s the reason this form exists rather than being a convenience: state that has to be ready before something else mounts is ready, because the call can’t finish until it is. Put the line above whatever depends on the state and the ordering takes care of itself.
Flow.in and Flow.out stay available for handoffs this pairing doesn’t fit — a value taken at one moment and applied at another, or a value sent with nothing to take on the other side.
Flow works on full page loads. A cross-origin hop is a real navigation, so the router’s link handling, view transitions and route cache stay out of it. Flow() reports the load and Flow.in() recovers the state; to make the load fast as well, pair it with Speculation Rules — declaring the flow origins widens the browser’s prerender rule to cover them, so a link to a sibling warms on hover with nothing extra authored. The destination opts in with speculate: { prerender: true, eagerness: 'moderate' }.
The handoff is momentary. The cookie lives thirty seconds and is consumed on first read — long enough for a click, short enough that a stale one never surfaces later. Durable state, sign-in included, belongs in its own layer.
History restores keep their own state. Going back or forward revives the previous document rather than loading a new one, so Flow() never runs again — and nothing needs recovering: the document still holds everything it had.