Skip to content

Glossary

A catalog of re-frame2's nouns, verbs, and concepts.

The Nouns

adapter

re-frame2 renders through one of several React-family rendering libraries — Reagent, UIx, or Helix (its substrate). An adapter is the small map of "glue" functions that wires re-frame2's core to the substrate you picked. It's a value, not the library itself.

You install it once, at boot, by passing it to init!:

(ns app.core
  (:require [re-frame.core :as rf]
            [re-frame.adapter.reagent :as reagent-adapter]))

(rf/init! reagent-adapter/adapter)

To switch substrate you change that one line — require re-frame.adapter.uix or re-frame.adapter.helix and pass its adapter. Nothing else moves: your events, subscriptions, and app-db are substrate-agnostic.

Related: Views, Adapters. Don't confuse it with the substrate — the substrate is the rendering library; the adapter is the value that binds re-frame2 to it.

app-db

The single, immutable map that holds your application's state — one per frame, and the one source of truth your code owns.

You design the map's shape to fit your app, and can optionally guard it with a schema (Malli by default). You never mutate it directly: an event handler returns a new app-db, the framework commits it atomically, and your subscriptions re-derive the views from the new value. State flows in through events and out through subscriptions — never the reverse.

An example shape:

{:cart  {:items [{:sku "BK-1" :qty 2}]
         :open? false}
 :user  {:name "Ada"}
 :route {:page :checkout}}

Note: The framework keeps its own state separately in runtime-db within a frame; see the two partitions.

Related: app-db, Validate with schemas.

path

A vector of keys addressing one location inside app-db, with get-in / assoc-in semantics — [:auth :token] names the :token entry inside the :auth map. Paths are the framework-wide addressing vocabulary: an app-db schema binds to a path, a data-classification mark names one, a flow writes its :output-path, and the path interceptor narrows a handler to one.

Related: app-db.

coeffect

A fact about the world — the current time, a fresh UUID, a value from local storage — that the framework supplies to an event handler as data, so the handler stays a pure function and never reaches out to the world itself. A handler is a pure function from coeffects to an effect map: the world flows in as coeffects and out as effects.

A handler's first argument is its coeffects map. It always carries :db (the current value of app-db). Any other fact must be declared up front in the event's metadata under :rf.cofx/requires, and the framework supplies it:

(rf/reg-event :order/place
  {:rf.cofx/requires [:today]}           ;; :today is a required coeffect
  (fn [{:keys [db today]} _]             ;; the key :today will appear in the coeffects map
    {:db (assoc db :order/date today)}))

You make a coeffect available by registering a supplier for it with reg-cofx:

(rf/reg-cofx :today
  (fn []
    (subs (.toISOString (js/Date.)) 0 10)))

Declaring the world this way — rather than reading it inside the handler — is what makes events pure, testable, and replayable.

Related: Effects, Coeffects.

effect

A single side effect, described as data for the framework to perform — an HTTP request, a navigation, a delayed dispatch, a write to local storage. An effect is a [effect-id config] pair, and effects ride in the :fx vector of an effect map:

[:dispatch-later {:ms 500 :event [:cart/saved]}]   ;; one effect

The event handler only describes the effect; the runtime performs it, via the effect handler you registered for that effect-id with reg-fx. Effects are the output side of an event — the dual of its input coeffects — and keeping them as data, rather than doing them inline, is what makes events pure and testable.

Related: Effects, Coeffects.

effect handler

The function — registered with reg-fx for a given effect-id — that actually performs an effect. The runtime calls it once for each effect in a handler's :fx vector, passing that effect's config. This is where the real, impure work happens (the HTTP call, the navigation, the localStorage write) — which is exactly why it's kept out of the pure event handler: the event handler describes effects as data; the effect handler carries them out.

(rf/reg-fx :cart/save             ;; teach the runtime a new effect
  (fn [_ctx {:keys [items]}]      ;; (fn [ctx args]) — args is the effect's config
    (save-to-server! items)))     ;; and does the impure work

re-frame2 ships handlers for the common effects (:dispatch, :dispatch-later, :rf.http/managed, …); you register your own with reg-fx.

Related: Effects, Coeffects.

effect map

The map an event handler — or a machine action — returns to describe how the world should change. The handler never makes the change itself; it's a pure function that returns this description, and the framework carries it out.

It has two reserved keys. :db is the new app-db value — "replace app-db with this":

{:db new-db}

:fx is a vector of effects — each a [effect-id config] pair describing one side effect to perform (an HTTP call, a delayed dispatch, navigation, local storage, …):

{:db new-db
 :fx [[:http {:url "/api/cart" :method :post}]
      [:dispatch-later {:ms 500 :event [:cart/saved]}]]}

:db and :fx are the only top-level keys application code may return; an unknown key fails loud. Each effect is performed by an effect handler; register your own with reg-fx.

Related: Effects, Coeffects. The :fx key is why effects are often just called "fx".

error record

A failure the framework surfaces as a structured map rather than a thrown, silent, or swallowed error — it fails loud, but as data. Every record is keyed by a reserved :rf.error/* category drawn from a fixed catalogue: traced/listener records carry it under :operation; the few construction-time errors that throw carry it as :rf.error/id in the exception's ex-data. Either way, branch on the category, never on the human-readable :reason (which is prose for people and can change).

{:operation :rf.error/no-frame-context
 :reason    "no frame in scope"}

Error records fan out to your always-on error listeners (Sentry, Datadog, …), so — unlike the dev-only trace surface — they survive production. Build your monitoring on them.

Related: Errors.

event

An inert data vector recording a fact: something happened. You dispatch an event; a registered event handler decides what to do in response — the event itself does nothing.

Most events are user intent — a click, a drag, a route change, a tab switch — but timers, browser APIs, WebSockets, and route loaders raise them too. The first element is an identifier (a namespaced keyword is the norm), optionally carrying facts:

[:event-id & facts]

Prefer a single map payload over positional arguments — it's self-describing and stays stable as the event grows:

[:cart/add-item {:sku "BK-1" :qty 1}]    ;; good — a named payload
[:cart/add-item "BK-1" 1]                ;; placeful and fragile

Because an event is just data, it can be logged, recorded, and replayed — the basis of re-frame2's testability and time-travel.

Related: Introduction.

event pipeline

The fixed stage sequence one dispatched event traverses. It has a write side and a read side, split at the commit — nothing crosses between them except the committed value. The write side runs per event — assembletransformcommitperform; the read side runs once per drain at settle — deriverender.

;; write side (per event):  assemble → transform → commit → perform
;; read side  (per drain):  derive → render

The commit is the seam: the write side is transactional up to it and best-effort after it, and the view renders once, from the committed state — never mid-run.

One traversal of the pipeline is a run; the record a run leaves behind is an epoch. Read the triple as pipeline (the structure) / run (one traversal) / epoch (the record). The to-fixed-point family — running the whole queue before the read side — is a drain.

Related: Introduction. (Older prose called this the event cascade or a turn of the loop; those spellings are retired for the event-traversal sense — the machines cancellation cascade keeps its name.)

run

One traversal of the event pipeline — a single dispatched event carried through every stage, write side then read side. It's the middle term of the triple: the pipeline is the fixed structure, a run is one trip through it, and the epoch is the record that trip leaves. One dispatch = one run = one epoch. (A whole queue run to a fixed point before the read side is a drain, which is many runs but one read side.)

Related: Introduction.

write side

The first half of the event pipelineassembletransformcommitperform — the part that runs once per event and computes and applies the change. It's transactional up to the commit (a throwing handler installs nothing) and best-effort after it. The commit is the seam that ends it; nothing crosses to the read side except the value the commit lands.

Related: Introduction.

read side

The second half of the event pipelinederiverender — the part that runs once per drain, after the queue settles, and brings the screen up to date. It reads only the value the commit landed; it never sees a half-written app-db, and it runs once no matter how many events the drain settled.

Related: Subscriptions, Introduction.

world

The map of declared facts assembled for an event handler to read — everything from the outside the handler is allowed to see, gathered as data. app-db is one entry (always present, under :db), not otherwise special; the clock, a fresh id, a storage read are other entries a handler declares with :rf.cofx/requires. The world is the handler's first argument — its coeffects map — assembled by the pipeline's assemble stage before the handler runs.

;; the assembled world a handler receives — app-db is just one fact in it
{:db {}  :today "2026-07-04"  :new-id #uuid "…"}

A frame is a running world: a world plus the runtime machinery (queue, caches, lifecycle) that keeps it alive and re-assembles it per event.

Related: Effects, Coeffects, frame.

event envelope

The runtime package created when an event is dispatched. This is a low level concept tied to the runtime.

App code dispatches an event (vector), sometimes with extra dispatch options. Before the event is queued, re-frame2 wraps those values into an event envelope. The envelope carries the event vector plus the runtime facts needed to process it: the target frame, where the event came from, tracing ids, per-dispatch overrides, and the durable :rf.cofx record used for replayable coeffects such as :rf/time-ms.

Most application code never reads an event envelope directly. It is the router's internal work item. Event handlers receive the envelope's event vector as their second argument, and receive an assembled map of coeffects as their first argument.

(rf/dispatch [:cart/add {:sku "BK-1"}]
  {:frame :checkout
   :source :ui
   :trace-id :cart/add-click})

;; The router queues an envelope shaped roughly like:
{:event    [:cart/add {:sku "BK-1"}]
 :frame    :checkout
 :source   :ui
 :origin   :app
 :trace-id :cart/add-click
 :rf.cofx  {:rf/time-ms 1781078400123}}

Related: Introduction, Frames, Effects, Coeffects.

event handler

A pure function that computes how a dispatched event should change the world. It takes two arguments — a map of coeffects (the facts it needs, :db among them) and the event vector — and returns an effect map:

(coeffects, event-vector) -> effect-map

It describes changes, it doesn't perform them: typically a :db value ("replace app-db with this") plus any :fx for other side effects. Because it's pure — no IO, no clock, no reading subscriptions; the world arrives only through declared coeffects — it's trivially testable and replayable.

Register one with reg-event:

(rf/reg-event :cart/add
  (fn [{:keys [db]} [_ item]]
    {:db (update db :cart/items conj item)}))

Related: Introduction.

flow

A pure derivation that re-frame2 keeps materialised at a path in app-db. The point: because the derived value lives in app-db, your event handlers can read it as plain state — whereas a subscription's value is only available to views.

You declare the :inputs to watch, a pure :derive function, and the :output-path to maintain; whenever an input changes, the framework re-runs :derive and writes the result, in step with the event pipeline:

(rf/reg-flow
  {:id :cart/total :inputs [[:cart :items]]
   :derive (fn [items] (reduce + (map :price items)))
   :output-path [:cart :total]})

Reach for a flow to collate or collapse many facts into one — e.g. folding several error flags into a single :any-errors?. Inputs may also read framework state (a path under :rf.db/runtime), and flows can be added and removed dynamically via effects.

Related: Flows, toggling a derivation at runtime.

frame

An isolated, running instance of an app. A frame is a running world — a world (the map of declared facts, app-db among them) plus the runtime machinery that keeps it alive: its runtime-db, event queue, subscription cache, and lifecycle.

A frame supplies state; its behaviour comes from an image. Crucially, a frame isolates state, not registrations — the registry is process-global, so the same event handlers, subscriptions, views, effects, flows, and machines run in every frame, each against that frame's own state. Most frames use the default image (all registrations); advanced setups hand a frame a selected one.

Most apps create one frame at boot and then forget about it. But because frames are independent, you can run several app instances on one page: short-lived frames power tests, stories, and per-request server rendering, and a sidecar tool like Xray is just a separate app in its own frame. A frame's identity is carried, not found — each operation reads its frame from scope.

(rf/make-frame
  {:id :app
   :initial-events [[:app/initialise]]
   :images [image1 image2]})    ;; optional: a selected composition of registrations

Related: Frames.

capture-frame

(rf/capture-frame) captures a frame as a value — a frame api: a small bundle of that frame's :dispatch / :dispatch-sync / :subscribe ops, plus the captured :frame id. Carry it across an async boundary — grab one while the frame is in scope, and a later setTimeout, promise, or WebSocket callback can still dispatch into the right frame instead of raising :rf.error/no-frame-context. (capture-frame is the verb; the frame api is the value it returns — spelled lowercase so it never reads as the public re-frame2 API.)

Related: Frames.

frame-provider

The React component that scopes an existing frame to a view subtree, so dispatch/subscribe inside resolve to it. SCOPE-only — roots ensure; providers scope: {:frame existing-id} provides an already-created frame's id down through React context, creating and destroying nothing, and fails loud if the frame is absent. Given an :id (the ENSURE key) it fails loud naming its sibling frame-root. (The everyday expression of frame identity is carried, not found.)

Related: Frames.

frame-root

The React component that ensures a named frame for a view subtree's mounted lifetime — the ENSURE sibling of frame-provider. Keyed by {:id …} (plus any make-frame opts), it creates the frame if absent — at commit, in a client layout effect, so a render React discards creates nothing — reuses it without re-seeding if present (hot reload and StrictMode remounts preserve app-db and never replay :initial-events), and provides its id to descendants. It never destroys the frame on unmount; true ownership is explicit make-frame + destroy-frame!. Given a :frame it fails loud naming frame-provider.

Related: Frames.

hiccup

The plain Clojure data that describes your UI: nested vectors where [:div.card {:on-click f} "Hi"] is a <div>. Because markup is data, not a template language, a view composes and diffs cheaply and even renders to a string on the server — and the substrate turns it into real React elements.

[:ul.cart (for [item items] [:li {:key (:sku item)} (:name item)])]

Related: Views.

image

The selected set of registrations a frame resolves its behaviour against — event handlers, subscriptions, views, effect handlers, flows, machines, and the rest. An image is a value: it's not a running app and holds no state.

Most apps use the default image automatically — "all the registrations already loaded." You name an image explicitly only when different frames need different behaviour: a test frame with fake effects, two examples that reuse the same event ids, or a sidecar tool like Xray.

The rule that ties it to a frame: the image supplies behaviour; the frame supplies state. When a frame starts, its image is resolved into a sealed registration set (its generation) — but most of the time that rule is all you need.

(def checkout-image
  (rf/image {:select-ns {:include ["app.checkout.*"]}}))

(rf/make-frame
  {:id :checkout/story
   :images [checkout-image]})

Related: Images.

generation

The sealed registration set a frame's image selection resolves into when the frame is constructed — the concrete "which handler answers this id" table the frame runs against, frozen as a value. Every make-frame frame carries one (the default image seals into a generation too); re-calling make-frame against the same :id with a new :images vector swaps a frame onto a freshly resolved generation.

Related: Images.

interceptor

A named, reusable wrapper around an event handler — a pair of :before / :after functions for cross-cutting concerns (logging, validation, tracing, undo, injecting effects). Each is a context → context function:

  • :before runs before the handler and can read or adjust its coeffects;
  • :after runs after and can read or adjust the effect map it returned.

Interceptors are registered by id with reg-interceptor (never written inline), and an event opts into them by id:

(rf/reg-interceptor :my-app/logger
  {:before (fn [ctx] ctx)
   :after  (fn [ctx] ctx)})

(rf/reg-event :cart/add
  {:interceptors [:my-app/logger]}    ;; opt in by id
  (fn [{:keys [db]} [_ item]]
    {:db (update db :cart/items conj item)}))

Related: Interceptors.

query vector

The vector you hand subscribe to read a subscription: an id plus any arguments — [:article/page "abc"]. The id picks the subscription; the rest are its arguments, and the whole vector keys the subscription cache, so two equal query vectors share one cached value.

@(rf/subscribe [:cart/count])             ;; id only
@(rf/subscribe [:article/by-id "BK-1"])   ;; id + argument

Related: Subscriptions.

registrar

The single, process-wide table every reg-* form writes to and every lookup reads from, keyed by kind + id. It holds all your registrations — events, subs, effects, views, machines — and every frame shares the one registrar rather than keeping its own copy. That's the mechanism behind "a frame isolates state, not behaviour."

registration

An app's behaviour is defined by the registrations you provide.

A single registration maps an id, usually a namespaced keyword, to a function or configuration the runtime can look up later.

At runtime:

  • a frame supplies isolated state and execution context
  • an image supplies the selected registrations
  • a stream of events drives the runtime, which uses those registrations to decide what to do

For example, this registration says: when the runtime sees the event id :cart/add, use this event handler function.

(rf/reg-event :cart/add
  (fn [{:keys [db]} [_ item]]
    {:db (update db :cart/items conj item)}))

Core registration:

(Frame construction is deliberately NOT a reg-* member — a frame is a live runtime object, not a registered program member. make-frame creates a named frame; frame-root is the ENSURE mount recipe.)

Flows registration:

  • reg-flow registers a flow.

Machines registration:

  • reg-machine and reg-machine* register machines.

Routing registration:

  • reg-route registers a route.

Schema registration:

  • reg-app-schema and reg-app-schemas register Malli schemas for app-db paths.

SSR registration:

  • reg-head registers an SSR head producer.
  • reg-error-projector registers an SSR error projector.

HTTP registration:

  • reg-http-interceptor registers managed-HTTP middleware.

Resource registration:

  • reg-resource registers a resource.
  • reg-mutation registers a mutation.
  • reg-resource-scope registers a named resource-scope resolver.

runtime-db

The framework-owned half of a frame's state — the other side of the two partitions, sitting beside the app-db you own.

re-frame2 keeps its own durable state here: machine snapshots, the current route, resource caches, mutation status, and similar machinery. App code reads it through subscriptions or accessors — never by editing :rf.db/runtime paths directly.

[:rf.db/runtime :rf.runtime/machines :snapshots :auth.login/flow]

Related: app-db, frame. Paths: :rf.db/runtime, children :rf.runtime/*.

schema

A data description of a value's shape — [:map [:sku :string] [:qty :int]] — written in Malli, the data-driven schema library re-frame2 uses by default. Attach one to an app-db path (reg-app-schema), an event, or an HTTP :decode step; the runtime checks it at a named boundary in dev and elides the check in production. Because a schema is itself data, it validates, coerces, and round-trips through tools.

Related: Validate with schemas.

subscription

A named, registered, pure, cached derivation of state — how a view reads what it needs. You declare it with reg-sub; the framework recomputes it only when its inputs actually change (by =), so a view never re-renders for a value that didn't move. Subscriptions compose in layers: some read app-db directly, others combine other subscriptions into a derivation graph.

(rf/reg-sub :cart/count (fn [db _] (count (:items (:cart db)))))

Related: Subscriptions. Casual "sub" is fine; not as a headword. (Need the value inside an event handler? Materialise it with a flow.)

substrate

The React-family rendering layer your app runs on — Reagent, UIx, Helix, or reagent-slim. You pick one and wire re-frame2 to it with an adapter. Because the core is substrate-agnostic, your events, subscriptions, and app-db are identical whichever you choose — only the rendering differs.

;; substrate = the rendering library; the adapter is the value you pass to init!

Related: Use UIx, Helix, or slim.

view

A pure render function from subscription values to hiccup — the Clojure data that describes your UI. Views read derived state and dispatch events on interaction; they hold no business logic. The substrate turns the hiccup into real React elements.

(rf/reg-view cart-badge []
  [:span.badge @(subscribe [:cart/count])])

Related: Views. Use "component" for React-analogy callouts only.

The Verbs

The six pipeline stages — assemble → transform → commit → perform (the write side, per event) and derive → render (the read side, per drain) — are the verbs of the event pipeline; they lead this section, in pipeline order.

assemble

The pipeline's first stage: gather the world an event handler will read — app-db (:db) plus every fact the event declared with :rf.cofx/requires — into one coeffects map, before the handler runs. This is where declared facts (the clock, a fresh id, a storage read) enter as data, so the handler stays pure.

Related: Effects, Coeffects, world.

transform

The pipeline's second stage: run the pure event handler — the assembled world and the event in, an effect map out. It transforms the world into a description of the change ({:db … :fx …}) and performs none of it; the stages after the commit carry that description out.

Related: Introduction, event handler.

commit

The single, deferred, all-or-nothing write of the new app-db — and the seam of the event pipeline. It is the one point the committed value crosses from the write side to the read side; nothing else crosses. Everything before it (assemble, transform) is transactional — the :db your event handler returns is staged and lands once, atomically, after flows run, so a throwing handler or flow installs nothing — and everything after it (perform) is best-effort. No observer ever sees a half-written app-db.

;; the :db you return is staged; it's committed once, atomically — the write/read seam

Related: Introduction.

perform

The pipeline's fourth stage and the last of the write side: run the :fx rows the transform returned, in source order, after the commit. This is the only place the system touches the world — the HTTP call, the navigation, the follow-up dispatch — carried out by the effect handler registered for each id. Past the commit seam it's best-effort: an effect that throws doesn't un-commit the state.

Related: Effects, Coeffects, effect.

derive

The pipeline's fifth stage and the first of the read side: recompute the subscriptions (and the rest of the derivation graph) that watch the changed parts of the committed app-db. Values that come out equal (by =) to last time prune everything downstream. The read side runs once per drain, so derivation happens against settled state, never mid-run. (The public verb you write is subscribe; derive names the stage.)

Related: Subscriptions.

render

The pipeline's sixth and final stage: the views that deref a changed subscription re-run, producing fresh hiccup, and the substrate patches just the DOM that moved. Because it runs once per drain from settled state, the screen never flickers through an intermediate value.

Related: Views.

dispatch

Enqueue an event for a frame to process.

dispatch wraps the event in an event envelope and returns immediately; the event handler runs later, during the pipeline run the event kicks off. (Its synchronous sibling dispatch-sync runs the pipeline now — mainly for tests and boot.)

(rf/dispatch [:cart/add-item {:sku "BK-1"}]
  {:frame :checkout})

Related: event, event envelope, event pipeline.

dispatch-sync

Like dispatch, but it runs the event and normally drains the whole queue to completion before returning, instead of queuing for the next tick. A drain-depth halt or successful exact-incarnation destruction claim is terminal. At a destroy claim an authored callback already on the stack may return and entered authored interceptor :after callbacks may unwind, but its returned framework tail is inert and later ordinary events do not begin. The right call at boot, in tests, and at the REPL — never from inside a running handler (which raises :rf.error/dispatch-sync-in-handler).

(rf/dispatch-sync [:app/initialise])   ;; app-db is committed before the next line

Related: Introduction.

drain / run-to-completion

The runtime normally drains the whole event queue to a fixed point — running the write side of every queued event to completion — before the read side runs. So the write side runs per event, the read side runs once per drain at settle, and the UI updates once, from a settled state, never mid-flight. A drain-depth halt or successful exact-incarnation destruction claim is terminal. At a destroy claim an authored callback already on the stack may return and entered authored interceptor :after callbacks may unwind, but its returned framework tail is inert; later ordinary events do not begin and no read side follows the interrupted event. A drain is normally many pipeline runs sharing one read side.

;; every queued event's write side runs, THEN — once — subs recompute and views render

Related: Run to completion. Hyphenate run-to-completion consistently.

elide

Compile dev-only code out of production via one flag (goog.DEBUG or -Dre-frame.debug). It removes the dev trace surface, the epoch buffer, and schema checks — but the always-on :errors and :events streams survive, so production observability keeps working.

;; goog.DEBUG=false removes the dev trace surface and schema checks

Related: Observability. Name DCE once, then use elide.

init!

The one-time boot call that installs a substrate adapter into the runtime — (rf/init! reagent-adapter/adapter). Idempotent, called once at startup. It does not create a default frame (identity is carried, not found); you register your root frame explicitly.

Related: Adapters.

project (egress)

Run a value through redaction before it leaves the app, via project-egress; direct reads are not auto-projected.

(rf/project-egress value {:frame :app/main :path [:auth]})  ;; redacts at the sink

Related: Keep secrets out of traces.

register

Name your handlers and machinery at boot time with registration forms — reg-event for an event handler, reg-sub for a subscription, and so on.

(rf/reg-event :cart/clear (fn [{:keys [db]} _] {:db (dissoc db :cart)}))

Related: Introduction. There is one reg-event: reg-event-db/-fx/-ctx are gone.

subscribe / derive

Read derived state by name through a subscription; @(subscribe …) both reads the current value and subscribes, so the view re-renders when it changes.

@(rf/subscribe [:cart/count])

Related: Subscriptions.

The Concepts

Effects are data

An event handler returns a description of side-effects — an effect map of data — and the runtime performs them; a pure handler plus data effects is what makes replay, test, and trace possible.

{:fx [[:http {:url "/api/login"}] [:dispatch [:ui/spinner true]]]}

Related: Effects, Coeffects.

Fail loud, not silent

A recognised input that can't be honoured raises a structured error record (:rf.error/*), never a nil or no-op. Keep fail-loud (raise instead of swallow) distinct from fail-closed (deny by default at a boundary).

;; unregistered id, missing cofx, unknown fx → raises :rf.error/*, never returns nil

Related: Errors.

Frame identity is carried, not found

An operation reads its frame from its scope (provider / running handler / captured handle); the runtime never invents one. A rootless call is :rf.error/no-frame-context.

[rf/frame-provider {:frame :app} [app-root]]   ;; scope carries the frame downward

Related: Frames.

The four homes (where state lives)

Subscriptionflowresourcemachine: pick the cheapest that fits, decided by the where-state-lives router. Every other concept defers here for "which one do I use?".

;; cart total → sub (or flow); the article → resource; checkout → machine

Related: Where state lives.

The two partitions

A frame holds app-db (yours) and runtime-db (the framework's), addressed by the projection paths :rf.db/app and :rf.db/runtime; runtime subsystems live under :rf.runtime/*.

[:rf.db/runtime :rf.runtime/resources]   ;; the resource cache IS a runtime-db subsystem

Related: app-db.

The uniform reply

Every managed async surface (HTTP, resources, mutations, route loaders, machine async) completes by dispatching an event carrying the one canonical reply map, never by an awaited value. Its discriminator is the closed :status field — :ok (value at :value), :error (failure at :error), :cancelled, or :stale — the same envelope on every surface, HTTP included. (Different from the resource read sub's :status, whose values are the five lifecycle states :idle/:loading/:fetching/:loaded/:error.)

[:auth/login-reply {:status :ok :value {:token "…"}}]

Related: Managed HTTP.

The derivation graph

The directed graph of pure derivations rooted at app-db, with views at the leaves. Subscriptions, flows, resource reads, route facts, and machine selectors are all nodes on this one graph; the runtime recomputes only along edges whose value actually changed (by =), so an unchanged input prunes everything downstream of it.

Related: Subscriptions.

Data classification

Marking an app-db path (or a payload slot) :sensitive or :large so the framework swaps in a redaction/size sentinel wherever that value would cross an egress boundary — a trace, Xray, an SSR payload, an off-box log — while on-box rendering still sees the real value. Hygiene applied at the boundary (see project (egress)), not security.

Related: Keep secrets out of traces.

Recordable vs ambient coeffects

Two grades of coeffect. A recordable one (the clock, a fresh id) is captured onto the event envelope before the handler runs, so the durable result depends on a recorded value and replays identically. An ambient one is read live and isn't recorded — fine for a display hint, never for anything that feeds a durable write.

{:rf.cofx/requires [:rf/time-ms]}   ;; :rf/time-ms is recordable — stamped on the envelope

Related: Effects, Coeffects.

Observability

re-frame2's observability surface — everything tools read to show you the pipeline. The trace stream and epoch history are dev-only (see elide); the always-on error and event streams survive production. See Observability.

trace stream

The live, in-process feed of trace events the runtime emits at every stage of the pipeline — event dispatched, handler run, sub recomputed, effect fired. Every tool (Xray, Story, the pair MCP) is just a reader of it; there's no second source of truth. Dev-only — elided from production.

trace event

One immutable record on the trace stream: an :operation (what happened), an :op-type (its family), a timestamp, and tags — including the id that correlates a whole pipeline run. Filter by :op-type to slice the stream; the always-on :errors/:events records are the production-surviving subset.

listener

A callback you register (register-listener!) on a named stream — :trace, :epoch, :events, or :errors — fired on each matching emit. One registration is a complete tooling integration; but a listener sees data in the clear, so project it before sending anything off-box.

epoch

The record one pipeline run leaves behind — its trigger event, the before/after app-db, and the run's trace events. The epoch is re-frame2's unit of time-travel: Xray rewinds, replays, and inspects history one epoch at a time. It's the last term of the triple: pipeline (structure) / run (one traversal) / epoch (the record).

;; one dispatch = one run = one epoch (the record)

Epochs live on the dev-only observability surface — they're elided from production builds.

Related: Observability.

time-travel

Restoring a frame to the exact state it held at an earlier epoch — both partitions, in one atomic write, no handlers re-run. It works because each epoch holds the real before/after immutable value; it's the superpower behind Xray's scrubbing and undo.

Xray

The dev inspector: an in-app panel that reads the trace stream and per-frame epoch history so you debug the pipeline, not the DOM.

;; open Xray to step through epochs, inspect app-db, and read each pipeline run

Related: the Xray docs.

Story

The view workbench: render a view's loading, empty, error, and happy states as named variants, each in its own isolated frame, then promote the good examples into tests. Reads the same trace stream as every other tool.

Related: the Story tab, Observability.