Skip to content

App Router layout persistence: compiler-backed router kernel architecture #726

@NathanDrake2406

Description

@NathanDrake2406

Updated 2026-04-30: this is the governing architecture for the App Router layout-persistence follow-up. It supersedes the original flat-map-first roadmap and earlier fixed-PR-count wording where they conflict.

App Router layout persistence: compiler-backed router kernel architecture

What This Issue Is Really About

The flat AppElements payload solved the first layout-persistence problem: the browser can merge layout, page, template, slot, and route entries instead of replacing the whole App Router tree.

The next problem is semantic authority.

Vinext needs to stop route meaning from leaking into wire keys, cache suffixes, local navigation guards, and missing payload entries. Future features like skip/reuse, streaming, prefetch, server actions, refresh, Activity preservation, and Cloudflare cache coherence need one clear source of truth.

This architecture makes Vinext behave like:

a compiler for route facts
a transaction system for navigation
a cache-coherence system for reuse
a flat transport shell for payload delivery
a React tree projection at the end

The law is simple:

No proof, no reuse.
No proof, no skip.
No proof, no visible commit.
No proof, render fresh.

Current Status

The first layout-persistence milestone has landed.

Vinext no longer needs to replace the whole App Router RSC tree on every client navigation. The current flat keyed AppElements payload gives us a practical bridge:

monolithic ReactNode payload
  -> flat keyed AppElements payload
  -> browser merge preserves layouts

That bridge should stay.

But it should not become the router's brain.

The next phase is about separating transport from meaning. The flat map is useful for payload delivery, merge buffers, compatibility, and encoding. It is not the right place to decide topology, slot preservation, interception, cache identity, root-layout transitions, skip/reuse, streaming, or stale async commit behavior.

The target architecture is:

Compiler-backed router kernel
+ navigation transaction authority
+ data dependency / resource graph
+ semantic cache-coherence model
+ Cloudflare-first runtime profile
+ flat wire transport
+ React tree projection

One-line version:

Compile stable facts, reduce live events, gate commits, prove cache reuse, execute first-class on Cloudflare, keep the wire shell dumb.

Problem

App Router behavior depends on more than the URL.

A client navigation can preserve layouts. A parallel route slot can remain mounted even when it is not represented in the new URL. An intercepted route can render as a modal during soft navigation, but as a full page on refresh or direct load. A refresh can target the same URL but still need a new visible commit. A server action can redirect, revalidate, patch, or force a refresh. A prefetch can complete after a newer navigation and must not mutate visible UI. A streamed chunk can arrive late and belong to a dead navigation.

Those behaviors cannot be safely inferred from:

missing AppElementsWire keys
string key suffixes
mounted-slot headers
local activeNavigationId checks
decoded wire paths
cache key fragments
Workers Cache hits
KV reads
R2 object existence

Missing data is not meaning.

A missing map key might mean preserve, delete, default, skipped, cached, failed, streamed later, stale, or irrelevant. Once absence starts carrying semantics, every feature adds another special case. That is how routers become hard to debug.

The architecture should make meaning explicit.

First Principles

The filesystem tells us what can exist. Runtime state tells us what currently exists. Events tell us what happened. The kernel decides what those facts mean. The lifecycle controller decides whether this result is still allowed to become visible. The cache model proves whether reuse is safe. The Cloudflare runtime makes proven reuse fast. The wire format carries payloads. React receives a tree at the end.

Core rules:

Transport moves data.
Topology describes structure.
State describes continuity.
Events describe time.
The kernel decides route meaning.
The lifecycle controller decides temporal commit authority.
Commit decisions and browser deltas mutate visibility.
CacheVariant defines equivalence; ResourceDependencyGraph defines invalidation and ownership.
InvalidationScope defines staleness.
The Cloudflare runtime implements the hot path.
Projection builds the React tree.

Any part doing more than its job will eventually become a second router.

The safety/performance law:

No proof, no reuse.
No proof, no skip.
No proof, no visible commit.
No proof, render fresh.

Uncertainty must degrade to a safe outcome:

fresh render
cache miss
discarded chunk
cache-seed only
noCommit
hard navigation

Uncertainty must never degrade to reuse.

Performance should come from certainty, not optimistic reuse:

compiler emits stable facts
  -> kernel proves reuse cheaply
  -> cache coordinator proves artifact compatibility
  -> server skips render work
  -> server sends fewer RSC bytes
  -> Cloudflare runtime serves approved artifacts from the fastest safe layer

Architecture Overview

flowchart TD
  subgraph Build["Build time"]
    A["Developer app/ filesystem"] --> B["Build-time compiler"]
    B --> C["RouteSemanticIR"]
    C --> D["CompiledRouteSchema / StaticSegmentGraph"]
    C --> G["DataDependencyIR / ResourceDependencyGraph"]
    D --> E["graphVersion + graph-minted IDs + route fact tables"]
  end

  subgraph Decision["Router decision path"]
    U["User intent / async result"] --> EV["RouterEvent"]
    S["RouterState / RouteSnapshot"] --> K["routerKernel.reduce"]
    E --> K
    G --> K
    EV --> K
    K --> R["RouterReduction / CommitProposal"]
    R --> L["NavigationLifecycleController"]
    L -->|approved| X["RouterExecutor / shell"]
    L -->|stale / aborted / superseded| N["reject or cache-seed only"]
    L -->|cross-root / incompatible graph| H["hard navigate"]
  end

  subgraph ServerEncode["Server encode path"]
    X --> SR["RSC render / payload materialization"]
    SR --> WE["AppElementsWire encode"]
    WE --> NET["network payload"]
  end

  subgraph ClientCommit["Client decode + projection path"]
    NET --> WD["AppElementsWire decode"]
    WD --> ES["ElementStore"]
    X --> BD["approved BrowserDelta application"]
    ES --> P["ReactProjection"]
    P --> UI["Visible UI"]
    BD --> UI
  end

  X --> CC["CacheCoordinator"]
  G --> CC
  CC --> CF["Cloudflare runtime profile"]
  CF --> WC["Workers Cache API hot local cache"]
  CF --> R2["R2 immutable artifacts"]
  CF --> DO["Durable Objects SQLite transaction shards"]
  CF --> Q["Queues background work"]
  CF --> KV["KV hints/config only"]
Loading

Core Ownership Model

1. RouteSemanticIR / CompiledRouteSchema / StaticSegmentGraph

The build-time compiler owns route facts and topology.

It compiles:

segments
layouts
pages
templates
parallel slots
implicit children slot
default entries
route groups
root-layout boundaries
interception rules
static/dynamic classification hints
canonical ID constructors
canonical key constructors

Example shape:

type StaticSegmentGraph = {
  graphVersion: GraphVersion
  roots: RootLayoutBoundary[]
  segments: Record<SegmentId, SegmentFact>
  layouts: Record<LayoutId, LayoutFact>
  pages: Record<PageId, PageFact>
  templates: Record<TemplateId, TemplateFact>
  slots: Record<SlotId, SlotFact>
  defaults: Record<SlotId, DefaultFact | null>
  interceptionRules: Record<InterceptionRuleId, InterceptionRule>
  dynamicInputs: DynamicInputFact[]
}

The compiler may emit conservative purity/cache hints:

type RenderPurity =
  | "static"
  | "param-static"
  | "search-sensitive"
  | "cookie-sensitive"
  | "header-sensitive"
  | "auth-sensitive"
  | "draft-sensitive"
  | "uncacheable"

These hints are not permission to be reckless. Runtime outcomes can always downgrade cacheability. If a render observes cookies, headers, auth, draft mode, dynamic fetches, thrown boundaries, or other dynamic inputs, the cache model must encode a safe dimension or mark the response private/uncacheable.

Compile facts, not every possible transition. A full generated transition automaton is not the next move. The compiler should produce stable facts, graph structure, canonical constructors, and conservative classifications. Runtime transition decisions remain in the kernel.

1b. DataDependencyIR / ResourceDependencyGraph

RouteSemanticIR says what can exist. ResourceDependencyGraph says what a render, payload, cached artifact, stream chunk, prefetch, or hidden route depends on and what can invalidate it.

Owns:

route graph IDs
segment/layout/page dependency ownership
params/search dependency fingerprints
cookies/headers/auth/draft fingerprints
cache tags and soft path tags
server-action invalidation effects
HTML/RSC renderEpoch pairing dependencies
stream chunk dependencies
prefetch and Activity resource ownership

Rule:

CacheVariant proves whether two outputs are equivalent.
ResourceDependencyGraph proves what can invalidate that equivalence and who owns the resource lifetime.

It must not decide visible commits, browser mutation, or wire encoding.

2. RouterState And RouteSnapshot

RouterState owns continuity.

It must never be reconstructed from wire keys.

type RouterState = {
  visibleSnapshot: RouteSnapshot
  previousSnapshot: RouteSnapshot | null
  activeOperation: OperationRecord | null
  visibleCommitVersion: number
  reachability: ReachabilityIndex
}

RouteSnapshot keeps facts that must change together:

type RouteSnapshot = {
  id: RouteSnapshotId
  routeId: RouteId
  rootBoundaryId: RootBoundaryId
  displayUrl: string
  matchedUrl: string
  params: ParamFingerprint
  search: SearchFingerprint
  mountedSlots: MountedSlotState
  interceptionContext: InterceptionContext | null
  cacheContext: CacheContextFingerprint
  scrollSnapshot?: ScrollSnapshotId
  focusSnapshot?: FocusSnapshotId
}

Without a snapshot, params, search, mountedSlots, interceptionContext, display URL, matched URL, and cache context become separate fields that merely hope to be updated together.

3. RouterEvent

Every router input becomes an event.

type RouterEvent =
  | { kind: "hydrate"; href: string; ssrSnapshot: HydrationSnapshot }
  | { kind: "csrInitialise"; href: string }
  | { kind: "navigate"; href: string; mode: "push" | "replace" }
  | { kind: "traverse"; direction: "back" | "forward"; historyState: unknown }
  | { kind: "refresh"; scope: RefreshScope }
  | { kind: "prefetch"; href: string }
  | { kind: "serverActionSubmitted"; actionId: ActionId }
  | { kind: "flightResponseArrived"; token: OperationToken; result: FlightResult }
  | { kind: "flightChunkArrived"; token: OperationToken; chunk: FlightChunk }
  | { kind: "prefetchFulfilled"; token: OperationToken; payload: PayloadResult }
  | { kind: "serverActionReturned"; token: OperationToken; result: ActionResult }
  | { kind: "operationFailed"; token: OperationToken; error: RuntimeError }
  | { kind: "operationAborted"; token: OperationToken; reason: AbortReason }
  | { kind: "restoreFromBfcache"; persistedState: RouterStateSnapshot }

Initial user intent events do not need to carry an operation token. The lifecycle controller creates operation identity after the event is classified.

Async result events must carry proof that they belong to a specific causal world.

type OperationToken = {
  operationId: OperationId
  lane: OperationLane
  baseVisibleCommitVersion: number
  graphVersion: GraphVersion
  deploymentVersion: DeploymentVersion
  targetSnapshotFingerprint: string
  cacheVariantFingerprint: string
}

A fetch result is not allowed to commit because it finished. It may commit only if it still belongs to the current visible world.

4. routerKernel.reduce

The kernel owns route semantics.

It answers one question:

Given the compiled graph, current router state, and event,
what transition is semantically valid?

Shape:

function reduce(
  schema: CompiledRouteSchema,
  state: RouterState,
  event: RouterEvent,
): RouterReduction

Suggested output:

type RouterReduction = {
  baseGraphVersion: GraphVersion
  baseDeploymentVersion: DeploymentVersion
  baseVisibleCommitVersion: number

  navigationKind: NavigationKind
  routeResolution: RouteResolution
  rootLayoutTransition: RootLayoutTransition

  segmentOps: SegmentOp[]
  slotOps: SlotOp[]

  cacheVariant: CacheVariant
  cacheOps: CacheOp[]
  elementStoreOps: ElementStoreOp[]

  effects: RouterEffect[]
  commitProposal: CommitProposal
  requestContext?: RequestContextDescriptor

  trace: RouterTrace
}

Use commitProposal, not commitDeltas.

The kernel proposes. The lifecycle controller approves. The executor applies.

The kernel must be pure:

no fetch
no cache write
no browser history mutation
no React state mutation
no Worker binding access
no transition promise settlement

5. SegmentRenderPlan

SegmentRenderPlan may remain useful, but it should be a traceable substructure of RouterReduction, not the center of the architecture.

It can explain:

which segments render
which layouts reuse
which slots preserve
which defaults activate
which entries are skipped
which cache variant applies
which root boundary is active
which interception rule applies

It must not add decisions after reduction.

It must not reference raw AppElementsWire keys.

6. NavigationLifecycleController

The lifecycle controller owns time.

It owns:

operation identity
operation lane
base visible commit version
operation token compatibility
terminal state
commit permission
transition promise settlement
RouterState / PreviousRouterState updates after successful commit

Every visible navigation is a transaction:

type OperationRecord = {
  opId: OperationId
  lane: "visible" | "traverse" | "refresh" | "action" | "prefetch" | "recovery"
  baseVisibleCommitVersion: number
  status:
    | "pending"
    | "committed"
    | "superseded"
    | "aborted"
    | "failed"
    | "hard-navigated"
    | "cache-seeded"
}

Lifecycle decision:

type LifecycleDecision =
  | { kind: "commit"; decision: CommitDecision }
  | { kind: "seedCacheOnly"; cacheOps: CacheOp[] }
  | { kind: "reject"; terminalState: OperationTerminalState }

Rules:

newer visible work supersedes older visible work
prefetch never commits visible UI
refresh is a real operation and can be superseded
same-URL commits use visibleCommitVersion, not URL alone
stale candidate commits must not patch visible state
RSC redirects stay inside one operation lifecycle when possible
server-action results are ordered by OperationToken and visibleCommitVersion
older action results may invalidate or cache-seed only; they may not overwrite newer visible state
cross-tab invalidation signals enter as RouterEvent; they never mutate visible state directly

7. CommitDecision And BrowserDelta

Visible browser mutation must go through one gate.

type CommitDecision =
  | {
      kind: "apply"
      browserDeltas: BrowserDelta[]
      transitionMode: "sync" | "transition" | "viewTransition"
    }
  | { kind: "hardNavigate"; url: URL; reason: HardNavigationReason }
  | { kind: "noCommit"; reason: NoCommitReason }

Hard navigation is terminal. It should not sit beside normal browser deltas. If a root-layout boundary is crossed, the browser takes one path.

transitionMode is part of commit authority. The shell may implement it with a synchronous update, React startTransition, or document.startViewTransition, but it must not choose a different mode as a side-channel semantic decision.

If viewTransition is requested but unavailable or unsafe for the current browser/runtime, the executor may degrade only through a lifecycle-approved fallback rule recorded in trace, such as viewTransition -> transition -> sync. Feature detection may select an execution mechanism; it may not silently change commit semantics.

Browser-visible deltas:

type BrowserDelta =
  | { kind: "replaceElement"; elementId: ElementId; payloadRef: PayloadRef }
  | { kind: "reuseElement"; elementId: ElementId; reason: ReuseReason }
  | { kind: "preserveSlot"; slotId: SlotId; fromVersion: number }
  | { kind: "renderSlotDefault"; slotId: SlotId; defaultElementId: ElementId }
  | { kind: "markSlotUnmatched"; slotId: SlotId }
  | { kind: "setRouterTree"; tree: RouterTreeSnapshot }
  | { kind: "setUrl"; url: URL; mode: "push" | "replace" | "traverse" }
  | { kind: "setScroll"; policy: ScrollPolicy }
  | { kind: "setFocus"; policy: FocusPolicy }

Transition settlement is lifecycle/accounting, not visible browser mutation:

type LifecycleSettlement =
  | { kind: "settleTransition"; transitionId: TransitionId; outcome: TransitionOutcome }
  | { kind: "markOperationTerminal"; operationId: OperationId; state: OperationTerminalState }

Do not put server cache writes, Workers cache effects, RSC stream control, prefetch storage, or lifecycle settlement into BrowserDelta.

The split is:

CommitDecision = visible outcome
BrowserDelta = visible browser mutation
RouterEffect = IO request
CacheOp = cache intention/mutation
ElementStoreOp = payload storage mutation
LifecycleSettlement = operation accounting
RouterTrace = explanation only

8. RouterExecutor

The executor owns imperative work.

It may:

run RSC fetches
start prefetch
abort operations
submit server-action continuation
write ElementStore
apply approved BrowserDelta[]
write approved cache entries
update URL/history/scroll/focus
apply LifecycleSettlement
request hard navigation

Executor may apply LifecycleSettlement; it may not derive settlement.

It may not decide:

slot preservation
layout reuse
root-layout hard navigation
interception meaning
cache equivalence
commit permission

The executor is the hand, not the brain.

9. ElementStore

ElementStore owns payload storage.

It stores:

resolved payloads
stream entries
cache-backed entries
wire-entry references
reachability metadata
retention metadata

It does not decide:

route visibility
slot preservation
layout reuse
interception behavior
cache identity
commit permission

Payload references should be indirect:

type PayloadRef =
  | { kind: "wireEntry"; key: AppElementWireKey }
  | { kind: "streamEntry"; id: StreamEntryId }
  | { kind: "cacheEntry"; key: ClientCacheKey }
  | { kind: "artifact"; ref: ArtifactRef }
  | { kind: "resolved"; id: ResolvedPayloadId }

A browser delta says what visible element changed. The store decides how the payload is materialized.

10. ReactProjection

ReactProjection builds the renderable tree.

Input:

RouterState
+ StaticSegmentGraph
+ ElementStore

Output:

React tree

Tree is the correct final shape for React. It is not the correct universal shape for the whole router.

Use the right shape for the job:

graph for topology
event for time
transaction for lifecycle
variant for cache equivalence
delta for mutation
map for payload storage
tree for rendering

Projection contract:

preserved InstanceId => stable React key
stable key shape across releases unless remount is intentional
changing key shape is a visible state-loss bug

11. AppElementsWire

AppElementsWire remains flat.

It owns:

serialization
deserialization
transport
compatibility
merge buffer
wire-key encoding
RSC/HTML boundary output

It does not own:

topology
slot ownership
layout preservation
default behavior
interception
cache identity
navigation lifecycle
visible commits

The rule:

AppElementsWire is how payloads travel.
It is not how the router thinks.

Correctness Oracle

Correctness must mean observable behavior matches a defined contract, not that the architecture feels principled.

Every semantic PR in this track must carry one of these labels:

Next public semantics conformance:
  matches documented or source/test-observed Next behavior

Vinext internal invariant:
  behavior is not user-visible Next API surface, but protects router/cache correctness

Intentional documented divergence:
  Vinext deliberately differs from Next, with the reason and user-visible effect documented

Do not accept unlabelled semantic changes. If a PR changes layout preservation, slot matching, default/unmatched behavior, interception, redirects, refresh, cache reuse, prefetch, streaming, hidden routes, or server actions, it needs an oracle.

Oracle hierarchy:

1. Next.js docs
2. Next.js stable tests
3. Next.js stable source behavior
4. Next.js canary behavior, pinned to a commit
5. Vinext intentional documented divergence

Do not encode one transient canary behavior as permanent truth without naming the pinned commit and the reason. If docs and source disagree, document the conflict and choose the compatibility contract deliberately.

Compatibility-first rule:

Match Next public semantics first.
Diverge only deliberately.
Optimize only after the contract is explicit.

Semantic Cache Coherence

This is the part that decides whether Vinext can safely become faster than Next in this slice.

A cache key is a semantic equivalence claim:

These two requests may reuse the same output.

So CacheVariant must not be a fancy string builder. It should be a typed proof object.

CacheVariant is not the full dependency model. It answers "may these outputs be reused as equivalent?" The ResourceDependencyGraph answers "what inputs, resources, tags, actions, and deployment facts can make that equivalence stale?"

type CacheVariant = {
  schemaVersion: number
  graphVersion: GraphVersion
  deploymentVersion: DeploymentVersion
  responseKind: "html" | "rsc" | "prefetch" | "server-action-refresh"
  renderIdentity: RenderIdentity
  reuseIdentity: ReuseIdentity
  invalidationScope: InvalidationScope
  privacy: "public" | "private" | "uncacheable"
}

Split the concepts:

type RenderIdentity = {
  rootBoundaryId: RootLayoutBoundaryId
  segmentPath: SegmentId[]
  pageId: PageId | null
  routeGroups: RouteGroupId[]
  params: ParamFingerprint | null
  search: SearchFingerprint | null
  dynamicInputs: DynamicInputFingerprint
}

type ReuseIdentity = {
  navigationKind: NavigationKind
  interceptionContext: InterceptionContext | null
  mountedSlots: MountedSlotFingerprint
  cacheContext: CacheContextFingerprint
}

type InvalidationScope = {
  tags: CacheTag[]
  paths: RoutePathFingerprint[]
  authScope: AuthScope
  draftMode: "on" | "off" | "irrelevant"
  privacy: "public" | "private" | "uncacheable"
}

RenderIdentity, ReuseIdentity, and InvalidationScope often overlap. They are not the same thing.

Two responses may render the same bytes but have different invalidation scopes. If that distinction collapses into one string too early, revalidateTag() and revalidatePath() become half-correct.

Route artifact record:

type RouteCacheRecord = {
  variant: CacheVariant
  renderEpoch: RenderEpoch
  htmlRef?: ArtifactRef
  rscRef?: ArtifactRef
  tags: CacheTag[]
  softTags: CacheTag[]
  createdAt: number
  expiresAt: number | null
}

type ArtifactRef = {
  store: "r2"
  key: string
  hash: string
  byteLength: number
}

Invalid state:

HTML from renderEpoch A
+ RSC from renderEpoch B
+ same visible route
= invalid cache pair

Valid cache reuse requires:

same graphVersion
compatible deploymentVersion
compatible CacheVariant
compatible renderEpoch
entry renderEpoch >= required invalidation floor
privacy permits reuse

Cache completeness rule:

If CacheVariant omits a semantic input, stale or private UI can leak.
If CacheVariant includes unnecessary inputs, the cache hit rate collapses.

Variant dimension policy must be explicit before implementation:

mandatory dimensions: graph/deployment/root/route/render identity, privacy, invalidation floor
conditional dimensions: params, search, mounted slots, interception, dynamic inputs observed by the route
bounded dimensions: params/search/header-derived values must be canonical, allowlisted, and bounded
forbidden dimensions: raw cookies, raw headers, bearer tokens, user secrets, unbounded raw user input
fallback dimensions: private, uncacheable, or render fresh when safe public variation is not known

No unbounded raw user input may become a public cache dimension.

Measure CacheVariant cardinality before enabling skip transport. If safe dimensions explode cardinality, degrade to private, uncacheable, cache miss, or fresh render instead of poisoning hit rate.

Build-time classification is a conservative hint, not final authority. Runtime observation may always downgrade cacheability:

cookies() observed -> safe variant dimension or private/uncacheable
headers() observed -> safe variant dimension or private/uncacheable
draft/auth observed -> private/uncacheable unless explicitly modeled
unknown dynamic input -> render fresh

Cloudflare-First Runtime Profile

Cloudflare is the reference runtime, not an adapter bolted on later.

The semantic core must not import raw Cloudflare bindings, but the official Cloudflare runtime should be first-class, optimized, and tested first. Other runtimes may implement the same semantic contracts later; they are not the optimization target for this issue.

Recommended Cloudflare profile:

Workers Static Assets:
  immutable client/build assets deployed with the Worker

Workers Cache API:
  fastest hot local response/artifact cache
  data-center-local and ephemeral
  never the sole source of truth

R2:
  durable immutable HTML/RSC/payload artifacts
  direct binding path for strong global object consistency
  do not rely on cached public R2-domain responses for correctness
  artifact keys are immutable; publish new records instead of mutating live identity
  renderEpoch pairing is semantic compatibility, not a workaround for R2 eventual consistency

Durable Objects with SQLite:
  sharded transaction log
  renderEpoch publish authority
  invalidation floors
  HTML/RSC pairing authority
  operation/cache coordination where strong ordering is required

Queues:
  background revalidation
  cache prewarm
  artifact cleanup
  purge fanout
  idempotent at-least-once jobs
  idempotency keys: route+variant+epoch for prewarm, artifact key for cleanup, scope+epoch for purge fanout

KV:
  hints, config, warm indexes, read-mostly metadata only
  never correctness authority

D1:
  optional queryable metadata/admin/reporting store
  not required on the hot cache path

The runtime layering:

L0 RequestMemo:
  per-request dedupe only

L1 IsolateMicrocache:
  opportunistic memory cache, no correctness authority

L2 Workers Cache API:
  hot local response/artifact cache

L3 R2 Artifact Store:
  durable immutable artifact blobs

Coordinator Durable Objects:
  sharded truth for renderEpoch, invalidation floors, pairing, transactions

Async Queues:
  background revalidation, prewarm, cleanup, fanout

Cloudflare Read Path

request
  -> build CacheVariant
  -> check whether the route class requires coherence lookup
  -> ask Durable Object shard for required invalidation floor / published epoch when needed
  -> check RequestMemo
  -> check IsolateMicrocache
  -> check Workers Cache API
  -> if hit: validate semantic metadata
  -> if miss or invalid: read published artifact refs
  -> fetch immutable blobs from R2
  -> fill Workers Cache API
  -> return response

Fast path is Cache API. Truth path is Durable Objects plus R2.

Do not put the full distributed coherence path on every request by default. Static public hits should not pay for Durable Object and R2 reads when local cache metadata is already sufficient. The runtime should use the cheapest proof that is valid for the route class.

Route/cache class budget:

Route/cache class Hot-path rule
static-public-local-valid Workers Cache API only when embedded graph/deployment/variant/epoch metadata proves freshness. No Durable Object call.
static-public-revalidation-aware Workers Cache API hit is accepted only if embedded epoch/floor metadata proves it is above the required invalidation floor.
tag-or-path-invalidated Durable Object floor check is required unless the request already carries a fresh invalidation token proving the floor.
auth-cookie-header-draft Private, per-request, explicitly dimensioned, or uncacheable. Never public by accident.
server-action-refresh Invalidation token travels with the current operation and participates in the lifecycle commit decision.

Cloudflare Write Path

render fresh HTML/RSC
  -> write immutable blobs to R2
  -> Durable Object transaction publishes RouteCacheRecord
  -> write local response to Workers Cache API with epoch/variant metadata
  -> queue prewarm / cleanup / purge jobs

Use immutable artifact keys:

/artifacts/{deploymentVersion}/{graphVersion}/{renderEpoch}/{hash}.rsc
/artifacts/{deploymentVersion}/{graphVersion}/{renderEpoch}/{hash}.html

Do not overwrite live artifact identity.

Cloudflare Invalidation Path

server action / revalidateTag / revalidatePath / draft/auth boundary
  -> route to CacheCoordinator Durable Object shard
  -> transaction increments invalidation epoch/floor
  -> return invalidation token to current request
  -> enqueue background cleanup/prewarm

Shard coordination by stable scope. Do not use one global Durable Object.

Example shard keys:

appId + deployment family + tag hash
appId + deployment family + path hash
appId + route identity hash

Hot-shard rules:

No global coordinator Durable Object.
High-traffic tags must not collapse all reads/writes onto one unbounded shard.
Shard by the smallest coordination atom that preserves correctness.
Allow partitioned or hierarchical epoch floors when one tag/path becomes hot.
Prefer local embedded epoch metadata on public hot reads when it is enough proof.
Measure Durable Object calls and p95/p99 separately for tag/path-invalidated routes.
Use location hints or Smart Placement only as latency tools, not correctness assumptions.

Hot-shard or coordinator failure must degrade safely:

serve stale only when policy explicitly allows it
otherwise render fresh/private
or treat as cache miss
or noCommit / hardNavigate when visible state cannot be proven safe
never silently reuse because the coordinator is unavailable or slow

KV may mirror hints for speed, but Durable Objects own authoritative invalidation state.

Cross-Tab Coherence

A tab can learn that another tab changed the route/cache world, but it must not patch UI through a side channel.

Allowed signal sources:

BroadcastChannel for same-browser tabs
focus / visibilitychange / pageshow checks
optional Durable Object or service subscription later
polling when stronger live subscription is not available

Rule:

Cross-tab signals emit RouterEvent or cache-invalidation events.
They never apply BrowserDelta, mutate RouterState, or rewrite ElementStore directly.

Deployment Compatibility

Graph-minted IDs are semantic identity only inside one graph version.

Rules:

Same graphVersion:
  reuse may be allowed if CacheVariant, renderEpoch, invalidation floor, and privacy match.

Compatible graphVersion:
  reuse requires an explicit generated compatibility/alias map.

Incompatible graphVersion:
  reject, render fresh, or hard navigate.

Never reinterpret an old ElementId, SlotId, LayoutId, or cached artifact under a new incompatible graph.

Compatibility/alias maps must be generated build artifacts, not hand-authored policy. Humans should review the generated compatibility explanation, not manually declare old and new IDs equivalent.

Deployment publish order matters:

Build graph/schema/client assets.
Publish immutable Static Assets and any precomputed R2 artifacts.
Publish generated compatibility metadata.
Publish deploymentVersion last.

If deployment metadata points to an artifact that is missing or incompatible, the safe fallback is render fresh or hard navigate, not reuse.

Deployment skew cases that must be handled:

hydration from deployment A, navigation against deployment B
prefetch from deployment A, visible state on deployment B
RSC artifact from old graph after schema changed
HTML artifact from one deployment paired with RSC from another
hidden route retained across incompatible graph/deployment
stream chunk from superseded graph/deployment

Cloudflare Compatibility Rules

No cache entry is reusable unless graphVersion, deploymentVersion, CacheVariant, renderEpoch, invalidation floor, and privacy are compatible.

HTML and RSC artifacts for one visible route must come from compatible renderEpochs.

KV may not be used as the authoritative source for invalidation floors, current renderEpoch, HTML/RSC pairing, or private/auth cache decisions.

Workers Cache API hits must still be validated against semantic metadata.

R2 artifact keys should be immutable. Publish new records instead of mutating live artifact identity.

Durable Objects must be sharded by app/route/tag/path scope, not one global coordinator.

Queue jobs must be idempotent because delivery can be at least once.

Skip Transport: Performance From Proof-Backed Omission

The server should not render or send entries the client claims to have and the server can verify.

ClientReuseManifest is an untrusted hint. The client claims; the server, kernel, and cache coordinator prove. Treat it like If-None-Match: useful for avoiding work, never authoritative.

Client sends a reuse manifest:

type ClientReuseManifest = {
  graphVersion: GraphVersion
  deploymentVersion: DeploymentVersion
  visibleCommitVersion: number
  entries: {
    elementId: ElementId
    payloadHash: string
    cacheVariantHash: string
    mountedSlotFingerprint: string
    renderEpoch: RenderEpoch
  }[]
}

Server responds with a render plan:

type ReuseProof = {
  elementId: ElementId
  graphVersion: GraphVersion
  deploymentVersion: DeploymentVersion
  cacheVariant: CacheVariantFingerprint
  dependencyFingerprint: DependencyFingerprint
  renderEpoch: RenderEpoch
  reason: ReuseReason
}

type ServerRenderPlan = {
  render: ElementId[]
  skip: ReuseProof[]
  invalidateClientEntries: ElementId[]
}

Law:

A skipped entry is valid only if the same kernel that would render it
can prove the existing client entry is semantically equivalent.

A client reuse manifest never proves reuse by assertion.
It only gives the server something to verify.

Every skipped entry carries a ReuseProof that trace and tests can inspect.

No byte surgery.

No "the path matches so layout is safe".

No "the key is absent so it must be reusable".

This is where the architecture can beat Next on performance: not by shaving microseconds from a reducer, but by avoiding server work and network bytes entirely.

Streaming

Streaming chunks must carry operation identity.

Reveal granularity must be explicit. A streamed payload should reveal through a known boundary, not through whatever React tree shape happens to exist that day.

type RevealBoundaryKind = "segment" | "slot" | "template" | "userSuspense"

type StreamChunk = {
  opId: OperationId
  graphVersion: GraphVersion
  deploymentVersion: DeploymentVersion
  baseVisibleCommitVersion: number
  targetSnapshotFingerprint: string
  elementId: ElementId
  elementGeneration: ElementGeneration
  revealBoundaryId: RevealBoundaryId
  revealBoundaryKind: RevealBoundaryKind
  chunkSeq: number
  payloadRef: PayloadRef
  cacheVariantHash: string
}

Operation-token compatibility is necessary but not sufficient. A streamed chunk must still target the current snapshot, element ownership generation, and reveal boundary. If the same ElementId has been replaced, hidden, evicted, or re-owned, the old chunk cannot reveal visible UI.

Handling rule:

if (lifecycle.canCommitChunk(chunk)) {
  elementStore.write(chunk.elementId, chunk.payloadRef)
  projection.reveal(chunk.elementId)
} else if (lifecycle.canSeedCache(chunk)) {
  cache.seed(chunk)
} else {
  discard(chunk)
}

A stale chunk may seed compatible cache when explicitly allowed. It may not mutate visible UI. A lifecycle-approved operation can still reject a chunk at the element-generation boundary if ownership has moved.

A commit once applied is not rolled back by a late chunk. Late chunks may extend an approved commit. If a late chunk would invalidate prior visible state, the kernel must produce recovery or hard navigation.

Activity / Hidden Route Preservation

Hidden route preservation should be modeled as retained router state with policy, not as entries that accidentally remain in a map.

type HiddenRoutePolicy = {
  maxRoutes: number
  maxMemoryBytes: number
  preserveKinds: {
    formDrafts: boolean
    scroll: boolean
    focus: boolean
    mediaPlayback: boolean
    clientState: boolean
  }
  eviction:
    | "lru"
    | "navigation-distance"
    | "memory-pressure"
    | "developer-hint"
}

Hidden routes must still obey:

graphVersion
deploymentVersion
root boundary
auth state
draft mode
cache invalidation
memory limits
effect cleanup
style isolation
scroll/focus policy

A hidden route is mounted state with an owner, epoch, invalidation scope, and eviction policy.

RouterTrace

Observability should arrive early, not after the architecture is already large.

A minimal RouterTrace and invariant checker must land with the first kernel/reduction contracts. The expanded harness can grow later, but every semantic-promotion PR should have an explanation oracle from the beginning.

RouterTrace should explain:

why this segment rendered
why this layout reused
why this slot preserved
why this slot defaulted
why this route intercepted
why this cache entry varied
why this operation committed
why this operation was rejected
why the browser hard-navigated
why a cache entry was accepted or rejected
why an entry was skipped

Shape:

type RouterTrace = {
  graphVersion: GraphVersion
  deploymentVersion: DeploymentVersion
  navigationKind: NavigationKind
  baseVisibleCommitVersion: number
  routeResolution: RouteResolution
  rootLayoutTransition: RootLayoutTransition

  segmentOps: SegmentTrace[]
  slotOps: SlotTrace[]

  cacheVariant: CacheVariantTrace
  invalidation: InvalidationTrace

  lifecycle: LifecycleTrace
  commitDecision: CommitDecisionTrace

  emittedWireKeys: AppElementWireKey[]
  terminalState: OperationTerminalState
}

Debugging should not require reverse-engineering flat-map absence.

Non-Negotiable Laws

1. AppElementsWire is transport, never meaning.

2. RouteSemanticIR / StaticSegmentGraph own build-time topology and route facts.

3. Stable graph-minted IDs, scoped by GraphVersion, are semantic identity.

4. RouterState owns continuity.

5. RouterEvent is the only input shape for runtime transitions.

6. routerKernel.reduce() owns route-topology and route-continuity decisions.

7. NavigationLifecycleController owns temporal commit authority.

8. CommitDecision / BrowserDelta own visible browser mutation.

9. CacheVariant owns cache equivalence.

10. ResourceDependencyGraph owns resource dependency, invalidation, and ownership facts.

11. InvalidationScope owns staleness.

12. Cloudflare runtime owns first-class execution of the cache profile, not route semantics.

13. ElementStore owns payload storage, not semantic interpretation.

14. ReactProjection owns final tree shape.

15. Generated code may call typed contracts but may not recreate their logic.

16. Missing payload, missing wire key, missing branch, and cache miss never imply semantic preservation.

17. Every semantic decision has exactly one writer.

18. When a decision moves into the compiler/kernel/lifecycle/cache/runtime owner, delete the old writer in the same PR.

19. Every semantic PR declares its correctness oracle: Next public semantics, Vinext internal invariant, or intentional documented divergence.

20. Unknown state degrades to fresh render, cache miss, discarded chunk, cache-seed only, noCommit, or hard navigation. Never to reuse.

21. ClientReuseManifest is untrusted. The server verifies every claimed reusable entry.

22. Graph compatibility maps are generated build artifacts, not hand-authored equivalence policy.

23. Boundary outcomes are route/cache/commit outcomes, not generic render failures.

24. CommitDecision.apply owns transitionMode; shell code may execute it but not reinterpret it.

25. Cross-tab coherence signals become RouterEvent or cache-invalidation events, never direct visible mutation.

26. Queue jobs must be idempotent because duplicate delivery is allowed.

27. Skip/reuse requires ReuseProof, not just a matching path, key, or client claim.

Sharper authority split:

No route-topology or route-continuity decision outside routerKernel.reduce().
No temporal commit decision outside NavigationLifecycleController.
No browser-visible mutation outside approved CommitDecision / BrowserDelta application.
No cache equivalence decision outside CacheVariant / InvalidationScope.
No Cloudflare storage hit bypasses semantic compatibility checks.

Validity Rules

A reduction or runtime action is invalid if:

rootLayoutTransition = crossRoot
and CommitDecision is not hardNavigate

BrowserDelta preserves a slot
but RouterState cannot prove the slot exists or is retained

slot default is rendered
but StaticSegmentGraph has no default for that SlotId

slot is marked unmatched
but graph resolution found a matching slot route or default

route is intercepted
but event/context is not interception-capable

layout is reused across a crossed root boundary

skip/reuse is emitted without compatible CacheVariant, ResourceDependencyGraph, and ReuseProof

cache read occurs without CacheVariant compatibility

cache entry has compatible CacheVariant but incompatible dependency fingerprint or invalidation ownership

HTML/RSC pair crosses renderEpoch incompatibly

successful RSC payload is paired with error/notFound/unauthorized HTML, or the reverse

cached notFound/forbidden/unauthorized boundary is reused as a successful route payload

global-error from a stale operation mutates visible state

private/auth/cookie/header/draft-sensitive output is cached as public

async result commits with stale visibleCommitVersion

older server-action result overwrites newer visible state instead of rejecting, cache-seeding, or invalidating through lifecycle

CommitDecision.apply is executed with a transition mode chosen outside commit authority

cross-tab signal mutates RouterState, BrowserDelta, or ElementStore directly instead of entering as RouterEvent/cache invalidation

stream chunk mutates visible UI without lifecycle approval

stream chunk mutates visible UI after its target snapshot, elementGeneration, or reveal boundary is no longer current

server accepts ClientReuseManifest entries without verifying graph, deployment, variant, epoch, payload hash, and invalidation compatibility

skip transport is enabled before CacheVariant cardinality is measured and bounded

unbounded raw user input becomes a public CacheVariant dimension

coordinator unavailable/slow path silently reuses cache instead of choosing policy-approved stale, fresh render, cache miss, noCommit, or hard navigation

KV decides invalidation floor, current renderEpoch, HTML/RSC pairing, or private cache safety

queue job lacks an idempotency key for its route/variant/epoch, artifact key, or invalidation scope

generated code constructs RouterReduction, BrowserDelta, or CacheVariant directly

wire-key absence is used as preserve/delete/default/skip proof

uncertain cache/deployment/graph compatibility results in reuse instead of safe fallback

Navigation Kind Semantics

The kernel must branch on explicit navigation kind, not infer it from payload shape.

hydrate:
  may use previous state: SSR snapshot only
  may preserve slots: only if SSR snapshot proves them
  may apply interception: only if encoded in hydration snapshot
  commit: no visible mutation unless recovery is required

csrInitialise:
  may use previous state: no
  may preserve slots: no
  may apply interception: no, unless explicit initial interception exists
  commit: initialise

soft:
  may use previous state: yes
  may preserve slots: yes
  may apply interception: yes
  commit: soft commit

hard:
  may use previous state: no
  may preserve slots: no
  may apply interception: no
  commit: full document navigation

refresh:
  may use previous state: yes
  may preserve slots: yes
  may apply interception: current-context only
  commit: refresh commit

traverse / popstate:
  may use previous state: yes
  may preserve slots: yes, from history-derived state
  may apply interception: history-context only
  commit: traverse commit

prefetch:
  may use previous state: limited
  may preserve slots: no visible commit
  may apply interception: target-context only
  commit: none

server-action refresh:
  may use previous state: yes
  may preserve slots: yes
  may apply interception: current-context only
  commit: action refresh commit

Import And Codegen Restrictions

Dependency direction should be mechanically enforced.

StaticSegmentGraph must not import AppElementsWire.
routerKernel must not import AppElementsWire.
routerKernel must not import Cloudflare bindings.
NavigationLifecycleController must not decode wire keys.
CacheCoordinator consumes CacheVariant, not raw router internals.
Browser runtime consumes CommitDecision, not decoded wire keys.
ElementStore consumes ElementStoreOp / PayloadRef, not route semantics.
Generated code may call schema/kernel/lifecycle/executor/encoder entrypoints.
Generated code may not construct semantic decisions manually.
Generated code may not hand-build wire keys.
Generated code may not hand-build cache variants.
Cloudflare runtime modules may import Cloudflare bindings.
Semantic core modules may not.

Add lint, grep, dependency-boundary tests, or module-boundary tests.

Architecture that is not mechanically defended will be violated eventually. Not because people are careless, but because future contributors will be working under pressure.

Implementation Dependency Layers

This is not a fixed PR budget. Split into as many small, reviewable PRs as necessary.

The dependency order matters more than the count.

Layer 0: Keep The Landed Foundation

Do not redo the flat payload milestone.

Keep:

flat keyed AppElements payload
layout/page/template/slot/route entries
browser merge/replace behavior
SSR/browser deserialization symmetry
root-layout hard navigation behavior, until promoted
absent-key soft-navigation preservation, until promoted

The bridge stays. The city moves off the bridge.

Layer 1: Freeze And Fence The Wire Shell

Goal: make current behavior safe to refactor around.

Deliverables:

compatibility tests for current flat-payload behavior
AppElementsWire codec boundary
forbidden raw wire-key parsing checks
import-direction tests

No behavior change.

No semantic promotion.

No raw wire-key parsing outside codec/encoder boundary.

Layer 2: Compile Route Facts

Goal: make route topology cheap and typed.

Deliverables:

RouteSemanticIR
CompiledRouteSchema
StaticSegmentGraph
graphVersion
graph-minted IDs
canonical constructors
RenderPurity / DynamicInputFact classifications
DataDependencyIR / ResourceDependencyGraph skeleton

Compile facts, not transitions.

Layer 3: Add Runtime Contracts

Goal: define the new vocabulary before moving behavior.

Deliverables:

RouterState
RouteSnapshot
RouterEvent
OperationToken
RouterReduction
CommitProposal
CommitDecision with transitionMode
BrowserDelta
CacheVariant
InvalidationScope
ResourceDependencyGraph
RouteCacheRecord
minimal RouterTrace
minimal invariant checker
correctness-oracle labels

No major behavior change yet.

Layer 4: Add The Boring Pure Kernel

Goal: route semantics pass through one pure reducer.

Deliverables:

routerKernel.reduce() that mirrors current behavior
minimal reduction validation
current AppElementsWire emitted from reduction + ElementStore projection
wire explanation checks
trace output for every promoted decision path

The reducer should be boring. Boring is good here.

Layer 5: Add Lifecycle Transactions

Goal: prevent stale async work from committing visible state.

Deliverables:

OperationRecord
operation lanes
terminal states
visibleCommitVersion
lifecycle approval barrier
server-action ordering rules
traversal/back-forward adapter
transitionMode execution path
stale/superseded/aborted/failed rejection tests

Async results return as RouterEvent.

Only lifecycle-approved commits update visible RouterState.

Prefetch may seed cache, never visible UI.

Layer 6: Promote Semantic Decisions One By One

Goal: delete old semantic writers.

Decision areas:

root-layout hard navigation
boundary outcomes: error/notFound/forbidden/unauthorized/global-error
mounted-slot preservation
default/unmatched slot behavior
interception transition rules
layout reuse/skip intent
server-action refresh semantics
scroll/focus/hash restoration

Boundary outcomes must land before aggressive cache coherence and skip rollout. notFound, error, forbidden, unauthorized, and global-error affect visible commit state, cacheability, streaming, and reuse proof.

Each promotion moves one decision area.

Each promotion deletes the old writer in the same PR.

No dual authority.

Layer 7: Semantic Cache Coherence

Goal: make cache reuse safe before making it aggressive.

Deliverables:

RenderIdentity
ReuseIdentity
InvalidationScope
canonical CacheVariant serialization
RouteCacheRecord
ResourceDependencyGraph
DependencyFingerprint
renderEpoch
HTML/RSC pairing checks
privacy/cacheability downgrade rules
runtime dynamic-input downgrade rules
invalidation floor algebra
cache variant cardinality checks

This layer must land before serious skip transport.

A skip system without cache coherence is a faster way to be wrong.

Layer 8: Cloudflare Runtime Profile

Goal: implement the official Cloudflare hot path.

Deliverables:

Workers Static Assets integration for immutable build/client assets
Workers Cache API hot cache validation
R2 immutable artifact storage
Durable Object SQLite coordinator shards
Durable Object hot-shard and locality budget
Queue-backed revalidation/prewarm/cleanup with idempotency keys
KV non-authority rule
cross-tab coherence event source
version metadata / deploymentVersion integration
Cloudflare runtime trace fields

Cloudflare is first-class here.

The semantic core stays binding-free.

Layer 9: Proof-Backed Skip Transport

Goal: reduce server work and network bytes only when equivalence is proven.

Deliverables:

ClientReuseManifest as untrusted hint
ServerRenderPlan
ReuseProof
skip validation against graph + CacheVariant + ResourceDependencyGraph + renderEpoch + visibleCommitVersion
payload hash / epoch / dependency verification for every claimed reusable entry
CacheVariant cardinality gate
trace explanation for every skipped entry

Law:

No proof, no skip.

Layer 10: Streaming With Operation-Tokened Chunks

Goal: support progressive reveal without stale chunk corruption.

Deliverables:

StreamChunk token shape
ElementStore streaming entries
targetSnapshotFingerprint / elementGeneration / revealBoundary guard
lifecycle approval for streamed reveals
Suspense/reveal boundary granularity tests
stale chunk cache-seeding path
progressive reveal tests

Streaming must not bypass lifecycle commit authority.

Layer 11: Activity / Hidden Route Preservation

Goal: preserve route state with ownership, invalidation, and bounds.

Deliverables:

hidden route model
HiddenRoutePolicy
eviction heuristics
auth/reset/scroll/focus/style/effect cleanup
memory bounds
traversal coherence

Invisible does not mean dead.

Retained does not mean immortal.

Layer 12: Invariant Harness

Goal: make invalid states loud.

Deliverables:

compiled schema fixtures
hostile navigation sequence generator
cache coherence invariants
streaming/lifecycle invariants
generated-code restriction checks
trace redaction checks

This can begin earlier in small form. It should be fully active before skip, streaming, and Activity work are considered complete.

Hot Path Budget

The Cloudflare runtime profile must not turn every request into a distributed coordination exercise.

Track budgets for:

static route hit
dynamic route hit
soft navigation
refresh
server-action redirect
prefetch
stream chunk reveal

At minimum, record:

Workers Cache API reads/writes
Durable Object calls
Durable Object shard hotness and p95/p99 by route/cache class
R2 reads/writes
Queue writes and duplicate/idempotent replays
Worker subrequests
server CPU time
RSC bytes sent
layouts rendered
client remount count
p95/p99 navigation latency
cache variant cardinality
ElementStore memory
hidden-route retained memory
stale commit rejection count

Performance claims should be based on work avoided:

fewer layout renders
fewer RSC bytes
fewer remounts
lower p95/p99 latency
bounded coherence calls on hot paths

Do not claim the architecture is faster merely because it is more formal.

The budget must be reported by route/cache class. A static public local hit that calls a Durable Object on every request has failed the hot-path design even if it is formally correct.

Test Strategy

The most important tests are sequence tests and property-style tests, not snapshots.

Generate apps with:

nested layouts
templates
parallel slots
default slots
unmatched slots
route groups
interception routes
dynamic params
search params
root-layout boundaries
static layouts
cookie/header/auth/draft-sensitive layouts
server actions
concurrent server actions
streaming delays
prefetch races
cross-tab invalidations
back/forward traversals
cache invalidations
deployment changes

Generate event sequences like:

A -> B -> C, with B resolving last

prefetch A
navigate B
old A prefetch resolves

server action resolves after same-URL refresh

server actions A and B resolve out of order

cross-tab invalidation arrives while navigation is pending

back/back/forward while RSC responses stream

cross-root navigation while old chunks arrive

slot mounted
slot unmatched
refresh
action redirect

HTML cache hit but RSC cache miss

RSC cache hit but HTML stale

deployment A client receives deployment B payload

Required invariants:

No stale operation commits visible state.

No visible state mutates without lifecycle-approved BrowserDelta.

No commit runs under a transitionMode chosen outside CommitDecision.

No older server-action result overwrites newer visible state.

No cross-tab signal mutates visible state outside RouterEvent/lifecycle.

No cache entry is read without CacheVariant compatibility.

No HTML/RSC pair crosses renderEpoch incompatibly.

No layout is reused across root-layout boundary.

No slot is preserved unless RouterState proves it existed.

No interception applies without an interception-capable context.

No generated code constructs semantic decisions manually.

No AppElementsWire absence has semantic meaning.

No private/cookie/auth-sensitive response is cached under a public variant.

No KV value is treated as authoritative invalidation or renderEpoch state.

No ClientReuseManifest entry is trusted without server-side verification.

No streamed chunk reveals into a stale target snapshot, elementGeneration, or reveal boundary.

No queue duplicate changes the final cache/invalidation/artifact state.

No uncertain graph/deployment/cache state results in reuse.

Every semantic change names its oracle: Next public semantics, Vinext internal invariant, or intentional documented divergence.

Performance And Correctness Claim

The claim is intentionally narrow.

Do not say Vinext is globally better than Next.js.

Say:

Vinext is designed to become more correct and more inspectable than Next.js
in the App Router navigation/cache slice, while matching Next public semantics
unless divergence is deliberate and documented.

And:

Vinext is designed to beat Next on Cloudflare in selected measured paths
through proof-backed omission, semantic cache coherence, and first-class
Cloudflare runtime execution.

The honest performance story:

Compiler-backed route facts make semantic decisions cheaper.
Kernel + cache model make safe reuse provable.
Cloudflare runtime makes approved reuse fast.
Skip transport and streaming reduce render work and RSC bytes.
Benchmarks decide when faster-than-Next is earned.

Do not claim guaranteed speed before the skip/cache/streaming layers exist and benchmarks prove it.

What Changes From The Current Flat-Map Model

From:

flat map carries payload and implicit meaning
missing keys imply preserve/default/skip
cache paths append their own suffixes
local guards decide stale commit behavior
generated code duplicates route decisions
browser merge logic becomes semantic
HTML and RSC cache entries may drift
edge consistency is treated as infrastructure detail
KV is tempting as a correctness shortcut

To:

flat map carries payload only
compiled graph owns topology
RouterState owns continuity
RouterEvent models time
kernel owns route semantics
lifecycle owns temporal commit authority
BrowserDelta owns visible mutation
CacheVariant owns equivalence
InvalidationScope owns staleness
Cloudflare runtime owns first-class execution
Durable Objects own transactional cache-coherence state
R2 owns immutable durable artifacts
Workers Cache API owns hot local cache
KV owns hints/config only
ElementStore owns payload storage
ReactProjection owns tree output

Success Criteria

The migration succeeds when:

AppElementsWire is never used as semantic input.

Every route-semantic decision comes from routerKernel.reduce().

Every visible browser mutation comes from lifecycle-approved BrowserDelta.

Every applied commit has an explicit transitionMode and lifecycle-approved fallback path.

Every async result carries operation identity and visible commit version.

Every stale operation is rejected, cache-seeded only, or terminally settled.

Every cache read uses CacheVariant compatibility.

Every cache reuse checks ResourceDependencyGraph dependency compatibility.

Every cache entry has graphVersion and deploymentVersion.

Every HTML/RSC pair has compatible renderEpoch.

Every skip is explained by graph proof, cache equivalence, dependency compatibility, and ReuseProof.

Every streamed chunk is operation-tokened and checked against target snapshot / element generation / reveal boundary.

Every cross-tab coherence signal enters through RouterEvent or cache invalidation, not direct mutation.

Every queue job is idempotent.

Every hidden route has owner, epoch, invalidation scope, and eviction policy.

Every ClientReuseManifest entry is treated as untrusted until the server verifies it.

Every graph compatibility map is generated and versioned.

CacheVariant cardinality is measured before skip transport is enabled.

Public CacheVariant dimensions are canonical, allowlisted, and bounded.

Boundary outcomes cannot be paired or reused across incompatible success/error/notFound/unauthorized states.

Generated code cannot recreate semantic decisions.

Missing payload, missing wire key, missing branch, and cache miss have no semantic meaning.

KV never owns correctness-critical cache state.

Out Of Scope For This Migration Stage

Do not add yet:

full generated transition automaton
separate reference interpreter
global strong cache consistency for all content
new public router API surface
detailed optimistic UI / useOptimistic overlay semantics before action lifecycle exists
large behavioral rewrites before lifecycle gating exists
stronger-than-Next performance claims before benchmarks prove them

The pure reducer is the reference for now. A second semantic engine would create the drift this architecture is trying to eliminate.

Related Issues / PRs / References

Final Statement

This architecture is not trying to be a prettier flat map.

It is trying to make the router less magical.

Inputs are explicit. Decisions have one owner. Effects wait for approval. Cache identity is semantic. Invalidation is algebraic. Cloudflare is first-class. Debug traces explain decisions. Tests generate hostile timelines.

Performance comes from certainty:

Because we know this layout is static, skip it.

Because we know this slot was mounted, preserve it.

Because we know this action is stale, reject it.

Because we know this cache variant is compatible, reuse it.

Because we know this graph version changed, hard navigate.

Because we do not know, render fresh.

That last line is the discipline.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions