State crossing
How HTTP request state crosses the Worker front door and Durable Object boundary.
When a Worker route mounts into a Durable Object, request state can cross the boundary without hand-rolling headers in application code. Flare serializes declared flareState tokens into a framework envelope on the way in and reads a smaller envelope on the way out.
Hand-rolled, the crossing is a header protocol you own at both ends: choose names, serialize on the Worker, parse in the DO, and keep both ends in sync as shapes change. And because any client can send your header names too, stripping forgeries is your code on every path that reaches the binding. The declared version collapses that into one vocabulary both ends share: the class’s static state list drives one encoder and one decoder, reserved headers are sanitized before framework state is attached, and a consumed token nobody provides fails build() instead of arriving as undefined on the far side of the boundary.
Declare crossing state
Section titled “Declare crossing state”The crossing vocabulary is the DO class’s static state declaration: those tokens are the ones the framework serializes into the envelope on the way in. A state: list on a DO route declares consumption only, and the front door must provably provide each consumed token, through a token default or derivation, a global before-middleware’s provides, or the mount’s resolve.provides.
import { FlareHost, FlareResponse, flareState } from "@flare-ts/core";import { cf, DurableState, FlareDurableObject } from "@flare-ts/core/cloudflare";
const Tenant = flareState<{ tenantId: string }>("Tenant");
class Room extends FlareDurableObject { public static override deps = [DurableState] as const; // The crossing vocabulary: the tokens this DO participates in. public static override state = [Tenant] as const;}
const host = new FlareHost(cf);
// The front door provably provides Tenant before any mount forwards.host.http.before({ provides: [Tenant] }, (ctx) => { ctx.state.set(Tenant, { tenantId: "acme" });});
const room = host.durableObject(Room);
// The DO route declares consumption of Tenant on the inbound crossing.room.http.get("/meta", { state: [Tenant] }, (ctx) => { const tenant = ctx.state.require(Tenant); return new FlareResponse(200, { tenantId: tenant.tenantId });});
room.mount("/rooms/:name");Build fails with MOUNT_STATE_NOT_PROVIDED when a token a DO route consumes inbound is not provably provided front-door (a token default or derivation, a global before-middleware’s provides, or resolve.provides). Output-only tokens (the DO sets them outbound, nothing consumes them inbound) build clean. Inside the DO, the class’s static state tokens count as provided at arc entry, so a DO route consuming a forwarded token validates clean.
Inbound vs outbound
Section titled “Inbound vs outbound”| Direction | What crosses |
|---|---|
| Front door to DO | Resolved values from the Worker’s ctx.state (defaults and derivations apply) |
| DO to front door | Only values the DO route explicitly set (raw read, no silent defaults) |
That asymmetry keeps a DO from overwriting front-door state it never touched on the response path.
Address a DO with durable()
Section titled “Address a DO with durable()”durable(namespace, name) is the second blessed seam. It returns a stub with two calls:
.fetch(req)is a state-free raw tunnel by design: it strips the reservedx-flare-stateandx-flare-traceheaders before the request reaches the DO, so a client-forged envelope can never cross..forward(ctx, Class)is the state-carrying call: it sanitizes those reserved headers, then encodes the framework-owned envelope fromctxbefore forwarding.
import { durable } from "@flare-ts/core/cloudflare";
// State-free raw tunnel: strips the reserved headers, so nothing a client// forged in x-flare-state can reach the DO.await durable(env.ROOM, roomName).fetch(ctx.req.nativeRequest);
// State-carrying: sanitizes the reserved headers, then encodes the// framework-owned envelope from ctx before forwarding.await durable(env.ROOM, roomName).forward(ctx, Room);Size limit
Section titled “Size limit”The serialized envelope is capped at 12288 bytes; exceeding it throws at the forward seam. Large payloads belong in DurableState.storage, not in crossed state. WebSocket ws.state uses a separate attachment budget on hibernating sockets. See Connection state.
What you do not see
Section titled “What you do not see”Framework-internal headers carry the envelope and trace id. Routes never read or set those headers directly.
Related
Section titled “Related”- HTTP request state: declaring and reading
ctx.state - Register and mount: mount setup
- Overview: the per-DO
.httpand.wssurfaces