EP-0001: Frame App/Runtime Partitions¶
Status: final
finalmeans the decisions are settled. The two-partition frame contract graduated into its normative home,spec/002-Frames.md§The two-partition frame contract; where this EP and the spec differ, the spec governs, and this EP remains the design record. (The:rf.runtime/*children this partition reserves are organized by the follow-on Runtime Subsystem Contract EP inspec/Runtime-Subsystems.md, without reopening this contract.) The fourteen deferred calls were ruled by Mike on 2026-06-08 (see Resolved Decisions). The design-review follow-ups were also closed before finalization: Appendix A's concrete recommendations are disposed by Resolved Decisions 3, 4, 7, and 13, the §Partition Keys naming note, and the follow-on Runtime Subsystem Contract EP; Appendix B's upstream handler-contract premise is settled by Resolved Decision 15. The partition is locked. The two implementation gaps tracked against those settled rulings have now also shipped — see Implementation errata. Finalizing the decisions did not, on its own, assert the implementation was gap-free; the errata ledger below tracked that separately to its close. Historical proposal sections below still preserve superseded alternatives such as the multi-container representations; the Resolved Decisions — including the 2026-06-11rf2-cg7llverrata on Decision 2, which blessed:db-before/:db-afteras the canonical projection slot names — and the graduated specs are authoritative where they conflict with earlier proposal voice.
Implementation errata¶
The EP decisions are final and the implementation has shipped in full: every tracked erratum below is closed. This section is kept as a closed record of the build-completion work that followed the decision-freeze; none of it reopens any ruling. These were implementation gaps against settled rulings, not open decisions. The EP is implementation-complete.
Resolved errata¶
The two build-gaps-against-settled-rulings below are fixed; they are kept here as a closed record and no longer reopen any ruling:
rf2-3939ig(fixed — PR #3707) — framework-authority minting was wired only off:rf/machine?, so routing/SSR framework-authority handlers (which legitimately write runtime-db) tripped the:rf.warning/app-handler-runtime-effectownership diagnostic on every navigation. The fix generalized minting to a reserved:rf/framework-authority?registration-meta key that the routing and SSR facades stamp, honouring Resolved Decision 4's convention-plus-diagnostics stance (the diagnostic now fires only on genuine app-handler runtime writes).rf2-1m6rf1(fixed — PR #3714) — the EP-0002 R3 frame-stamp rename folded into this partition (:frame→:rf.frame/idfor runtime context) had landed additively:assemble-initial-ctxinjected the bare:framecoeffect alongside:rf.frame/id, and internal consumers still read the retired spelling. The fix dropped the bare:framecoeffect, migrated the internal consumers to:rf.frame/id, and pinned the exact coeffect key set with a conformance test (the sanctioned:framesurvivors — public dispatch/subscribe opt, fx-handler ctx, trace tags — are kept, per §Namespacing).
Abstract¶
This proposal defines a frame as owning two durable state partitions:
- app-db: user-owned application data.
- runtime-db: framework-owned durable runtime data.
The event pipeline threads both partitions through one coherent cascade:
{:coeffects
{:db <app-db>
:event [:some/event]
:rf.db/runtime <runtime-db>
:rf.frame/id <frame-id>}
:effects
{:db <next-app-db>
:rf.db/runtime <next-runtime-db>}}
The ordinary :db key remains the app-facing app-db partition. The reserved
:rf.db/runtime key is the framework-owned runtime partition. Full-frame tools
such as SSR hydration, epoch restore, time travel, and Xray may project both
partitions as a single frame-state value:
The purpose is to remove the current ownership footgun where framework runtime
state is stored under :rf/runtime inside the user app-db map, allowing an
ordinary fresh :db return to delete machine, routing, elision, SSR, or tool
state.
Motivation¶
Today, re-frame2 stores per-frame runtime state inside app-db under
:rf/runtime.
That means this ordinary handler:
does not only replace user state. It replaces the whole app-db map, including runtime state. If the previous value was:
{:session/status :authenticated
:rf/runtime
{:machines {:snapshots {:door/main {:state :open :data {}}}}
:routing {:current {:id :route/home}}}}
the handler commits:
and the runtime state disappears.
This is worse than ordinary data loss. Machine snapshots, route state, elision declarations, SSR hydration metadata, epoch records, resource caches, and tool state are live framework facts. Dropping them can leave external handles, queued work, timers, subscriptions, and traces pointing at durable state that no longer exists.
The existing :rf.warning/runtime-state-dropped diagnostic is useful, but it is
not the right final boundary. A warning still asks app code to preserve
framework internals. The design should make ordinary app code unable to delete
runtime state by returning app data.
The current diagnostic also fires only after a durable ordinary :db commit
has dropped a live :rf/runtime subsystem. Legitimate full replacements such
as epoch restore and tool db reset bypass that commit path today, and direct
runtime writes can bypass it as well. This confirms the warning is a
containment measure for the current layout, not the ownership model.
Goals¶
This proposal aims to:
- Make accidental runtime-state deletion structurally impossible for ordinary app handlers.
- Preserve the ergonomic re-frame meaning of
:db: app handlers receive and return the app's data. - Keep app-db and runtime-db committed, restored, hydrated, and inspected as one coherent frame transition.
- Make framework ownership visible through qualified names.
- Preserve time travel, SSR, Xray, pair tooling, schema elision, machines, routing, and future resource caches.
- Give tools and AI agents intentional access to app-db, runtime-db, or the whole frame-state.
- Avoid compatibility aliases while re-frame2 is pre-alpha.
Non-Goals¶
This proposal does not:
- require a particular physical storage layout;
- define a generic N-partition database system;
- change ordinary app subscriptions into runtime-db readers;
- authorize application handlers to write runtime-db directly;
- move every framework-private cache, listener, in-flight handle, or tool buffer into runtime-db;
- settle every epoch record field name;
- redesign machine snapshot internals;
- define the resource query API, except by reserving a runtime partition for it.
Relationships¶
This EP is foundational for other EPs that store framework-owned state.
- Foundational for resource queries. The Resource Queries
EP stores its cache in the framework-owned runtime
partition (
:rf.runtime/resources) this EP introduces, so this partition should land — or at least its key vocabulary be fixed — before resources rely on it. - Substrate for the runtime-subsystem contract. The Runtime Subsystem
Contract EP adopts Appendix A item 5
from this EP and grades the
:rf.runtime/*children this partition creates. Its normative home isspec/Runtime-Subsystems.md; it organizes runtime-db's children without reopening this EP's two-partition frame contract. - Shares a path with the machine
:dataschema EP. The Machine:dataSchema EP touches the same[:rf/runtime :machines :snapshots]path this EP renames to[:rf.runtime/machines :snapshots]. If both land, the EP-0005 machine:dataschema redaction-bridge work sequences after this partition rename so it targets the final path. The two are otherwise independent. - Composes with explicit frame target resolution. Both partitions are frame-owned; resolving the frame target precedes committing or projecting either partition. See the Explicit Frame Target Resolution EP.
Specification¶
Terminology¶
| Term | Meaning |
|---|---|
| frame | The runtime boundary: id, router, queue, reactive container, sub-cache, lifecycle, config, trace ring, epoch history, and durable state partitions. |
| app-db | The user-owned application data partition. It is exposed as the ordinary :db coeffect/effect. |
| runtime-db | The framework-owned durable, serializable runtime partition. It is exposed internally as the reserved :rf.db/runtime coeffect/effect. |
| frame-state | A coherent snapshot/projection containing both app-db and runtime-db. |
| transient runtime state | Framework-owned state outside frame-state because it is host-specific, cache-like, or lifecycle-only, such as HTTP in-flight registries, SSR request/response slots, trace listeners, epoch capture buffers, and sub-cache entries. |
| runtime handles | Non-serializable host handles outside frame-state, such as timers, AbortControllers, listeners, promises, and substrate objects. |
Public docs should teach:
app-db is your data.
runtime-db is re-frame2's durable bookkeeping.
frame-state is the coherent snapshot containing both.
Transient runtime state is implementation/tooling machinery, not part of frame-state unless a future design deliberately promotes a serializable fact into runtime-db.
Partition Keys¶
The event pipeline uses these keys:
| Key | Location | Owner | Meaning |
|---|---|---|---|
:db |
coeffects/effects | app | User app-db partition. Kept unqualified for re-frame compatibility and ergonomics. |
:event |
coeffects | framework/app | The event vector. Kept unqualified because it is inherited re-frame vocabulary. |
:rf.db/runtime |
coeffects/effects | framework | Managed durable runtime partition. |
:rf.frame/id |
coeffects | framework | Current frame id. |
The full-frame projection uses:
| Key | Meaning |
|---|---|
:rf.db/app |
App-db inside a frame-state value. |
:rf.db/runtime |
Runtime-db inside a frame-state value. |
:rf.db/app is not the ordinary app handler key. Ordinary handlers continue to
use :db. :rf.db/app exists so full-frame snapshots can name both
partitions without overloading :db.
Naming note: this table trades CLARITY's one-concept-one-name principle — app-db
now has two spellings, :db in handler context and :rf.db/app in the
frame-state projection — for reserved-namespace discipline (the frame-state
projection is a new framework-owned structure, so its keys are qualified). The
cost is real and accepted; the surviving asymmetry (:db unqualified,
:rf.db/runtime qualified) deliberately mirrors the ownership asymmetry the
partition is built on.
Namespacing¶
Unqualified keys are allowed only where they are inherited from the established re-frame contract. New framework-owned facts, coeffects, effects, and durable paths must be qualified.
Keep inherited keys such as:
Use qualified names for new framework-owned facts:
:rf.db/runtime
:rf.db/app
:rf.frame/id
:rf.frame/epoch
:rf.route/match
:rf.ssr/request
:rf.ssr/response
:rf.trace/call-site
Use qualified names for runtime-db children:
{:rf.runtime/machines <machine-runtime>
:rf.runtime/routing <routing-runtime>
:rf.runtime/elision <elision-runtime>
:rf.runtime/ssr <ssr-runtime>
:rf.runtime/resources <resource-runtime>}
Avoid ambiguous names such as :runtime, :frame, or :rf/frame, where the
value might be an id, object, state map, or context. Prefer attribute-shaped
names such as :rf.frame/id.
This rename is about event context, framework fx/cofx context, and other
runtime-owned maps that carry a frame id as data. It does not, by itself,
rename every existing public :frame option or trace tag. The current
specification deliberately keeps :frame as the public dispatch/subscribe
option and as the canonical trace routing tag. If those surfaces are renamed,
that is a coordinated Frames/Instrumentation change, not an accidental side
effect of this partition.
Pre-Alpha Keyword Cleanup¶
This proposal intentionally chooses final vocabulary now.
The migration cost is real: implementation code, tests, examples, docs, skills, MCP tools, and migration rules that mention old framework-owned keys must be updated. That cost is preferable to carrying compatibility aliases into the public language.
Concrete replacements:
| Old or ambiguous shape | Replacement |
|---|---|
app-db root :rf/runtime |
runtime-db coeffect/effect :rf.db/runtime |
full snapshot app partition :db |
frame-state key :rf.db/app |
event-context or fx-context frame id facts such as :frame or :rf/frame |
:rf.frame/id |
runtime child :machines |
:rf.runtime/machines |
runtime child :routing |
:rf.runtime/routing |
runtime child :elision |
:rf.runtime/elision |
runtime child :ssr |
:rf.runtime/ssr |
future runtime child :resources |
:rf.runtime/resources |
Legacy-key diagnostics should point to the replacement vocabulary. They should not silently normalize the old names.
Event Context¶
A standard event context threads both partitions:
{:coeffects
{:db {:todo/items []}
:event [:todo/add "Write EP"]
:rf.db/runtime {:rf.runtime/machines {}
:rf.runtime/routing {}}
:rf.frame/id :app/main}
:effects
{}}
An ordinary app handler may destructure only the app data it needs:
(rf/reg-event-fx
:todo/add
(fn [{:keys [db]} [_ title]]
{:db (update db :todo/items conj {:title title})}))
Framework interceptors and framework handlers may read or write
:rf.db/runtime:
The runtime partition is in the context because it is part of the causal input and output of a frame transition. It is not hidden from the interceptor system. The contract is that ordinary application code treats it as reserved.
Existing ancillary coeffects such as dispatch source, trace id, call-site
metadata, and other instrumentation inputs may continue to ride in the context.
They are orthogonal to the partition split. The load-bearing change is that the
state-bearing inputs are no longer conflated: :db is app-db,
:rf.db/runtime is runtime-db, and :rf.frame/id names the frame whose
transition is running.
User Handler Semantics¶
reg-event-db handlers receive and return only app-db:
If the frame currently contains:
{:rf.db/app
{:session/status :authenticated
:user/id 42}
:rf.db/runtime
{:rf.runtime/machines {...}
:rf.runtime/routing {...}}}
the handler return value commits as:
The same rule applies to ordinary :db effects from reg-event-fx:
The :db coeffect is app-db, not frame-state. That keeps the user handler
mental model stable: db means the app's data.
Runtime Write Semantics¶
Framework code writes runtime-db through :rf.db/runtime effects, privileged
runtime APIs, or internal interceptors. It does not write runtime state through
ordinary app :db effects.
The top-level event effect map must therefore be widened from the current
closed #{:db :fx} shape to a closed set that includes the reserved
:rf.db/runtime state effect. Shape policing, schema descriptions, docs, and
diagnostics must be updated together. Unknown top-level effect keys should
remain errors or logged-and-skipped according to the existing effect-map
contract; :rf.db/runtime is the only new state-bearing top-level key this EP
introduces.
Examples:
- machine handlers read and write snapshots under
[:rf.runtime/machines :snapshots <id>]inside runtime-db; - route events write current route state under
[:rf.runtime/routing :current]inside runtime-db; - SSR hydration writes metadata under
[:rf.runtime/ssr :hydration]inside runtime-db; - schema-derived elision writes declarations under
[:rf.runtime/elision]inside runtime-db; - future resource queries write cache state under
[:rf.runtime/resources]inside runtime-db.
These writes still participate in one atomic event commit. A cascade can produce both app-db changes and runtime-db changes, and the frame installs the combined result as one coherent transition.
Durable runtime writes must not bypass that transition boundary by directly
swapping the app-db container after :db has already committed. Existing
direct container-write sites, such as machine lifecycle fxs, schema-derived
elision population, SSR hydration helpers, epoch restore/reset helpers, and
test fixtures, must be classified and moved to one of three explicit surfaces:
- an app-db-only replacement;
- a runtime-db-only replacement;
- a full frame-state replacement.
Host handles remain outside frame-state. For example, timers, AbortControllers, listeners, and promise handles are still teardown resources, not serializable runtime-db values. The runtime-db records enough durable facts to reconstitute or clean up those handles, but it does not store the handles themselves.
The durability boundary is intentional. Framework-owned state that is cache-like, host-specific, or request-lifetime remains outside runtime-db unless the design deliberately promotes a serializable fact into frame-state. Examples include SSR request/response accumulators, pending error buffers, head snapshots, streaming continuation registries, HTTP in-flight registries, AbortControllers, retry timers, epoch capture buffers, epoch listeners, trace rings, sub-cache entries, and flow registries or last-inputs dirty-check caches. These surfaces must still be frame-scoped and torn down correctly, but full-frame serialization, hydration, restore, and time travel must not pull them in by accident.
Guardrails¶
Because :rf.db/runtime is part of :coeffects, app code can technically read
it:
That visibility is acceptable. Interceptors, flows, extension APIs, tests, and tools need a uniform context model.
The rule is:
Application code does not mutate
:rf.db/runtimedirectly.
re-frame2 should strengthen that rule with diagnostics:
- warn or fail when a non-framework handler returns
:rf.db/runtime; - warn or fail when ordinary app code registers effects under
:rf.db/runtime; - hard-reject when app schemas try to describe runtime-db paths —
reg-app-schema/reg-app-schemasthrow:rf.error/app-schema-runtime-pathat registration when a path's first segment is a:rf.runtime/*keyword, the:rf.db/runtimecontainer root, or the legacy:rf/runtimeroot (rf2-k0ew8n). This is a category error, not a warnable misuse: app schemas validate only app-db, so a runtime path either detonates every dev commit or silently installs a validator over the wrong partition — there is no behaviour to soft-land and no legitimate caller, so it fails closed. (reg-runtime-schemais the surface for validating runtime-db state.) The earlier "warn" framing here predates the fail-closed hardening campaign and is superseded; - warn when examples or skills teach raw runtime path access;
- provide explicit extension APIs for code that intentionally participates in runtime behavior.
The implementation must define how it distinguishes framework-owned writers from application writers. Acceptable mechanisms include reserved handler ids, registration metadata stamped by framework registrars, or a small explicit extension marker. Namespace heuristics alone are too weak for plugins, tests, and optional artefacts.
This is the "user land / kernel space" analogy. The data lives in the same running system, but ownership is real.
Full-Frame Operations¶
Some operations must install or inspect the whole app/runtime snapshot:
- epoch restore;
- time travel;
- SSR hydration;
- frame reset;
- test fixture install;
- tool-driven replay;
- frame destroy records.
Those operations use explicit full-frame APIs, not ordinary app :db effects.
Current names should be audited rather than mechanically preserved. In the
existing implementation, reset-frame! is a lifecycle operation
(destroy-frame! followed by reg-frame), while reset-frame-db! is a
dev/tool app-db replacement that bypasses dispatch and records a synthetic
epoch. After the partition split, those names must either keep their narrower
meaning in documentation or be paired with explicit full-frame replacement
surfaces. A tool API named as a db replacement should not silently replace
runtime-db.
Possible surfaces:
(rf/app-db-value frame-id)
;; => <app-db>
(rf/runtime-db-value frame-id)
;; => <runtime-db>
(rf/frame-state-value frame-id)
;; => {:rf.db/app <app-db>
;; :rf.db/runtime <runtime-db>}
Names are illustrative. The required distinction is that app-facing APIs return app-db by default, while tools and privileged runtime code request runtime-db or frame-state explicitly.
Full-frame APIs must state which projection they use: app-db only, runtime-db only, frame-state, or frame diagnostics including transient runtime state. The default for serialization, SSR, and time travel is frame-state only; transient side channels require an explicit trusted-local tooling API.
Subscriptions And Flows¶
Ordinary layer-1 app subscriptions receive app-db:
Framework subscriptions read runtime-db through framework helpers:
App code should not reach into runtime-db directly for machine or route state. It should use public framework subscriptions.
The subscription invalidation contract must follow the partition boundary:
- app subscriptions depending on app-db rerun when app-db changes;
- framework subscriptions depending on runtime-db rerun when runtime-db changes;
- subscriptions composed from both partitions rerun when either input changes;
- a runtime-only commit must still update views that read route or machine subscriptions;
- an app-only commit must not require app authors to carry runtime paths in their schemas or subscription code.
This can be implemented with one physical frame-state container, two coherent
containers, or partition-aware dirty flags. The observable requirement is that
runtime-only changes are not invisible to framework subs, and app-only changes
do not make :db mean frame-state.
Flows follow the same partition rule:
- ordinary flow inputs over app data read app-db;
- framework flows may read runtime-db through explicit qualified inputs;
- flow-produced
:dbwrites update only app-db; - flow-produced runtime writes use
:rf.db/runtimeand remain reserved.
If a flow or framework derivation reads both partitions, its dirty-check must key on both input sets. A runtime-db write cannot be hidden from a flow merely because the app-db partition was value-identical.
Flow materialized outputs that model app state remain app-db values; this EP does not move ordinary flow output paths into runtime-db. Flow definitions, topological order, and last-inputs dirty-check caches are runtime bookkeeping. Unless a future design intentionally makes them durable frame-state, restore/replay/rollback must recompute or clear these caches from the restored app/runtime partitions so stale dirty-check rows cannot suppress recomputation.
App Schemas¶
App schemas validate app-db, not the whole frame-state.
The old :rf/runtime schema becomes a runtime-db schema:
Applications can still register schemas for app paths. They do not register app
schemas under :rf.db/runtime.
Current specs and source model the runtime shape as reg-app-schema
[:rf/runtime]. That registration must move out of the application schema
surface. Runtime-db schemas may still exist, but they are framework-owned
validators over runtime-db, not application-owned validators over app-db.
Schema-derived elision remains app-schema-driven: applications mark app paths
with :large? or :sensitive?. The extracted declaration records are runtime
bookkeeping and should be written under :rf.runtime/elision in runtime-db,
not under an app-db path the app can accidentally replace.
Mental Model¶
The explanatory analogy is:
App handlers, ordinary :db effects, app subscriptions, and app schemas
operate in user land.
Machines, routing, SSR, elision, traces, future resources, and runtime bookkeeping live in framework-owned runtime-db.
The frame is the whole running system. Epochs, SSR hydration, reset, and time-travel operate on the whole frame-state because app-db and runtime-db are causally linked. Normal application code crosses into runtime-db only through public framework APIs, not raw map writes.
The formal terms remain frame-state, app-db, and runtime-db.
Examples¶
Fresh app-db return¶
Before this proposal, a fresh app-db return could drop :rf/runtime:
After this proposal, the handler replaces only app-db:
;; before
{:rf.db/app
{:session/status :authenticated
:user/id 42}
:rf.db/runtime
{:rf.runtime/machines {...}
:rf.runtime/routing {...}}}
;; handler return
{:session/status :anonymous}
;; after
{:rf.db/app
{:session/status :anonymous}
:rf.db/runtime
{:rf.runtime/machines {...}
:rf.runtime/routing {...}}}
No preservation code is needed in the application handler.
App and runtime update in one cascade¶
A route transition may update app data and runtime route state in one commit:
{:db
{:page/loading? false}
:rf.db/runtime
(assoc-in runtime-db
[:rf.runtime/routing :current]
{:id :route/account
:params {:id 42}
:query {}
:nav-token nav-token})}
The exact internal effect shape is illustrative. The requirement is one coherent app/runtime transition, not two independently visible writes.
Machine snapshot read¶
Application code should use the public machine subscription:
The machine runtime reads:
inside runtime-db. Application code no longer reaches into app-db at:
Full-frame restore¶
Epoch restore installs both partitions:
It does not go through ordinary :db effect semantics, because ordinary :db
effects replace only app-db.
Rationale¶
The core decision is not that re-frame2 must store everything in one physical map.
The core decision is:
A frame has two durable partitions, and the cascade commits them coherently.
An implementation may store the partitions as two maps, two containers, or one internal aggregate. The public and internal contract is expressed through partition keys in the event context and frame-state projection.
This design preserves the useful property of the current model:
A frame transition remains one coherent app/runtime state change.
It removes the harmful property:
User app code can no longer own or replace framework runtime state through ordinary
:dbreturns.
The result is simpler to teach:
- app-db is user data;
- runtime-db is framework bookkeeping;
- frame-state is the coherent snapshot of both;
- ordinary app code uses
:db; - framework code uses
:rf.db/runtime; - tools can ask for either partition or both.
Backwards Compatibility¶
This is a breaking change.
re-frame2 is pre-alpha, and the project posture favors elegance and correctness
over compatibility shims. The proposal therefore rejects long-lived aliases
such as :rf/runtime, :runtime, :rf/frame, or bare :frame for new
framework-owned facts.
Compatibility work should be limited to:
- migration diagnostics;
- lint rules;
- codemods;
- clear docs;
- tests that catch legacy path usage;
- short-lived warnings during implementation.
The stable vocabulary should be the final vocabulary.
Migration¶
Migration is in-repo only — re-frame2 ships no external alpha — and is mechanical rather than gradual:
- Move runtime subsystems off app-db. Machines, routing, elision, SSR, and
related schemas move from
:rf/runtimepaths under app-db to the framework-owned runtime-db (:rf.runtime/*). Snapshots move from[:rf/runtime :machines :snapshots]to[:rf.runtime/machines :snapshots]. - Classify write authority at call sites. Every existing
[:rf/runtime …]reader/writer is reclassified: ordinary app handlers keep returning:db(app-db only); framework subsystems write runtime-db through privileged framework-authority paths. - Reserve
:rf.db/runtimein the effect map. The closed top-level event effect map (:db,:fx) gains:rf.db/runtimeas a deliberate reserved key writable only by framework-authority handlers. - Update full-frame operations. Epoch records, SSR hydration,
restore-epoch!, reset, and destroy switch from app-db-only snapshots to frame-state projections. - Drive legacy access loud. A short-lived
:rf/runtime-access diagnostic eases the in-repo migration during implementation, then is removed; the stable vocabulary is the final vocabulary (see Backwards Compatibility above).
The detailed staged sequence lives in Reference Implementation and Bead Plan below.
Security And Privacy Considerations¶
The current :rf/runtime location is not only a machine/routing correctness
problem. It is also a privacy problem.
Data-classification and elision state can live under runtime state. If a fresh app-db return drops the mark set, later trace or tool egress may fail to redact values that should be hidden. Moving elision state into runtime-db makes it part of the protected framework partition.
Privacy-sensitive projection should still fail closed:
- off-box egress should not default to raw values when mark state is absent;
- runtime-db should be redacted or selectively projected for SSR and tools;
- Xray and pair tools should distinguish human-local inspection from AI/log egress;
- app schemas should not authorize application code to write runtime-db.
The App/Runtime Partition EP should be coordinated with data-classification and Xray egress work so elision state is treated as privacy-load-bearing runtime state, not incidental tooling state.
Reference Implementation¶
The implementation should proceed in stages.
1. Specify the new contract¶
Update the normative docs:
- Frames: a frame owns app-db and runtime-db partitions.
- Conventions: remove
:rf/runtimeas a reserved app-db key; introduce:db,:rf.db/runtime,:rf.db/app,:rf.frame/id, and:rf.runtime/*. - Runtime Architecture: show app-db and runtime-db as frame-owned partitions committed by one cascade.
- Reactive Substrate: clarify whether adapter containers physically hold one frame-state value or two coherent partition containers.
- State Machines: snapshots move to
[:rf.runtime/machines :snapshots]inside runtime-db. - Routing: the route slice moves to
[:rf.runtime/routing :current]inside runtime-db. - SSR: hydration installs a coherent frame-state.
- Instrumentation/Epoch: records distinguish app-db and frame-state.
- Schemas and Spec-Schemas: app schemas validate app-db; runtime schemas
validate runtime-db; the
:rf/effect-mapshape allows reserved:rf.db/runtimeeffects; flow-output validation language is reconciled with the current pre-install flow-throw atomicity contract.
2. Introduce partition helpers¶
Add internal helpers before moving all call sites:
(frame/app-db-value frame-id)
(frame/runtime-db-value frame-id)
(frame/frame-state-value frame-id)
(frame/replace-app-db! frame-id app-db)
(frame/replace-runtime-db! frame-id runtime-db)
(frame/replace-frame-state! frame-id frame-state)
(frame/commit-frame-transition! frame-id {:keys [app-db runtime-db]})
Names are illustrative. The design point is to stop making every call site know the physical storage shape.
These helpers should become the only durable state write boundary. Existing
uses of raw adapter/replace-container! and frame/swap-frame-db! should be
audited and either rewritten through the helpers or documented as host-handle
teardown that deliberately does not alter frame-state.
3. Change event context and commit semantics¶
Change the dispatch pipeline:
- inject
:dbcoeffect as app-db; - inject
:rf.db/runtimecoeffect as runtime-db; - inject
:rf.frame/id; - remove ambiguous frame/runtime context keys instead of supporting them as parallel spellings;
- widen the closed top-level effect-map shape from
:db/:fxto include reserved:rf.db/runtime, and update the bad-effect-map diagnostic accordingly; - interpret ordinary
:dbeffect as app-db replacement; - interpret reserved
:rf.db/runtimeeffects as runtime-db writes only for framework-owned code paths; - reject or warn if user code returns a frame-state-shaped value under
:db; - preserve pre-install atomicity: handler/interceptor/flow throws leave both partitions unchanged.
The commit path must emit enough change information for both subscriptions and
tools. A runtime-only commit should be visible to framework route/machine subs
and to Xray or pair tooling even when app-db is unchanged. An app-only commit
should preserve the existing app-facing :db semantics and should not require
tools to infer runtime changes from :rf.event/db-changed alone.
4. Move runtime subsystems¶
Move durable runtime subsystems:
| Old path | New path inside runtime-db |
|---|---|
[:rf/runtime :machines] |
[:rf.runtime/machines] |
[:rf/runtime :routing] |
[:rf.runtime/routing] |
[:rf/runtime :elision] |
[:rf.runtime/elision] |
[:rf/runtime :ssr] |
[:rf.runtime/ssr] |
future [:rf/runtime :resources] |
[:rf.runtime/resources] |
In a frame-state projection, those paths appear under :rf.db/runtime.
Machine snapshot-internal keys such as :rf/history, :rf/after-epoch, and
:rf/machine-type can remain inside machine snapshots. This proposal changes
where snapshots live, not their internal open-map shape.
The migration must move the whole runtime-owned subsystem, not only the most
visible leaf. For machines that includes :snapshots, :system-ids,
:spawned, and :spawn-counter or any successor allocator slots. For routing
that includes :current, :pending-navigation, navigation-token counters,
scroll restoration caches, and other per-frame routing internals. For elision
that includes both large-value declarations and sensitive-value declarations.
For SSR, only durable hydration metadata and serializable SSR facts move under
:rf.runtime/ssr. Server-only request/response accumulators, pending-error
buffers, head snapshots, streaming continuation queues, and host adapter state
remain transient side-channel state; they are cleared on frame destroy and
excluded from hydration payloads and epoch restores. Managed-HTTP in-flight
registries, abort controllers, retry/backoff timers, and transport promises
follow the same transient rule unless a future resource-cache design promotes a
serializable fact into :rf.runtime/resources.
5. Update subscriptions, flows, and coeffects¶
Audit:
- layer-1 sub execution;
inject-cofx :db;- any new
inject-cofx :rf.db/runtime; - path interceptors;
- app-db schema validation;
- flow input resolution;
- flow registry and last-input cache alignment;
- flow write effects;
- the
[:rf/machine <id>]sub; - route subs;
- partition-aware sub-cache invalidation;
- elision lookup during trace and wire projection;
- transient SSR, HTTP, epoch, trace, and sub-cache side-channel teardown and projection;
- test helpers that assert app-db shape.
Ordinary app subscriptions continue to see app-db. Framework subs and framework flows read runtime-db through internal helpers or qualified inputs.
6. Update epoch, SSR, reset, and tools¶
Epoch records should make the snapshot unit clear.
Possible shape:
{:frame-state-before <frame-state>
:frame-state-after <frame-state>
:db-before <app-db>
:db-after <app-db>}
Alternatively, keep :db-before / :db-after as app-db projections and add
:frame-state-before / :frame-state-after for full restore. This proposal
does not settle the exact epoch record shape, but the implementation must avoid
ambiguity.
Existing epoch code records :db-before / :db-after as app-db values and
restore-epoch! rewinds to :db-after. Under this proposal, a restore that is
meant to revive machines, routes, elision state, or resources must restore
frame-state, not just the app-db projection. If :db-before / :db-after
remain in the record for tool diff ergonomics, they should be named and
documented as app-db projections.
SSR hydration should serialize and install an allowed frame-state projection,
not a raw runtime dump. The current hydration payload shape uses an app-db
slice plus runtime metadata stashed during :rf/hydrate; the partitioned
design should make the app/runtime split explicit on the wire and should
validate both partitions fail-closed before installation.
SSR hydration allowlists operate over app-db plus the serializable runtime-db
projection. They must not serialize server-only request/response accumulators,
head snapshots, streaming continuation registries, pending error buffers, or
host handles. If the client needs one SSR fact, such as a server render hash,
project that fact into [:rf.runtime/ssr :hydration] and validate it there.
Xray should show app-db and runtime-db as separate views inside one frame. Xray, pair-MCP, and epoch egress should treat runtime-db as its own redactable projection. Off-box projection must redact or omit both app-db and runtime-db according to policy, while transient side-channel state is absent by default and available only through explicit trusted-local diagnostic APIs.
7. Add guardrails¶
Add diagnostics for:
- user app-db containing a legacy
:rf/runtimeroot; - user code registering schemas under runtime-db paths;
- ordinary app
:dbeffects returning a frame-state wrapper; - non-framework handlers returning
:rf.db/runtimeeffects; - old framework-owned coeffect/effect/context keys such as
:rf/runtime,:runtime,:rf/frame, or bare:framewhen they mean runtime-db or frame id; - raw reads of
[:rf/runtime ...]in examples, docs, tests, or skills; - full-frame install attempted through ordinary dispatch;
- durable runtime writes performed through raw app-db container swaps instead of partition helpers;
- transient runtime side-channel state serialized into frame-state, SSR payloads, or epoch records without an explicit projection.
The diagnostics should teach:
app-db is user data. runtime-db lives in the frame runtime partition.
Use [:rf/machine <id>] / route subs / tool APIs instead of raw runtime paths.
8. Update docs, examples, skills, and migration¶
Rewrite examples that seed machine snapshots under :rf/runtime.
Update:
- human guides;
- API docs;
- specs;
- examples and testbeds;
- Xray docs;
- pair-MCP docs;
- re-frame2 skills;
- migration rules.
The human guide should teach:
The frame owns user land and runtime state. Your handlers see user land through
:db. Runtime state is threaded through the context as :rf.db/runtime for
framework machinery and explicit extension points. Time travel restores the
whole frame.
Test Plan¶
Conformance tests should verify:
reg-event-dbreceives only app-db;- ordinary
:dbeffects replace only app-db; :rf.db/runtimeis present in coeffects;:rf.frame/idis present in coeffects;- the top-level effect-map shape accepts reserved
:rf.db/runtimeand still rejects unrelated unknown keys; - ambiguous framework-owned context keys such as
:rf/frame,:runtime, and:rf/runtimeare not emitted as compatibility aliases; - runtime-db survives fresh app-db returns;
- non-framework app handlers cannot write
:rf.db/runtimewithout the planned diagnostic; - runtime-only commits invalidate framework route and machine subscriptions;
- app-only commits do not force ordinary app subscriptions to read or preserve runtime-db;
- full-frame restore restores both app-db and runtime-db;
- machine snapshots move with epoch restore;
- route state moves with epoch restore;
- SSR hydration installs both partitions without double-initializing runtime;
- SSR hydration payloads exclude request/response/head/streaming side channels and host handles while preserving explicit hydration metadata such as a server render hash;
- HTTP in-flight handles, abort controllers, retry timers, epoch capture buffers, trace rings, flow dirty-check caches, and sub-cache entries are not serialized into frame-state;
- flow materialized outputs remain app-db values while dirty-check caches are recomputed or cleared on rollback and restore;
app-db-valuereturns only user app-db;- tool/full-frame APIs return the frame-state projection;
- off-box epoch, Xray, and pair projections redact or omit runtime-db according to projection policy;
- app schemas validate only app-db;
- runtime schemas validate runtime-db;
- legacy
:rf/runtimewrites emit the planned diagnostic; - direct runtime writes cannot bypass the partition commit helper unnoticed;
- Xray can inspect app-db and runtime-db separately;
- frame destroy still records coherent before/after state.
Rejected Ideas¶
Documentation only¶
Keep :rf/runtime inside app-db and document that app handlers must preserve it
when returning a fresh map.
This leaves the footgun active, requires app authors to preserve framework internals, and fails the pre-alpha correctness bar.
Preserve :rf/runtime at the commit boundary¶
Keep the physical shape, but ordinary app :db commits automatically carry
forward the previous :rf/runtime.
This fixes the common accidental drop, but keeps runtime state inside the
user-owned app-db value and makes :db no longer literally mean "the app map".
It may be useful as temporary containment, but it is not the destination.
Reject writes that drop or modify :rf/runtime¶
Keep the current shape but turn the warning into an error when ordinary app handlers drop or modify runtime state.
This prevents silent corruption, but it makes common fresh-map handlers fail and
still asks users to know about :rf/runtime.
Physical frame-state wrapper as the public design¶
Physically store:
inside the frame's reactive container.
This is an acceptable implementation strategy, but the public design should be "two durable partitions committed coherently", not "one wrapper map".
Fully separate runtime store outside frame state¶
Move runtime state outside frame durable state and omit it from frame-state snapshots.
This gives a clean ownership boundary but loses the simple property that time-travel, SSR, and reset operate on one coherent frame-state.
Generic N-partition frame database¶
Generalize frames into arbitrary named durable partitions.
This is more ceremony than the current problem needs and is harder to teach than app-db/runtime-db.
Resolved Decisions¶
The fourteen calls this EP left open were ruled by Mike on 2026-06-08 (also
recorded in bead rf2-h0d6s6). All fourteen are now final and authoritative; the
rulings below settle the design so the partition can be locked. Where a ruling
fixes vocabulary or shape that earlier sections describe as illustrative, the
ruling here is the binding form.
-
Accessor names — fixed. The frame-state readers are
app-db-value/runtime-db-value/frame-state-value, and the mutators arereplace-app-db!/replace-runtime-db!/replace-frame-state!. The illustrative names in §Full-Frame Operations and §Reference Implementation step 2 are adopted verbatim as the public names. -
Epoch record shape — frame-state canonical, app-db projections optional.
:frame-state-before/:frame-state-afterare the canonical snapshot fields; the optional app-db projections kept for tool diff ergonomics are:db-before/:db-after(see the 2026-06-11 errata note below). The canonical-plus-optional-projection shape from §6 is the one adopted.
Errata — 2026-06-11 (Mike, bead
rf2-cg7llv). This decision's original text named the optional app-db projection slots:app-db-before/:app-db-afterand stated "There is no:db-before/:db-after." That naming did not land: the reference implementation, the conformance fixtures, the skill docs, and most ofspec/+docs/guide/ship the projection slots as:db-before/:db-after. Per one-name-per-fact (EP-0007), the de-facto short names are now blessed as canonical — renaming the corpus to the originally-ruled names was the costlier path and is rejected. The canonical-plus-optional-projection shape (frame-state canonical, app-db projection optional) ruled here is unchanged; only the projection slot names are corrected. The "alternatively, keep:db-before/:db-after" option in §6 is therefore the adopted naming.
-
Representation — one physical container plus projection reactions. A frame holds one physical frame-state container; app-db and runtime-db are derived as projection reactions over it. This resolves the representation and invalidation alternatives together (see Decision 7): the "two coherent containers" and "explicit dirty flags" alternatives in §Subscriptions And Flows are not used. This is the single-container model Appendix A §3 recommends.
-
:rf.db/runtimereserved by convention, not a security boundary.:rf.db/runtimeis reserved by convention, not enforced as a capability boundary: app code can technically emit it. Docs and dev-diagnostics frame the key as belonging to the framework and to runtime extensions, and steer ordinary app code away from it, but the framework does not reject an app-authored:rf.db/runtimewrite at the commit boundary. (This declines the authority-at-registration hard- rejection posture Appendix A §2 argues for; the convention-plus-diagnostics stance is the ruled one.) -
Runtime writes — support both whole-value replacement and operation-style writes. Framework runtime writes support both whole-value
:rf.db/runtimereplacement and operation-style (helper / op) writes. Normal subsystem writes prefer ops/helpers; whole-value replacement is for restore, hydration, and reset. This is the "both" branch of the §Runtime Write Semantics question. -
Change-event vocabulary —
:rf.event/db-changedstays app-db-only; add a sibling.:rf.event/db-changedremains app-db-only. A new:rf.event/frame-state-changedis added for partition-tagged frame-state commits. The "widen:rf.event/db-changedwith partition tags" alternative is rejected in favour of the distinct-sibling event. -
Sub-cache invalidation — derives from the one-container/projection model. Partition-aware sub-cache invalidation falls out of the one-container-plus- projection-reactions model (Decision 3): a partition's projection reaction recomputes on commit and propagates only when its slice changed by identity. No explicit dirty flags are introduced unless a specific adapter needs them. This is the projection- equality mechanism described in Appendix A §3.
-
Legacy
:rf/runtime— hard error in the final form. In the final form, a legacy app-db root:rf/runtimeis a hard error. A temporary migration warning is permitted only during the in-repo migration campaign, then removed — consistent with the short-lived-diagnostic posture in §Backwards Compatibility and §Migration. -
reset-frame!— resets the whole frame.reset-frame!resets the whole frame: lifecycle plus both partitions (app-db and runtime-db). It is not an app-db-only reset. -
reset-frame-db!— split into app-db and frame-state surfaces. The currentreset-frame-db!is replaced by an app-db-only surface, namedreset-app-db!(equivalentlyreplace-app-db!per Decision 1), plus a distinctreplace-frame-state!for full-frame replacement. A tool API named as a db replacement no longer silently replaces runtime-db — the concern §Full-Frame Operations raises. -
App schemas — keep
reg-app-schemaand "app-db schema"; clarify scope. The public API namereg-app-schemaand the term "app-db schema" are kept. The docs clarify that an app-db schema validates only the app partition, not the whole frame-state. No rename to "app partition schema". -
Flow defs and last-input caches — transient. Flow definitions and last-input dirty-check caches are transient runtime bookkeeping: they are recomputed or cleared on restore and rollback, never serialized as durable frame-state. Durable flow outputs that model app state remain app-db values. This confirms the §Subscriptions And Flows treatment.
-
runtime-db durable/transient boundary — fixed. runtime-db holds the serializable facts needed for restore, SSR-hydration, and time-travel. Host handles, request slots, trace rings, in-flight HTTP, and dirty caches stay transient (frame-scoped, torn down on destroy, never serialized). This is the "survives epoch-restore and SSR-hydration" line Appendix A §4 names as the single durable/transient principle.
-
Off-box projection policy — redacted/omitted by default. runtime-db is redacted or omitted off-box by default. A trusted-local caller may request richer diagnostics explicitly. This is the fail-closed default in §Security And Privacy Considerations and §6.
-
Handler contract — whole-db return retained, knowingly. Appendix B asked for the scoped/change-describing handler-contract question to be settled before the partition locked. Ruled: re-frame2 keeps
reg-event-db's whole-db return. The re-frame mental model is the product; the general clobber-co-located-owners hazard inside app-db is accepted as the price of(fn [db event] …), and this partition fences only the framework's slice. This is a decision, not a default — a futurescoped-event-handlersEP may reopen it, but the partition was built knowing what it does not fix.
Bead Plan¶
- Decision bead: adopt app-db/runtime-db as two durable frame partitions and record the final key vocabulary.
- Spec bead: update Frames, Conventions, Runtime Architecture, Reactive Substrate, Machines, Routing, SSR, Instrumentation, Schemas, and API docs.
- Helper bead: introduce app-db/runtime-db/frame-state helper functions without changing behavior.
- Event context bead: inject
:rf.db/runtimeand:rf.frame/idinto coeffects while preserving:dbas app-db; update effect-map schemas and shape policing for reserved:rf.db/runtime. - Event commit bead: scope ordinary
:dbeffects to app-db and add privileged runtime/full-frame commit paths; emit partition-aware change signals. - Runtime migration bead: move machines, routing, elision, SSR, and related
schemas from
:rf/runtimeto runtime-db. - Epoch/SSR bead: update snapshot, restore, hydration, reset, and destroy semantics to use frame-state projections.
- Subscriptions/tooling bead: update framework subs, Xray, pair tools, and egress surfaces to distinguish app-db, runtime-db, and frame-state.
- Guardrail/migration bead: reject or warn on legacy
:rf/runtimeaccess, ambiguous context-key use, and unauthorized:rf.db/runtimeeffects. - Docs/examples/skills bead: rewrite human docs, AI specs, examples, and skills to teach the new model.
Recommendation¶
Adopt the two-partition frame model: a frame owns a user-owned app-db (:db)
and a framework-owned runtime-db (:rf.db/runtime), committed coherently by
one cascade and projectable as a single frame-state value for SSR, epoch restore,
time travel, and Xray.
This makes accidental runtime-state deletion structurally impossible for ordinary
app handlers while preserving the ergonomic re-frame meaning of :db, keeping one
coherent app+runtime snapshot, and giving app-db a pure application contract an
agent can read without framework noise. The pre-alpha posture favors the clean
break (no long-lived :rf/runtime aliases) over compatibility shims.
The design-review appendices below are historical review records, not open
issues. Their dispositions are recorded above: the single-container/projection
model (Resolved Decisions 3 and 7), convention-plus-diagnostics write authority
rather than a capability gate (Resolved Decision 4), the durable/transient
boundary (Resolved Decision 13), the :rf.db/app naming tradeoff (§Partition
Keys), the follow-on runtime-subsystem contract (EP-0006), and the retained
whole-db handler contract (Resolved Decision 15).
Source Findings¶
This proposal synthesizes:
ai/findings/2026-06-06.app-db-claude.mdai/findings/2026-06-06.app-db-codex.md
Both findings agree on the destination: application code owns app-db, re-frame2 owns runtime state, and the frame owns both as one coherent frame-state snapshot.
At proposal time, this review also checked the then-current local implementation and specs. The findings below are historical evidence for why the migration was needed, not current build status:
implementation/core/src/re_frame/router.cljcthen built event contexts with:db,:event, and bare:frame; emits:rf.warning/runtime-state-droppedonly after durable ordinary:dbcommits; and treats:dbas the single app-db container write boundary.implementation/core/src/re_frame/events.cljcandspec/Spec-Schemas.mdthen defined the top-level event effect map as closed to:dband:fx, so:rf.db/runtimemust be added there as a deliberate reserved key.- Machines, routing, schemas, SSR, and epoch code then read or wrote
[:rf/runtime ...]paths under app-db. Some write through ordinary:dbeffects, while others used direct container helpers such asframe/swap-frame-db!oradapter/replace-container!; the migration had to classify those call sites before the partition was real. - Instrumentation and schema specs deliberately used bare
:framein public opts and trace tags at the time. This EP's:rf.frame/idrename therefore had to be applied to runtime context/fx context first, with any public:framerename left to the owning Frames and Instrumentation specs. - Epoch records then stored
:db-beforeand:db-afteras app-db values. A partitioned restore needed an explicit frame-state snapshot or an unambiguous sibling shape. - The SSR artefact deliberately keeps request slots, response accumulators, pending error buffers, head snapshots, and streaming continuation registries in side-channel atoms so they do not ride hydration payloads. The partition migration had to keep that privacy and lifecycle boundary.
- Managed HTTP keeps in-flight request handles, abort controllers, retry timers, and actor indexes in registries outside app-db. These are host handles or transient runtime state, not durable runtime-db values.
- Flows materialize outputs into app-db but keep registrations and
last-inputsdirty-check rows in per-frame registries. Restore and rollback semantics had to keep those caches aligned or recompute them. - Epoch, Xray, and pair tooling store histories, capture buffers, listeners, trace rings, and mount attribution outside app-db. This EP's "tool state" language had to mean explicit frame-state projections, not every tool-side cache.
Appendix A — Design review: elegance, simplicity, sophistication¶
Review notes (2026-06-07). Commentary on the proposal above; not normative. Each item is a "consider", not a mandate, and several interlock.
The destination is right and the document is unusually complete. This review does not re-argue the partition (it is correct) — it asks where the design could be more elegant, simpler, more rigorous, or more sophisticated. Those four lenses are re-frame2's own primary lenses (ELEGANCE + CLARITY + CORRECTNESS + GOOD PRACTICE), so the review weighs each push against the project's distinctive ethos: AI-first / spec-is-the-artefact, effects-as-data, loud-failure / reject-misuse, reserved-namespace discipline, the value-oriented reactive substrate, and re-frame2's habit of naming a recurring contract (Managed-Effects, Ownership) rather than re-deriving it. Several pushes below are simply that ethos pointed back at the EP.
One framing the EP itself undersells: the partition is also an AI-legibility / CLARITY win, not only a correctness patch. Once app-db holds nothing but app data, reg-app-schema describes a pure application contract an agent can read without framework noise — the spec-is-the-artefact payoff. The Motivation sells the footgun fix; it should sell the legibility win too.
Five substantive pushes, then minor notes, then what should be left alone.
1. Elegance vs namespace discipline — give app-db one name, or own the two¶
The one aesthetic wart is that app-db has two names: :db in the event context, :rf.db/app in the frame-state projection. But this is a genuine collision between two re-frame2 values, not a simple miss:
;; EP's projection ;; alternative — one name for app-db
{:rf.db/app <app-db> {:db <app-db>
:rf.db/runtime <runtime-db>} :rf.db/runtime <runtime-db>}
- CLARITY (one concept, one name) favours reusing
:dbin the projection, so:dbalways means app-db with nothing to keep in sync. The asymmetry (:dbunqualified,:rf.db/runtimequalified) then mirrors the ownership asymmetry the EP is built on, and rides re-frame2's blessed exception::db/:event/:fxare the inherited unqualified keys, and app-db is exactly that inherited concept. - Reserved-namespace discipline favours
:rf.db/app: the frame-state projection is itself a new framework-owned structure, and the EP's own rule is that new framework-owned surfaces are qualified. By that rule:rf.db/appis the EP applying its own discipline — not the "cosmetic symmetry" I first took it for.
So the EP's choice is principled. The cost it pays is real (two spellings for the one concept users live in); the cost the alternative pays is an unqualified key inside a framework projection. I lean mildly toward one-name :db — app-db is the inherited exception and the surface users touch most — but it is close, and which way it should go is genuinely an ethos-internal call. The actionable point is therefore smaller than "change it": the EP should state which value it is prioritising and why (§Partition Keys currently asserts :rf.db/app without naming the CLARITY cost it is trading away). Owning the tension is the re-frame2 move; leaving it implicit is the only real defect here.
2. Rigour — pin the write-authority mechanism; it is load-bearing¶
The headline guarantee is "accidental runtime deletion is structurally impossible." But the mechanism that makes app-writers distinguishable from framework-writers is deferred (Resolved Decision 4; §Guardrails: "the implementation must define how… namespace heuristics alone are too weak"). Until that mechanism is named, the guarantee is only as strong as a diagnostic — i.e. no stronger than today's :rf.warning/runtime-state-dropped, which the Motivation rightly rejects. The EP's strongest claim currently rests on its least-specified part.
Pin it, and pin it at the cleanest altitude: authority is conferred at registration, by which registrar created the handler. reg-event-db / reg-event-fx mint app-authority handlers; the framework's own registrars (machines, routing, resources, SSR) mint framework-authority handlers. Then:
- an app-authority handler that emits
:rf.db/runtimeis rejected at the commit boundary (a hard:rf.error/*), not "warned" — the footgun cannot fire; :rf.db/runtimeneed not be a freely-writable key in the app-facing effect map at all (it widens the closed effect set only for framework-authority handlers);- the "user land / kernel space" analogy becomes a real capability boundary rather than a convention with diagnostics bolted on.
This also resolves the question §5's extension work raises: a library owning a future :rf.runtime/<lib> child gets runtime-write authority by registering through a framework-blessed registrar — not by namespace luck. Authority-at-registration is the one mechanism that makes both the guarantee and the extension story principled.
It is also the more on-ethos posture in two further ways. Loud-failure / reject-misuse: the EP hedges "warn or fail" (§Guardrails, §7), but pre-alpha re-frame2 rejects misuse rather than tolerating it — authority-at-registration lets these be hard :rf.error/* rejections, not warnings (the warning posture is a back-compat reflex the project has disowned elsewhere). AI-legibility: authority conferred by registrar is enumerable registration metadata — an agent or the AI-Audit can ask "is this handler app- or framework-authority?" exactly as it queries registrar kind today — rather than an opaque runtime check. It extends the registrar-kind model (:event, :resource, :mutation, machine handlers) that already exists, instead of inventing a parallel notion of trust.
3. Simplicity — commit to one physical frame-state value, and partition-invalidation falls out for free¶
The EP keeps representation open (one container / two containers / dirty flags — Resolved Decisions 3 & 7) on the principle "contract over representation." That principle is right in general, but here it defers the single hardest sub-problem — partition-aware reactive invalidation — into "implementation detail", and the reactive substrate is exactly what must not be hand-waved.
Committing to one frame-state value in the reactive container, with two cached projection reactions collapses the problem into the existing reaction machinery:
frame-state (one signal: {:db <app-db> :rf.db/runtime <runtime-db>})
├── app-db = (reaction (:db @frame-state)) ; layer-1 input for app subs
└── runtime-db = (reaction (:rf.db/runtime @frame-state)) ; layer-1 input for framework subs
A runtime-only commit bumps frame-state; the app-db projection reaction recomputes, finds (:db …) identical?, and does not propagate — app subs neither re-render nor recompute. An app-only commit is symmetric. The EP's four invalidation requirements (§Subscriptions And Flows) are then satisfied by reaction-deref equality that re-frame already has, instead of new "partition-aware dirty flags." This is both simpler (no new invalidation machinery) and more concrete (Resolved Decision 7 mostly disappears). The "two coherent containers" option, by contrast, multiplies the substrate plumbing for no contract benefit. I would commit to single-container in the spec and demote the alternatives to "ports may differ if they preserve the projection-equality semantics."
4. Simplicity (document) — state the durable/transient boundary once¶
The durable-vs-transient line is re-derived per subsystem in at least four places (Motivation, §Runtime Write Semantics, §4 Move runtime subsystems, Test Plan, Source Findings), each re-listing machines / routing / elision / ssr / HTTP / epoch. It compresses to one principle plus one table:
A fact lives in runtime-db iff it must survive epoch-restore and SSR-hydration. Everything else (host handles, request-lifetime accumulators, caches, in-flight registries, trace rings) is transient: frame-scoped, torn down on destroy, never serialized.
State that once; make every "what moves / what stays" passage a row in a single table keyed by subsystem. The EP shrinks materially and stops risking drift between its own enumerations.
5. Sophistication — name the "runtime subsystem" as a contract (it is already latent)¶
The EP treats runtime-db's children as an ad-hoc list (:rf.runtime/machines, :rf.runtime/routing, :rf.runtime/elision, :rf.runtime/ssr, :rf.runtime/resources). But every one of them already shares the same five properties:
- a reserved sub-tree of runtime-db;
- a write-authority (its framework code) — §2;
- a read API (subs:
[:rf/machine <id>],:rf.route/*,:rf.resource/state) — never raw paths; - a serialization / elision / projection policy (what hydrates, what redacts);
- a teardown contract (durable facts vs transient handles).
That recurring shape is a runtime-subsystem contract, and naming it is the deepest available altitude. The payoff is threefold: it turns the ad-hoc list into instances of one contract (de-duplicating §4/§6/Test-Plan — see #4); it gives the §5 plugin seam and future resource/integration work a principled home (a library registering a new runtime child is "a new runtime subsystem", first-class, not a special case); and it makes the per-subsystem serialization/elision/teardown rules a single conformance checklist instead of prose repeated five times.
Crucially this is not the "generic N-partition database" the EP rightly rejects: that was user-facing arbitrary top-level partitions. This is an internal organizing contract for runtime-db's own children plus a blessed-extension seam — the same distinction as "the framework has subsystems" vs "users get arbitrary partitions." The EP rejected the wrong neighbour; this is the one worth adopting.
And it is not an imported abstraction — it is re-frame2's own organising habit. Managed-Effects.md already names a recurring shape (the eight properties) so new effect surfaces grade against one checklist; Ownership.md already maps every contract surface to its owning spec. A runtime-subsystem contract is the durable-state analogue of Managed-Effects — the identical "name the shape once, grade instances against it" move, applied to runtime-db's children instead of to effects. Because the AI-Audit already grades surfaces against the Managed-Effects checklist, this contract slots straight into existing tooling: machines / routing / resources / future integrations become enumerable, audit-gradeable instances rather than prose. That is spec-is-the-artefact doing what it is for.
Minor notes¶
- Two prefixes for one area. The parent is
:rf.db/runtimebut its children are:rf.runtime/*. Either justify the split (the children are globally greppable when detached — a real tooling benefit) or align them; right now it reads as two naming schemes for the same region. - Scope creep of
:rf.frame/id. The:frame→:rf.frame/idcontext-key rename is really the EP-0002 (Explicit Frame Target Resolution) concern riding along because both touch the event context. Consider splitting it out so this EP is purely the partition; coupling two renames makes both harder to land and review. - Cost of threading runtime-db every event. Confirm the runtime-db coeffect is injected by reference (persistent structure, no copy) and that an app-only commit performs no runtime re-commit — otherwise every pure app event pays for a partition it never touches. Worth one sentence in §Event Context.
What I would not change¶
- Two partitions, not N — correct; resist generalizing the partition even while naming the subsystem contract (#5).
:dbstays app-db for handlers — the load-bearing ergonomic invariant; keep it (and extend it to the projection per #1).- Contract-over-representation as a principle — right; #3 argues only to commit the representation in the reference impl, not to over-specify it for all ports.
- Rejecting the overlay-protect stopgap as the destination — correct; it is containment, not ownership.
Net¶
The EP lands on the right design (the findings' "A2 → frame-state split"). The four pushes that would make it more elegant / simpler / more rigorous / more sophisticated, in priority order: (2) pin authority-at-registration so "structurally impossible" is earned; (1) give app-db one name (:db in the projection too); (3) commit to single-container frame-state and get partition invalidation from projection-equality; (5) name the runtime-subsystem contract — which also unblocks extension/plugin and resource work cleanly. (3) and (5) interlock with EP-0003 Resource Queries and future runtime-subsystem extensions; landing the authority model (2) is the prerequisite that makes all of them honest.
None of these are imports. Each argues from re-frame2's own values — loud-failure (2), name-the-recurring-contract (5), the value-oriented equality substrate (3), and spec-as-artefact / AI-legibility (3, 4, 5, and the partition itself). The single genuine ethos-internal tension is (1) — CLARITY vs namespace discipline — and the right resolution there is not for this review to crown a winner but for the EP to name the value it trades and why, which is the move the rest of the document already models well.
Appendix B — Questioning the premises¶
Review notes (2026-06-07). Appendix A reviewed the design within the EP's accepted frame ("given a partition, do it well"). This appendix steps outside it: is the partition the right shape at all, or a well-built workaround for an assumption that should be questioned first? Not normative — a challenge to the premises, raised because pre-alpha is the only time it is cheap to raise. The two appendices are complementary: Appendix A is safe to adopt regardless; Appendix B is a decision to make before the partition is locked, because the partition's shape depends on the answer.
Separate two things that get conflated. re-frame2's native values — the causal model (events cause, views are passive), effects-as-data, frames as isolated contexts, AI-first / spec-is-the-artefact, loud-failure toward correctness — are right, and they are the elegance. This appendix does not touch them. The blockers are two inherited v1 assumptions that the native values quietly protect from scrutiny, and that the EP treats as fixed points while redesigning everything around them.
Premise 1 — "a handler returns the whole db"¶
This is the root cause of the entire partition, and the EP is a workaround for it, not a fix of it. The chain is mechanical: handlers return a whole map → the framework needs somewhere to keep runtime state → "one value" puts it in app-db → a fresh-map return clobbers it → so we need a partition, an authority model, and reject-rules. None of that apparatus exists if a handler never holds the whole map.
And the partition fixes only the framework-vs-app instance of a general flaw. "Whole-map-return clobbers co-located owners" is still live inside app-db: in a large app, feature A's (fn [_ _] {:fresh}) handler still wipes feature B's state. The partition fences the framework's slice and leaves the general problem standing — so it is, by Appendix A's own altitude test, a special case layered on shared infrastructure rather than a deepening of it. The general fix is the handler contract: a handler sees and returns its scoped view, or describes its writes, instead of replacing a shared map. Illustrative shapes (the point is the contract, not the spelling):
;; today — replaces everything in the handler's (whole-db) scope
(rf/reg-event-db :session/reset (fn [_db _] {:session/status :anonymous}))
;; scoped view — handler only ever sees/returns its declared slice
(rf/reg-event-db :session/reset
{:owns [:session]}
(fn [session _] {:status :anonymous})) ; cannot touch anything it does not own
;; change-describing — handler never holds the map at all
(rf/reg-event :session/reset
(fn [_ _] {:db/set {[:session :status] :anonymous}}))
Under either, the clobber-co-located-owners bug is structurally impossible — for the framework and for app features — and the partition collapses to a degenerate case (the framework is just one more "owner"). re-frame2 already ships the seed of this: the path interceptor scopes a handler to a sub-tree. The elegant move is to make scoping the model, not opt-in plumbing. Notably, this alternative is absent from the EP's Rejected Ideas — not because it was weighed and dropped, but because it lives one level up from the frame the Rejected Ideas section reasons within.
The honest cost: the whole-db model's appeal is (fn [db ev] (assoc db …)) — the cleanest possible "pure function from old state to new state." A scoped or change-describing contract is more composable and isolation-safe but adds a declaration or a small write-DSL, which is friction in the 95% case. That trade is the real decision — but it is a different and more fundamental decision than "how do we partition," and it should be made first.
Premise 2 — "all rewindable state is one co-located snapshot"¶
Every findings doc and this EP treat the unified time-travel snapshot as a constraint to preserve — it is why runtime must live with app-db. But co-location is assumed necessary, not shown to be. If runtime state were a fold over the event log rather than a stored value, app-db could be purely the app's, no partition needed, and time-travel would be replay-to-N with runtime falling out for free.
This lever is weaker, and the EP should not chase it: replay collides with irreversible effects (a machine that already fired an HTTP request cannot be cleanly re-folded) — the same constraint that correctly killed full fx-rollback (see 013 §Why this asymmetry). So pure event-sourcing is not the answer. But it is worth naming, because the co-location premise is currently assumed away rather than decided, and the decision ("runtime is snapshotted, not derived, because effects make derivation unsound") is a stronger foundation than treating co-location as axiomatic.
The real blocker is a value-precedence, not a value¶
Underneath both premises is an unresolved precedence between two re-frame2 values that point opposite ways here:
"Pre-alpha masterpiece — no back-compat shims, question everything for elegance" vs "preserve the re-frame mental model."
The partition sits exactly on that fault line, and heritage is winning by default, not by decision: :db ergonomics and reg-event-db-returns-the-db are treated as immovable while everything else is up for redesign. The "no back-compat shims" value explicitly grants permission to question the handler contract; the "preserve the mental model" value is what holds it in place. Neither is wrong — but right now the conflict is being resolved silently, which is the one thing a spec-is-the-artefact project should not do with a load-bearing choice.
What this means for this EP¶
The partition is a sound local fix and Appendix A's improvements stand. But adopting it commits to the whole-db contract and forecloses the general fix — so the sequencing matters:
- The handler-contract question is upstream of the partition. Settle it (or consciously defer it) before locking the partition, because if scoped/change-describing handlers are ever adopted, this EP shrinks to a footnote.
- If the answer is "keep the whole-db model" (a legitimate outcome), the EP should say so, and say why — i.e. record that the partition exists because re-frame2 chose to preserve
reg-event-db's whole-db return over the more general scoped contract. That turns an unexamined default into a decision. - This deserves its own page — a sibling
scoped-event-handlersEP — rather than a buried decision, because it is bigger than the partition and several other surfaces (feature isolation, interceptors, flows) depend on the same contract.
The honest counterweight¶
There is a real argument for keeping the heritage: the re-frame mental model is the reason re-frame exists. A "more elegant" framework that nobody recognizes as re-frame may win the design and lose the audience. Familiarity is itself a value, and (fn [db ev] …) is a large part of why people reach for re-frame at all. So preserving the whole-db contract is defensible — possibly even correct. The critique is not "the heritage is wrong." It is: right now the heritage is winning by inertia, and the partition is the elaborate cost of a premise no one has decided to keep. Decide it on purpose.