Skip to content

re-frame2 — API

Type: Reference Reference for the CLJS implementation's API: signatures, status, cross-references. No rationale — per-Spec docs own the why. Pattern-level contracts live in 000-Vision §The pattern and the per-Spec docs. :fx-overrides asymmetry: id-valued at the pattern level; CLJS reference also accepts fn values — see 002 §:fx-overrides.

Conventions

  • Status — exactly one base value, optionally combined with one or more parenthesised qualifiers. The closed set below is the same vocabulary the generated api-manifest.edn curates (its :status field) — the two MUST agree:
    • Base values:
      • v1 (ships in v1).
      • v1 (preserved) (exists in current re-frame; preserved unchanged).
      • v1 (preserved + extended) (exists today; v1 adds new arity or behaviour).
      • EP-NNNN (a surface introduced or reshaped by a named pre-alpha EP, shipping in v1 — e.g. reg-event (EP-0018), reg-interceptor (EP-0022); the canonical lineage is the named EP, cited in the row's Notes).
      • post-v1 lib (design spec in v1 Specs but ships in a post-v1 library).
      • post-v1 (planned, rf2-<id>) (specced normatively but not yet shipped; the impl is tracked by the named bead — per the Projection-maintenance rule below).
    • Qualifiers (parenthesised, combinable): dev-only (elided in production builds — the macro emit site or runtime body, depending on the API); changed, EP-NNNN (a preserved v1 surface a named EP changed — e.g. v1 (changed, EP-0017)); optional capability (ships only when the owning optional artefact is on the classpath, optionally narrowed optional capability, dev/test); internal lowering only (an EP surface retained as a framework-internal lowering seam, not a public authoring form — e.g. EP-0022 (internal lowering only)).
    • Examples: v1, v1 (preserved), v1 (dev-only), v1 (preserved, dev-only), v1 (changed, EP-0017), EP-0018, EP-0022 (internal lowering only), v1 (optional capability), post-v1 lib, post-v1 (planned, rf2-<id>).
    • The re-frame.alpha namespace is dissolved — no APIs in this reference live outside re-frame.core (with the documented per-namespace exceptions: re-frame.test-support and re-frame.test-helpers).
  • Macro/Fn: marked M (macro), Fn or Var. A parenthetical after the marker adds detail: Fn (UIx hook), or M (also Fn on CLJS) for a macro whose name is also a plain-fn value on CLJS.
  • Spec column — names exactly the canonical owning Spec (the per-Spec doc whose contract this API implements). Migration rules and other cross-references are NOT in the Spec column; they appear in the Notes column when relevant.
  • Generated cells — every shipped-var table sits under an <!-- api-manifest:generated <namespace> --> comment. A row whose first cell is one back-ticked var has its M/Fn marker, Status, Tier and Spec cells written from the manifest by the manifest generator (implementation/scripts/api-manifest), whose drift check fails when they differ. A bare var belongs to the comment's namespace, and a qualified one to its qualifier's (rf.adapter.uix/use-sub, re-frame.resources/resource-state). Change those facts in api-manifest-metadata.edn and regenerate. Signature and Notes stay authored, and so does a parenthetical after the M/Fn marker (Fn (UIx hook)). So do the other rows — keywords, configure keys, — cross-references, and planned vars the manifest does not carry yet — and the Spec cell of a var whose owner Ownership does not name (init!, configure!, current-config, features).
  • Configure keys — runtime configuration is uniformly via (rf/configure! {<key> <opts>, …}), a single nested map. Every <key> is enumerated in §Configure keys below; per-area tables call out which keys their APIs read but do not redefine the key's vocabulary.
  • Per-artefact public namespaces. The core surfaces live in re-frame.core. Per-feature artefacts ship their own public namespace; consumers :require the namespace directly. The facade rule, by role: what app code authors is on rf/; what you use to inspect, replay or tool lives in the feature's own namespace. re-frame.core is the small app-developer front porch — registration, dispatch, subscribe, the frame basics, interceptors, lifecycle, configure, and the core trace / egress / registrar-query / app-realm surfaces. The optional-feature registration MACROS stay on the façade (reg-route, reg-flow, reg-app-schema / reg-app-schemas, reg-machine / defmachine, reg-resource / reg-mutation / reg-resource-scope, reg-error-projector, reg-head, reg-http-interceptor) because they capture call-site source-coords and have no owned-namespace macro form — registration stays central, and authoring stays a single import. So do the feature pieces written inside handlers and views: route-link (a view component) and resolve-resource-scope (called from a logout or account-switch handler). The optional features' inspection, history, tooling and other non-authoring helpers are NOT on the façade; reach them through their owning namespace: re-frame.schemas (app-schemas / app-schema-meta / app-schemas-digest / set-schema-fns! / schema-fns / default-schema-fns), re-frame.machines (reg-machine* / machine-transition), re-frame.routing (match-url / route-url), re-frame.resources (resource-state / mutation-state), re-frame.epoch (epoch-history / current-epoch-id / restore-epoch! / replay-epoch! / replace-frame-state!). The registrar INVERSE stays on the façade with the registrations it undoes, as the one kind-keyed clear (§Clearing registrations): it dispatches across four optional artefacts and core, so no owning namespace could carry it, and the façade row is what turns an absent artefact into a documented :rf.error/<artefact>-artefact-missing rather than a hard require failure. The SSR QUERY surface is reached at home too (loading re-frame.ssr is what installs the SSR runtime, so every SSR app already names the artefact namespace):

    Namespace Artefact Surfaces
    re-frame.core core the registration / dispatch / subscribe / interceptor / lifecycle / configure surfaces, every optional feature's registration macros, and the feature pieces app code authors (route-link, resolve-resource-scope); epoch-history re-exports re-frame.epoch/epoch-history, and the epoch listener stream is reached through the core-native (register-listener! :epoch …) verb, not a per-channel re-export. SSR contributes only its two REGISTRATION macros (reg-head / reg-error-projector, rowed in §Registration like every other artefact's registrar); its query surface is reached at home on re-frame.ssr / re-frame.ssr.head. Façade surfaces of an optional artefact activate when it is on the classpath; absent artefacts surface :rf.error/<feature>-artefact-missing errors. Epoch is not an optional artefact: it ships in core, and its doors degrade silently only in a build that elides it (per Tool-Pair §Time-travel).
    re-frame.test-support core assert-path-equals, flush-views!, poll-until, fixture machinery (per §Testing). Runtime-state axis — registrar, frames, app-db, drain. assert-path-equals mirrors the :rf.assert/path-equals Story event. View-tree assertions live in the sibling re-frame.test-helpers.
    re-frame.test-helpers core View-assertion helpers — hiccup-walk (find-by-testid / find-by-attr family, text-content, extract-handler, invoke-handler), the testid authoring helper, and the expand-tree walker (per §Testing — View-assertion helpers). View-tree axis — hiccup data, testids, attached handlers. Runtime-state assertions live in the sibling re-frame.test-support.
    re-frame.ssr day8/re-frame2-ssr render-to-string, project-error, head-model->html, hydrate! (per §SSR). reg-head, head-model and default-head are defined in the sibling re-frame.ssr.head, which consumers (:require [re-frame.ssr.head :as head]) directly; the reg-head REGISTRAR also rides the re-frame.core façade. re-frame.ssr re-exports head-model and head-model->html from that sibling, so the whole read side of the head contract sits on the same door as render-to-string.
    re-frame.ssr.ring day8/re-frame2-ssr-ring the Ring host-adapter (default-html-shell, trusted-shell hooks per Spec 011); ssr-handler's :renderer construction opt is the render-body seam — (fn [{:keys [frame-id request opts]}] → {:body-html …}), default the local :root-view render (Spec 011 §HTTP response contract).
    re-frame.schemas day8/re-frame2-schemas app-schemas, app-schema-meta, app-schemas-digest, set-schema-fns!, schema-fns, default-schema-fns (per §Schemas).
    re-frame.machines day8/re-frame2-machines (post-v1 scaffolding) reg-machine / defmachine (registration), machine-transition, the :rf.machine/spawn / :rf.machine/destroy fx (per §Machines).
    re-frame.epoch core epoch-history, current-epoch-id, restore-epoch!, replay-epoch!, replace-frame-state!, called at home (per §Epoch history); plus (rf/configure! {:epoch-history ...}) and the epoch listener stream reached via (rf/register-listener! :epoch id f) / (rf/unregister-listener! :epoch id), which re-frame.core calls directly. re-frame.core loads the namespace, so no boot require is needed; a production build elides it (per Tool-Pair §Time-travel).
    re-frame.adapter.uix day8/re-frame2-uix UIx-specific surfaces (per §UIx adapter).
    re-frame.substrate.plain-atom core the headless adapter for JVM and Node tests, scripts and headless hosts (per §Plain-atom adapter).
  • Projection-maintenance rule. This doc is a non-canonical projection — the canonical contract lives in the per-Spec docs cited in each row's Spec column. The projection MUST stay in sync with shipped artefacts. Every row carries: owner (Spec column) — the canonical spec doc; artefact / namespace — where the public-var lives (table above); public-var status — v1 / v1 (preserved) / post-v1 lib / post-v1 (planned, rf2-<id>) for surfaces specced normatively but not yet shipped (the spec contract holds; the impl is tracked by the named bead); verification pointer — the conformance fixture, the per-artefact test, or the AI-Audit row that asserts the row holds. Rows that document a surface neither shipped nor on a tracking bead MUST be cut from this projection — the design's normative claim then lives only in the owner spec.


Tier taxonomy

Every public-surface row in this document is either supported — a documented API that downstream apps and tools may rely on, in the status the row's Status column gives — or, for the one implementation tier, public-for-technical-reasons only (exported but explicitly not a surface to depend on; see below). Supported is not the same as front-porch: a system that ships restore-epoch!, project-egress, adapter hooks, and replace-frame-state! has a large public surface, but only a small slice of it is what a new app developer should ever reach for. The Tier column on every table below records that slice, using a closed vocabulary of six values:

Tier Meaning Who reaches for it
front-porch The tight set a new app developer needs to build a working app — register / dispatch / subscribe, the frame basics, the everyday reads. Loaded by default by the Guide and the skills. Every app author, day one.
advanced Power-user surfaces that solve real problems but are not first-reach — epoch-listener registration, trace projection, flows internals, the *-twin fn forms, the lower-level lifecycle (destroy-adapter!). Opt-in. Experienced authors, library authors, niche cases.
tooling Dev / inspection surfaces consumed by Story, Xray, the pair-MCP servers, and other Spec 009 / Tool-Pair tools — trace listeners, the registrar query API, off-box-egress projections, the epoch query surface, the Story run-result read accessors — and the host/tool embedding points a tool ships, such as the Xray mount-<panel>! / mount-shell! family a host that builds its own Xray chrome may call. Not for application logic. Tool and instrumentation authors, and hosts embedding a tool.
adapter The substrate-adapter hooks — the per-substrate adapter Var, the hooks and set-*! seams a substrate adapter implements or installs. Adapter authors (Reagent / UIx / custom).
testing Test-only helpers — fixture machinery, assertion helpers, view-tree walkers, the HTTP-stub surfaces. Elided or simply absent from production paths. Test authors.
implementation Public-for-technical-reasons only — exported because a sibling namespace, the tool chrome, or a test must reach it across a namespace boundary (ClojureScript has no cheap cross-namespace-private seam), but it is NOT a supported surface: an app or tool MUST NOT depend on it, and it may change or vanish without notice. The per-feature artefact lifecycle/validation/cache helpers (re-frame.schemas, re-frame.ssr, re-frame.routing, re-frame.machines, re-frame.flows), the Ring host-adapter internals, and the Xray panel-leaf Panel reg-views live here. No facade export may sit at this tier — a facade export tiered implementation is annotation rather than removal, and the api-manifest drift-check refuses it (per Conventions §Removing or demoting a facade export); that is why the Story vocabulary is tooling and match-schema-expectations is not on the facade at all. Nobody downstream — internal plumbing that merely happens to be public.

The Guide and the skills load FRONT-PORCH only by default. A new app developer reading the Guide, or an AI agent operating through the skills, sees the front-porch tier and nothing else unless they opt in. Advanced, tooling, adapter, and testing surfaces are opt-in — reached by explicitly pulling in the relevant chapter, the relevant artefact, or the relevant tool. The implementation tier is never reached for: it is not opt-in, it is off-limits — exported only for technical reasons (see the table) and excluded from the supported surface entirely. The acceptance bar for this taxonomy: a new app developer can read the one-page front-porch list (the union of all front-porch rows below) and never trip over trace projection, epoch-listener registration, adapter hooks, or Xray internals.

Tier vs Status. Tier and Status are orthogonal axes. Status records shipping lineage (v1 / v1 (preserved) / post-v1 lib / …). Tier records who-reaches-for-it. A surface can be v1 and tooling (register-listener!), or post-v1 lib and front-porch (the Story run verb is post-v1 lib/tooling; a hypothetical post-v1 ergonomic core helper would be post-v1 lib/front-porch). Read the two columns together.

The front-porch / back-room split is the first instance, generalised. The multi-frame surface (§View ergonomics, per 002 §The multi-frame surface) is organised as a front-porch / back-room split — dispatch / subscribe / with-frame on the porch, the {:frame …} override in the back room (and capture-frame, the ONE public HOLD primitive, alongside it). The Tier column generalises that split across the whole API: front-porch is the porch; advanced / tooling / adapter / testing are the back rooms (all still supported); implementation is below the floorboards — exported but off the supported surface entirely.

Closed vocabulary, restrictive-by-default. The six values above are the complete set — no seventh tier is added without an explicit governance decision. When a surface is genuinely ambiguous between two tiers, pick the more restrictive one (advanced over front-porch; implementation over tooling when the var is not actually a host/tool integration point but merely an exported helper) and note the call in the row's Notes. The downstream manifest consumes Tier as a first-class field, so the value must come from this closed set. The tooling / implementation boundary: ask "is this a surface a host or tool legitimately embeds against?" — if yes, tooling (the Xray mount family); if it is exported only so the framework's own namespaces / tests can reach it across a boundary, implementation. The split keeps the supported surface honest.

What the Tier column covers. Tier is a property of a public var (a fn / macro / Var). The tables that enumerate keyword-addressed registrations — standard events (:rf.route/navigate, :rf/hydrate, …), standard subs (:rf/route, :rf/response, …), standard fx (:rf.nav/push-url, :rf.http/managed, …), standard cofx, reserved fx-ids, the :fx-entry catalogue, the spec-internal schemas, the configure keys, and the error/trace-event catalogues — are not vars and carry no Tier column. They are part of the contract of whichever artefact owns them; their availability follows that artefact's tier (e.g. the :rf.route/* events ship with the advanced routing artefact). The front-porch one-page list is the union of front-porch-tiered var rows.

Tiering of cross-tool surfaces (Story, Xray, pair-MCP)

Story, Xray, and the MCP support namespaces ship their own public-var rows in their own specs (007-Stories.md, tools/xray/spec, Tool-Pair.md); this projection rows only the slices that surface through re-frame.core or the per-feature artefacts. The Tier taxonomy is nonetheless repo-wide and authoritative over those surfaces — the per-tool specs classify against this closed vocabulary rather than inventing local terms. The standing classifications (resolved here, so the per-tool specs reference rather than re-litigate):

  • Story facade — every re-frame.story export is tooling, with no implementation carve-out. The public execution verbs run / is / explain and the registration macros (reg-story / reg-variant / reg-workspace / reg-tag / reg-decorator / reg-story-panel / reg-fragment / reg-check / reg-mode, each with its *-fn partner) are tooling — a Storybook-shaped dev surface, not application logic. So are the run-result read accessors (run-result, result-status, result-passed?, run-result-schema, valid-run-result?, explain-run-result), the statement-of-record for reading what a run produced (per story spec 017 §Run result); the variant lifecycle (run-variant / reset-variant / watch-variant / destroy-variant! / render-variant); the registry query, canonical vocabulary, assertion, recorder, fingerprint, run-artifact, determinism and golden-slice families; and the *-id Vars for the built-in decorators. The run-variant / watch-variant / reset-variant vocabulary is not implementation ("public-for-technical-reasons only"), because 007-Stories.md — the owning spec — names run-variant / reset-variant among the API the Story-as-test duality leans on; tools/story/spec/API.md §Facade re-export discipline lists the run/reset/watch/destroy lifecycle, the assertion and recorder facades and variant-share-url as user-callable; and the Story tutorials teach (story/run-variant …) directly. Per Conventions §Story / Xray nuance, for a tooling product tooling can legitimately be the front porch — the discriminator is the user's workflow, not the tier label. (There are no is-variant / run-plan / is-plan vars.) An export with no such workflow, match-schema-expectations, lives at its owning re-frame.story.result and not on the facade, rather than being annotated internal.
  • Xray facade (day8.re-frame2-xray.core) — every one of its 16 exports is tooling, and all 16 are :action :keep. The facade is the canonical entry point most hosts ever touch: the install verb (init!), the mount/visibility verbs (open! / open-overlay! / close! / toggle! / popout! / status), the inspected-frame pair (target-frame / set-target-frame!), the Story→Xray focus door (focus! + valid-focus-panels), load-theme!, and the four highest-traffic config setters (configure! / set-auto-open! / set-editor! / set-egress-profile!). Per Conventions §Story / Xray nuance, for a tooling product tooling can legitimately be the front porch. tools/xray/spec/API.md §Wider public surface draws the porch/workshop line and names the two surfaces deliberately kept OFF the facade — the per-key config setters beyond the four above, and the keybinding/attach! / detach! lifecycle pair. Unlike the other Xray rows these are :cljs-only manifest rows carrying :facade? true per row (the namespace is not JVM-loadable), and the CLJS probe holds it fully-rowed in both directions — the only Xray namespace held to completeness. load-theme! is the sole export here that mutates, and load-* sits on no row of the §Lifecycle-verb law closed roster; it carries its bang under §Naming bucket 3 alone, with no roster row — the roster names which lifecycle verb a surface takes; §Naming decides whether it carries !; a verb missing from the roster is not a reason to drop the bang. register-listener! takes its bang by the same reading, likewise without a roster row.
  • Xray mount-<panel>! family (mount-epoch-panel!, mount-app-db-diff!, mount-trace!, mount-machine-inspector!, mount-routing!, the master mount-shell!, …) — tooling. They are exported and stable, but they are a host-embed surface, not an application surface: a host that builds its own Xray chrome may call a mount-<panel>!; an app never should.
  • Xray panel-leaf Panel reg-views (day8.re-frame2-xray.panels.<area>/Panel, the Static-mode panel) — implementation. They are exported only so the shell can compose them across namespaces; they are NOT a host-facing single-panel embed surface — a host embeds the full shell via mount-shell! (per 008-Embedding-Contract), never a bare Panel. They are not tooling (the supported embed surface is the mount family above), so the leaves do not read as a supported embed API.
  • Xray panel-helper functions (the per-panel render/projection helpers beneath the mount-fns) — tooling where they are a documented panel-author surface, otherwise unrowed-internal.
  • pair-MCP support namespaces — the trace/egress surfaces they consume (sensitive?, the registrar query API, the epoch query surface) are tooling, tiered at their re-frame.core / artefact home rows below.

Registration

Return value. Every reg-* row below returns its primary id — the keyword (or path, for reg-app-schema) the caller registered with (its first positional argument). Per Conventions §reg-* return-value convention.

API M/Fn Signature Status Tier Spec Notes
reg-event M (reg-event id ?metadata handler) EP-0018 front-porch 002 The ONE public event form — a two-arg (fn [coeffects event-vec] effect-map) handler, coeffects in, a closed three-key effects map out — #{:db :rf.db/runtime :fx} (see §Effect-map shape) — or nil no-op. The db write is an explicit {:db …} effect; there is no db-only return shape. Coeffects declared uniformly via :rf.cofx/requires. Full-context work is expressed with a registered interceptor (reg-interceptor, referenced by id). Metadata-map superset middle slot carries the reserved :interceptors key (a vector of interceptor refs). There is no reg-event-db/reg-event-fx, and reg-event-ctx is a framework-internal primitive — calling any of those names is a hard error (see §The retired event-registration names + the Removed §).
reg-sub M (reg-sub id ?metadata computation-fn) v1 front-porch 002 v1 name; grammar changed to :inputs. The only sub-registration form in v2. Dependencies are DECLARED under :inputs in the metadata map — a literal vector of query vectors (:static) or a producer fn of the query vector (:parametric) — and declared inputs always reach the body as a VECTOR. See §reg-sub input-production modes. Omitting :inputs is the layer-1 app-db reader.
reg-fx M (reg-fx id ?metadata handler) v1 (preserved + extended) front-porch 002 The handler is binary, context-first: (fn [ctx args]) per 002 §The binary fx-handler signature. ctx is a small map carrying :frame (the active frame id), :event (the originating event vector), and a runtime-internal :envelope; args is the value the event handler placed beside the fx-id in its :fx vector. A unary (fn [args]) handler survives on CLJS only via JS argument-dropping — it is not the blessed contract. :platforms metadata (a set of :server / :client) gates execution by active platform (default universal).
reg-cofx M (reg-cofx id ?metadata supplier) v1 (changed, EP-0017) front-porch 001 Register a coeffect id with a value-returning supplier ((fn [] v) / (fn [arg] v)) and a registration grade — recordable (the default, optionally :provided? true) or ambient (:recordable? false). A handler takes delivery by declaring :rf.cofx/requires (the value arrives FLAT under the id; 001 §:rf.cofx/requires). :rf/time-ms is the framework's one provided recordable registration. There is no ctx→ctx handler shape and no public inject-cofx. See §Coeffects. Also 002 §Recordable coeffects: the flat :rf.cofx envelope field and its delivery.
reg-interceptor M (reg-interceptor id ?metadata descriptor) EP-0022 front-porch 001 The public application-authoring form for an interceptor — a first-class registered program member (registrar kind :interceptor). descriptor is one of {:before f} / {:after f} / {:before f :after f} (static) or {:factory f} (a parameterized family; the factory takes ONE arg and is the mechanism the standard [:rf.interceptor/path …] rides). Event/frame :interceptors chains reference registered interceptors by id (bare keyword) or [id arg], never inline values. Captures source coords; surfaces via handler-meta :interceptor. A migration value carrying an :id is accepted at this boundary only (the id must match). There is no public ->interceptor authoring form. See 001 §Interceptors + 002 §Registered interceptors and the chain grammar.
make-frame Fn (make-frame opts) / (make-frame opts descriptors) → frame value EP-0024 advanced 002 The ONE frame constructor: builds a live frame and returns the frame value (the lifecycle token — the routing ops dispatch / subscribe / frame-provider accept the value directly OR its id, normalizing a value to its id; destroy-frame! accepts either too but is the one lifecycle exception — the value is an exact-incarnation token that tears down only the incarnation it names (a stale value no-ops against a same-id successor), while the id is address-directed, per §Destroy. There is no frame-value->id accessor and no need to unwrap the value). opts is a map (required — a non-map opts, including nil, fails loud with :rf.error/make-frame-bad-opts; the all-defaults frame is (make-frame {})). Accepts BOTH image-selection opts AND record-config opts in one call: :images (a non-empty vector; present ⇒ the selected generation; [] is an error — :rf.error/make-frame-bad-images; absent ⇒ no generation: the frame resolves the whole source store live + framework standards, and a [kind id] registered from more than one namespace raises :rf.error/ambiguous-registration where it is used while every other id keeps working; app isolation is named via :select-ns, not absence) / :id (registers in the one frame registry; a duplicate id is idempotent replacement — config + generation refresh, durable state preserved) / :adapter, plus any record-config key (:initial-events — seed app-db via a leading [:rf/set-db {…}] step — :fx-overrides, :http-interceptors (the frame's ordered HTTP interceptor chain, per 014 §Activation), :flows (the flow ids the frame runs — none by default — per 013 §Activation), :platform, :ssr, :doc, :preset, :tags, …) honoured in the same call. (There is no :capabilities image-selection key and there are no image-declared host capabilities; :capabilities flows through as ordinary record-config.) Re-calling make-frame against the SAME :id with a NEW :images vector IS image hot-reload — the generation swaps while durable state is preserved (there is no dedicated reload-images! verb; read the diff via generation-diff). make-frame is the ONE programmatic constructor (tools / tests / SSR / dynamic / image-loaded frames — there is no reg-frame spelling); the day-1 mount recipe is frame-root (ENSURE). (re-frame.frame/make-anon-frame-record! is the internal no-:id record helper.)
image M (image spec) → normalized, INERT image value — spec carries :id (optional), :select-ns ({:include [globs] :exclude [globs]} provenance selection), :registrations (inline registrar-keyed sections). Pure data — no registrar, no side effect; supplied to make-frame via :images (later image wins; shadows reported on :rf.gen/shadows) EP-0023 advanced 002 The one public image constructor. A macro for documentation elision only — literal inline :registrations :doc bytes DCE in production (001 §Production elision contract) — that delegates to the runtime value constructor and stamps no source coordinates. The cross-host contract is the value (Conventions §The public rf/image source keys).
generation-diff Fn (generation-diff before after) → {:added #{[kind id] …} :changed #{…} :removed #{…} :retained #{…}} — a PURE diff between two sealed image generations (read via frame-generation before/after a re-make-frame reload). EP-0023 tooling 002 The reload report — a read over two generation values, not a bespoke verb.
reg-view M (reg-view sym [args] body+) / (reg-view sym docstring [args] body+) / (reg-view ^{:rf/id :explicit/id} sym [args] body+) v1 front-porch 001 Defn-shape; auto-defs the symbol; auto-derives id from (keyword *ns* sym); auto-injects dispatch / subscribe as lexical bindings; rejects non-defn-shape bodies at macroexpand. Also 002 §What reg-view injects.
reg-view* Fn (reg-view* id render-fn) / (reg-view* id metadata render-fn) v1 advanced 001 Plain-fn surface beneath reg-view. No auto-def, no auto-inject, no compile check. Use for computed ids, library-generated views, Reagent Form-3 (create-class), or registration without a Var. The * follows Clojure's let/let*, fn/fn* idiom (per Conventions). Also 002 §What reg-view injects, which reg-view* does not do.
reg-state M (reg-state concern) / (reg-state concern opts) → concern v1 advanced 002 Per-instance view state, the same under every view substrate. Registers a sub and a setter event under the namespace-qualified concern, reading and writing app-db at [:ui concern instance-key], with opts' :default (its only key) for an absent entry, plus the shared clear event [:rf.state/clear concern instance-key], which removes the entry. A bad concern, option or instance key raises :rf.error/state-bad-argument. Per 002 §Per-instance view state.
reg-machine M (reg-machine machine-id machine-spec) / (reg-machine machine-id opts machine-spec) v1 advanced 005 Optional re-frame.machines artefact. Walks the literal spec form at expansion time; co-locates per-element source on each :guards / :actions entry + a reference-site :source-coords on each :states-tree map node. Top-level call-site coords land on handler-meta. The optional opts metadata map is the canonical Spec 001 MIDDLE slot; it carries an event-vector :schema (the :where :event boundary on the dispatched outer vector) — the machine + event-vector-schema shape.
defmachine M (defmachine name spec) / (defmachine name docstring spec) v1 advanced 005 def-shape for the def-then-register pattern. Walks the literal spec at the definition site, stamping per-element source onto the def'd value so it travels into a later (reg-machine id name). Does not register.
reg-app-schema M (reg-app-schema path schema) / (reg-app-schema path metadata schema) — the schema is the positional value slot; the optional middle metadata map carries :doc and open :my/* keys (e.g. (reg-app-schema [:user] {:doc "The signed-in user."} UserSchema)) v1 advanced 010 Optional re-frame.schemas artefact. Path is the registration id. App-db schemas are the registrar kind :app-schema, keyed by path: a frame-agnostic program registration that every frame resolving it validates against. A frame that should validate differently selects a later image that shadows the path, and a :frame on the registration — a metadata key, or a frame value in that slot — is a loud :rf.error/app-schema-frame-target (per 010 §Schemas resolve through the frame's images). Every other reg-* is keyword-id-keyed; here the first arg is the path vector (e.g. [:user]) and (app-schemas {:frame f}) / (app-schema-meta {:frame f :path [:user]}) look up by the same vector. The path-as-id asymmetry is principled (paths are first-class in get-in / assoc-in grain — schemas-at-paths matches the dataflow grain), not accidental; otherwise reg-app-schema is an ordinary family member — the schema is the positional value slot uniform with the rest of the reg-* family. A non-map middle metadata arg (the 3-slot form) is a loud :rf.error/app-schema-bad-metadata. Per Conventions §reg-* return-value rule.
reg-app-schemas M (reg-app-schemas {path-1 schema-1, path-2 schema-2, ...}) / (reg-app-schemas {…} opts) — bulk plural form for feature-modular apps that register 5–20 paths against the same prefix (per Conventions §Feature-modularity prefix convention). The bulk {path -> schema} shape needs no positional slot (the map value IS the schema — no positional ambiguity in a bulk map); the plural form exists for its all-or-nothing atomic-batch contract (every path validated before any mutation — a naive doseq does not preserve it). Each entry routes through the singular reg-app-schema positionally and is stamped with this call's source-coords. A second argument naming a frame — a frame-id, a frame value, or an opts map carrying :frame — is :rf.error/app-schema-frame-target. Returns the vector of paths registered v1 advanced 010
reg-flow M (reg-flow flow-id metadata derive-fn) v1 advanced 013 Optional flows artefact. Per the canonical Spec 001 3-slot grammar: the pure :derive fn is the third VALUE slot; metadata carries :inputs / :output-path (both REQUIRED) plus optional :doc / the EP-0025 classification keys. A :derive, a :frame or a :schema left inside the metadata map is rejected loudly (:rf.error/invalid-flow-metadata); the output's shape is declared with reg-app-schema at :output-path. Returns flow-id (per Conventions §reg-* return-value convention). A top-level registration under the registrar's :flow kind: it reads no frame, and a frame runs the flows its :flows config names (make-frame / frame-root). Introspection is the ordinary registrar query — registrations / handler-meta with :kind :flow, and (:flows (frame-meta f)) for the ids a frame runs. Per 013-Flows.md §The registration shape / §Activation.
reg-classification M (reg-classification id decl) — decl is a closed map {:sensitive [paths] :large [paths] :doc "…"} EP-0039 advanced 015 Static app-db classification, the registrar kind :classification: the app-db-absolute paths a feature makes sensitive or large, declared once beside its other registrations. Each frame's egress classification unions the declarations its own view resolves, so a declared path is redacted from its first egress with no classification event, and a retained epoch record keeps the declarations it was recorded under. A malformed declaration is :rf.error/bad-classification at registration; (clear :classification id) removes one. Per 015 §Static app-db classification.
reg-route M (reg-route id metadata path) — canonical 3-slot grammar: the URL :path pattern is the third VALUE slot, metadata the pure reflection map v1 advanced 012 Optional routing artefact. A :path left inside the metadata map is a loud :rf.error/route-bad-metadata.
reg-head M (reg-head id ?metadata head-fn) v1 advanced 011 Optional SSR artefact. New registry kind :head; routes name a registered head via :head route metadata. Captures source-coords; under the optional-artefact wrapper convention the surface routes through the :ssr/reg-head late-bind hook.
reg-error-projector M (reg-error-projector id ?metadata projector-fn) v1 advanced 011 Optional SSR artefact. New registry kind :error-projector; named per-frame via the frame's :ssr {:public-error-id ...} config (per make-frame / frame-root).

reg-sub input-production modes

reg-sub supports three input-production modes. Every subscription has an input query-vector producer, and it is declared ONCE — under :inputs in the metadata map, the same slot reg-flow uses. Layer-1 has no producer; a literal :inputs vector is the static producer; an :inputs fn is the query-parametric producer.

Mode Form Meaning
App-db reader (reg-sub id computation-fn) :inputs OMITTED. No upstream subscriptions. The computation fn receives app-db and the outer query-v.
Static inputs (reg-sub id {:inputs [q1 q2]} computation-fn) Inputs are literal query vectors known — and shape-checked — at registration.
Parametric inputs (reg-sub id {:inputs producer-fn} computation-fn) Inputs are computed from the outer query-v when a concrete cache entry is materialized.

Declared inputs always arrive as a vector. At zero, one or many, the computation fn receives [v0 v1 …] in declaration order. Moving a dependency between the literal and the producer form never changes the body, and adding a second input never turns a scalar argument into a vector. An explicit {:inputs []} declares no dependencies and delivers []; OMITTING :inputs is the layer-1 reader, which receives app-db itself. The two are distinct by design.

(rf/reg-sub :cart/by-price {:inputs [[:cart/items]]}
  (fn [[items] _] (sort-by :price items)))

(rf/reg-sub :cart/visible {:inputs [[:cart/by-price] [:cart/filter]]}
  (fn [[items f] _] (filter f items)))

An :inputs producer fn is a pure function from the outer query-v to a vector of input query vectors. It is not a v1 signal function: it must not call subscribe, deref app-db, dispatch, mutate, or perform IO; it receives only the outer query-v; and it must not return live reactions. It is never executed at registration — only at materialization.

(rf/reg-sub
  :article/page
  {:inputs (fn [[_ article-id]]
             [[:article/by-id article-id]
              [:comments/for-article article-id]
              [:viewer/current]])}
  (fn computation-fn [[article comments viewer] [_ article-id]]
    {:id article-id :article article :comments comments
     :can-edit? (:edit? viewer)}))

Input grammar. A literal :inputs MUST be a vector, and every element MUST be a query vector (a vector whose first element is a keyword); a producer fn MUST return that same shape:

inputs := [query-vector*]      ;; query-vector := vector with a keyword head
;; Accepted
[[:article/by-id id] [:viewer/current]]   ;; multiple inputs
[[:item/by-id id]]                        ;; single input — still a vector OF query vectors
[]                                        ;; no inputs (unusual but valid)

;; Rejected
:viewer/current                           ;; bare keyword
[:article/by-id id]                       ;; scalar query vector (ambiguous: arg vs two inputs)
[[:article/by-id id] :viewer]             ;; mixed vector + bare keyword
{:article [:article/by-id id]}            ;; map return

The scalar query-vector rejection is deliberate: [:x :y] is ambiguous at this boundary (one query with argument :y, vs two inputs). The only accepted single-query spelling is [[:x :y]]. No bare keyword shorthand, no map return, no reaction/derefable. A literal :inputs is checked at registration (:rf.error/reg-sub-bad-args) against the same grammar a producer's return is checked against at materialization (:rf.error/sub-input-fn-bad-return) — one grammar, two moments. An explicit {:inputs nil} is refused: nil is not "absent". The literal check is SHAPE-only and never a registry lookup, so {:inputs [[:a]]} may be registered before :a exists. Reach for a producer fn only when the upstream query vectors need values from the outer query-v; a literal vector is exactly a constant producer, and it is the form a tool can read as a static edge. Per 006 §Subscription input producers, 008 §compute-sub algorithm, and Conventions §reg-sub input grammar. Registration-shape and input-return errors signal loudly via :rf.error/reg-sub-bad-args, :rf.error/sub-input-fn-exception, and :rf.error/sub-input-fn-bad-return (catalogued in 009 §Error event catalogue).

:inputs is a vector here, and whether the family should instead take a named map is an open question shared with reg-flow — see 013 §Map-keyed :inputs instead of vector. It will be ruled ONCE, for reg-flow and reg-sub together; until then both ship the vector.

Clearing registrations

One verb, kind-keyed. The registrar is ONE map, (kind, id) → metadata (001 §Registry model), and clear is its inverse in the same grammar the read side already speaks. There are no per-kind names (clear-event, clear-sub, clear-fx, clear-flow, clear-route, clear-http-interceptor, clear-resource, clear-mutation, clear-resource-scope): per-kind names would differ on where the name lives, whether a nilary clear-all exists, how the frame is named, and what comes back.

(rf/clear :sub  :cart/total)                    ;; => :cart/total
(rf/clear :cofx :now)                           ;; every registrar kind has an inverse
(rf/clear :http-interceptor :shop/auth)         ;; for every frame that names it

Each kind routes to its OWNING lifecycle fn — :flow emits :rf.flow/cleared, and each frame running the flow vacates its output on its next event, :route emits :rf.route/cleared, :http-interceptor emits :rf.http.interceptor/cleared, the resources kinds dispose per-frame runtime state — rather than short-cutting to the registrar. Kinds with no owning lifecycle fn (:event :sub :fx :cofx :interceptor :view :head :error-projector :app-schema :classification) go straight to re-frame.registrar/unregister!, which IS their owner. Returns the id for every kind. The per-kind clear column is in 001 §Registry model.

No frame. Every registration is a program member, read through each frame's own view, so clear removes it for every frame at once; a trailing opts map THROWS :rf.error/registrar-clear-bad-request. A frame stops running a flow or an HTTP interceptor when it is re-made without naming it. An unknown kind fails closed the same way, naming the closed set.

There is NO clear-all arity. Bulk clearing is a fixture concern: re-frame.registrar/clear-kind! and re-frame.test-support own it, and they are the only callers a nilary arity would have. The cache / buffer clear-*! names (clear-sub-cache!, clear-trace-buffer! …) are a DIFFERENT axis — they clear runtime state rather than registrations — and keep both their names and their bangs.

API M/Fn Signature Status Tier Spec
clear Fn (clear kind id) → id. The ONE registrar inverse. kind is the registrar kind set MINUS :frame (a live runtime object, torn down by destroy-frame!). No frame argument. See the note above. v1 (preserved) advanced 001
destroy-frame! Fn (destroy-frame! frame-id) — the normative teardown boundary. A frame value target carries exact-incarnation authority (tears down only the incarnation it names — a stale value no-ops against a same-id successor), while a frame-id keyword is address-directed (tears down whatever incarnation is currently live), per 002 §Destroy. Per-feature artefacts (flows, machines, schemas, SSR) and epoch hang their frame-scoped cleanup off this call; flows release per 013 §Frame-destroy teardown. v1 front-porch 002
clear-sub-cache! Fn (clear-sub-cache! frame-id?) v1 (preserved) advanced 002

Dispatch and subscribe

API M/Fn Signature Status Tier Spec Notes
dispatch M (also Fn on CLJS) (dispatch event) / (dispatch event opts) v1 (preserved + extended) front-porch 002 macro captures call-site for :rf.trace/call-site; on CLJS the same name is ALSO a plain-fn value-alias (Convention A) for HoF / programmatic dispatch with no call-site capture
dispatch-sync M (also Fn on CLJS) (dispatch-sync event) / (dispatch-sync event opts) v1 (preserved + extended) front-porch 002 macro captures call-site for :rf.trace/call-site; on CLJS the same name is ALSO a plain-fn value-alias (Convention A) for HoF / programmatic sync dispatch with no call-site capture
subscribe Fn (subscribe query-v) / (subscribe query-v opts) v1 (preserved + extended) front-porch 002 opts may carry :frame (mirrors dispatch); a plain fn on both hosts, so it is also a value ((map subscribe query-vs)); no call-site capture — inside a reg-view body the injected subscribe attributes a read's errors to the view's definition site
subscribe-once Fn (subscribe-once query-v) / (subscribe-once query-v opts) → value (subscribe + deref + immediate unsubscribe; one-shot, non-reactive read for the REPL, tests, SSR builders and tools; an event handler takes it through a declared cofx or computes it with compute-sub against its coeffect — never inside a machine callback: an in-callback ambient read is unrecorded and breaks 005's replay contract; a machine reads facts via payload or a declared recordable cofx) v1 (preserved + extended) advanced 006 opts may carry :frame (mirrors subscribe, so an author carrying the opts form over from subscribe is not misbound)
unsubscribe Fn (unsubscribe r) → nil (r is the reaction subscribe returned; return one counted share of it, identity-guarded — a reaction whose slot was evicted or whose frame was destroyed is a no-op that never touches a successor, and so is nil; a query vector throws :rf.error/bad-unsubscribe-arg; on the 1 → 0 transition the cache slot is disposed synchronously in-tick — no grace-period timer, per 006 §unsubscribe and 006 §Reference counting and disposal). Carved out from the Conventions §Tear-down verb axis — the registrar decrement is (clear :sub id), so un- is reserved as the singular form for the sub-cache ref-count decrement. The two are different operations on different state. v1 advanced 006
compute-sub Fn (compute-sub query-v db) → value — computes the sub's value against a supplied db value, bypassing the reactive cache; db is an app-db, a runtime-db or a whole frame-state value. It is how an event handler reads framework state: the vector a view subscribes to, computed against the handler's :rf.db/runtime coeffect — (compute-sub [:rf/machine id] (:rf.db/runtime cofx)), (:nav-token (compute-sub [:rf/route] rt)) — never a spelled storage path. It is also the pure, JVM-runnable form a test asserts a sub with (per 008) v1 front-porch 008

To read a machine's snapshot, subscribe to the canonical [:rf/machine machine-id] vector (see §Standard registered subs (machines)); an event handler computes the same vector against its :rf.db/runtime coeffect with compute-sub.

opts map keys: :frame, :fx-overrides, :interceptor-overrides, :trace-id, :source. Envelope shape and semantics: see 002 §Routing: the dispatch envelope.

Canonical event / query-v shape (best practice). [<id>] (trivial), [<id> <single-scalar>] (single-arg), [<id> {<k> <v>}] (multi-arg → single map payload). Variadic [<id> a b c] is tolerated by the runtime for v1-migration and caller convenience; the linter nudges new code toward the map form. Full rationale and cross-refs: Conventions §Canonical event-vector shape.

Standard frame events (registered by core in the regular registrar and the image standard registry, so every frame resolves them):

Event What it does Spec
:rf/set-db Replace the whole app-db partition with one map argument — the app-db seed step of :initial-events. A missing / nil / non-map argument, or an extra one, throws :rf.error/set-db-bad-value. 002 / EP-0027
:rf/install-frame-state Install a persisted frame-state {:rf.db/app <map>? :rf.db/runtime <map>?}, read earlier with frame-state-value: a present :rf.db/app replaces app-db, each runtime-db subtree a present :rf.db/runtime carries replaces that subtree while the rest are preserved, and restored machines' :after timers are re-armed after the commit. A malformed payload or a resource-runtime subtree throws and surfaces as :rf.error/handler-exception. Per 002 §Installing a persisted frame-state. 002

dispatch-* family taxonomy

The dispatch-* family has two sub-shapes that look alike on first read but answer different questions. Both are dispatch operations — the family-prefix is honest — but they sit in different sub-families.

Stamping-pair sub-family (dispatch / dispatch-sync). The pair-shape question is "do you want call-site stamping or not?" In call position the macro form captures :rf.trace/call-site from the surrounding source position so tooling can navigate from a trace event back to the originating expression. On CLJS the same name is ALSO a plain-fn value-alias (Convention A — mirrors reg-event / reg-sub / etc.) for HoF composition ((map dispatch events)) where a macro can't sit inside the higher-order call — no call-site capture on that path. There is no *-suffixed twin (dispatch* / dispatch-sync*); a JVM programmatic caller reaches the owning ns fn directly (re-frame.router/dispatch! / -dispatch-sync!). Both the macro path and the value-alias route through the same underlying dispatcher; only the trace stamping differs. subscribe is not a stamping pair: it is one plain fn on both hosts, so it composes as a value everywhere and a read carries no call-site of its own — inside a reg-view body the injected subscribe attributes a read's errors to the view's definition site.


View ergonomics

The multi-frame surface is organised by intent, not mechanism (a front-porch / back-room split — per 002 §The multi-frame surface):

  • Single-frame (no frames in play): dispatch, dispatch-sync, subscribe.
  • Scope: with-frame, with-new-frame, frame-provider {:frame …} (SCOPE-only — scope an existing frame into a React subtree; fails loud if absent). (ENSURE is its sibling component frame-root {:id …} — see below.)
  • Hold (carry a frame's ops as a value, across async): capture-frame — the ONE public carry primitive (there is no frame-bound-fn / frame-bound-fn* — capture-frame or an explicit {:frame …} opt expresses the real use cases).
  • Override: the {:frame …} opt — first-class explicit routing for tools / tests / SSR / fx handlers.
  • Reads / lifecycle: app-db-value, frame-state-value, current-frame-id, destroy-frame!, make-frame, frame-ids, frame-meta (see §Public registrar query API).
API M/Fn Signature Status Tier Spec
frame-provider Fn (Reagent component) SCOPE-only (roots ensure; providers scope). [rf/frame-provider {:frame :todo} & children]: provides an ALREADY-CREATED frame id through React context; :frame accepts a frame-id keyword OR a live frame value; creates / refreshes / destroys nothing; FAILS LOUD when the frame is absent (:rf.error/frame-provider-frame-absent; a value/keyword that is neither raises :rf.error/bad-frame-provider-arg; a nil :frame → :rf.error/no-frame-context). Given an :id (the ENSURE key), FAILS LOUD naming frame-root (:rf.error/frame-provider-given-id). For create-if-absent, use frame-root. v1 front-porch 002
frame-root Fn (Reagent component) ENSURE — a COMMIT-OWNED TWO-PASS boundary. [rf/frame-root {:id :todo :images […] :initial-events [[:rf/set-db {}]]} & children]: creates the frame if absent (via make-frame) in a client useLayoutEffect (NOT during render — first render emits no descendant subtree; a Suspense-aborted render creates + seeds nothing, no ghost frame), REUSES it WITHOUT re-seeding if present (idempotent re-mount / keyed remount preserves durable state and does NOT replay :initial-events; StrictMode-safe), provides its id to descendants; NO destroy-on-unmount. :id required (a missing/non-keyword :id → :rf.error/frame-root-missing-id); a mounted :id/opts change → :rf.error/frame-root-reconfigured; a stray :frame → :rf.error/frame-root-given-frame. True ownership stays make-frame + destroy-frame! in a create-class. v1 front-porch 002
with-frame M (with-frame :keyword body) — pin to an existing frame-id. Vector arg is a compile-time error (use with-new-frame) v1 front-porch 002
with-new-frame M (with-new-frame [sym expr] body) — eval expr, bind sym, run body, destroy frame on exit. Keyword arg is a compile-time error (use with-frame) v1 front-porch 002
capture-frame Fn (capture-frame) or (capture-frame frame-id) → {:frame :dispatch :dispatch-sync :subscribe} — the keystone OPERATION BUNDLE. Captures the frame at CREATION; its ops always target the captured frame and survive async. Read app-db via (app-db-value (:frame h)), not the handle v1 front-porch 002
view Fn (view view-id) → the installed substrate's own mountable head for that registration, or nil when nothing is registered. Not hiccup, and (on CLJS) not the raw render-fn: the head is re-derived against the adapter installed now and memoized, so repeat lookups are reference-stable and React reconciles them as one component type. Reagent: the :contextType-carrying head, mounted as [(rf/view :id) args…]; UIx: a substrate-marked component type, mounted as ($ (rf/view ::row) props); Fresco: the minted boundary itself, mounted the way h/defview documents (inside a body as [head props], from outside via h/as-element) — h/defview's registration is debug-gated, so nil in a release build is the documented answer for a Fresco view. On the JVM the answer is the stored :handler-fn. The lookup form for late-binding a registered view by id. v1 advanced 001

with-frame (pin) and with-new-frame (eval-bind-run-destroy) are documented in 002 §with-frame and with-new-frame. The macros are non-overlapping: each rejects the other's argument shape at compile time, with :recovery pointing the caller at the right sibling.

capture-frame is the keystone affordance — there are no dispatcher / subscriber nouns; it is the single answer to "carry a frame's dispatch/subscribe ops across an async boundary." The handle is locked: a per-call :frame opt MUST NOT override the frame captured at handle creation — the captured frame always wins (per 002 §capture-frame). There is no public frame-bound-fn / frame-bound-fn* — capture-frame (or an explicit {:frame …} opt) expresses the real use cases and is the ONE public carry primitive; the frame-rebinding closure semantics live internally as re-frame.frame/bind-fn for the framework's own reach.


Reagent adapter (Spec 006)

Reagent-specific surfaces live in re-frame.adapter.reagent (artefact day8/re-frame2-reagent, the browser default). Reagent is the default substrate; its frame-provider Component is rowed in §View ergonomics and the reg-view macro in §Registration, not here. Apps targeting Reagent :require [re-frame.adapter.reagent :as rf.adapter.reagent] and pass rf.adapter.reagent/adapter to (rf/init! …).

API M/Fn Signature Status Tier Spec Notes
rf.adapter.reagent/adapter Var (map) the adapter spec map {:kind :make-state-container :read-container :replace-container! :subscribe-container :make-derived-value :render :render-to-string :register-context-provider :flush-render! :dispose-adapter!} plus the ratom family's capability keys (:current-frame, :ratom, :make-reaction, :add-on-dispose!, :dispose!, :after-render, …), which init! publishes (ratom-backed; per Spec 006 §Capability keys) v1 adapter 006
rf.adapter.reagent/set-hiccup-emitter! Fn (set-hiccup-emitter! f) — install the render-tree → HTML fn for render-to-string (the SSR late-bind seam, published through the :reagent/set-hiccup-emitter! hook) v1 adapter 006 Also 011 §The render-tree → HTML emitter.
rf.adapter.reagent/client-root Fn (client-root) → an inert handle — no DOM work at allocation, so it is defonce-safe at namespace load and Node-safe. Opaque: hold it, hand it to render! and unmount!, and nothing else (per Spec 006 §The client root) v1 adapter 006
rf.adapter.reagent/render! Fn (render! handle render-tree mount-point) / (render! handle render-tree mount-point opts) → nil — the first call creates the React Root at mount-point and renders into it, or with {:hydrate? true} hydrates the server markup once; every later call updates that SAME Root, so one call is both the boot path and the ^:dev/after-load hook. render-tree is hiccup v1 adapter 006 Also 011 §Client-side hydration boot helper, for {:hydrate? true}.
rf.adapter.reagent/unmount! Fn (unmount! handle) → nil — release the Root and return the handle to inert. Idempotent, and a no-op after rf/destroy-adapter! has already drained it; a later render! mounts afresh v1 adapter 006

UIx adapter (Spec 006)

UIx-specific surfaces live in re-frame.adapter.uix (artefact day8/re-frame2-uix) — they are NOT re-exported from re-frame.core because core has no static dependency on the adapter (the dependency direction is adapter → core per Conventions §Adapter shipping convention). Apps targeting UIx :require [re-frame.adapter.uix :as rf.adapter.uix] and call the surfaces directly.

API M/Fn Signature Status Tier Spec Notes
rf.adapter.uix/adapter Var (map) the adapter spec map {:kind :make-state-container :read-container :replace-container! :subscribe-container :make-derived-value :render :render-to-string :register-context-provider :flush-render! :dispose-adapter!} (6 required + 3 optional + :dispose-adapter! + the :kind discriminator) plus the React-hook spine's capability keys (:current-frame, :add-on-dispose!, :dispose!, :wrap-view, :componentize-view, :after-render, …), which init! publishes (per Spec 006 §Capability keys) v1 adapter 006
rf.adapter.uix/use-sub Fn (UIx hook) (use-sub query-v) → the current sub value, re-rendering the caller when it changes; resolves the frame from React context ONLY — the surrounding frame-provider / frame-root, never a with-frame dynamic scope (no boundary above → :rf.error/no-frame-context). (use-sub query-v {:frame target}) pins ONE read to an explicit frame — the same opts form subscribe publishes, target a frame-id keyword or a live frame value, :frame required in that arity. The ONE value-hook name across React function components: re-frame.fresco.native/use-sub is the same operation for an island under Fresco v1 adapter 006
rf.adapter.uix/use-frame Fn (UIx hook) (use-frame) → the frame api for the ambient provider frame — EXACTLY what (rf/capture-frame) returns ({:frame :dispatch :dispatch-sync :subscribe}), capture-frame in hook position; resolves from React context ONLY, exactly as the ambient use-sub does — the surrounding frame-provider / frame-root, never a with-frame dynamic scope (no boundary above → :rf.error/no-frame-context); reference-stable across re-renders for the same resolved frame incarnation — a same-id destroy-and-recreate retargets it, because the bundle is pinned to the incarnation capture-frame ran against and not to the address. No opts, no variants — explicit frames use (rf/capture-frame frame-id). v1 adapter 006 Also 002 §capture-frame: the bundle it returns.
rf.adapter.uix/frame-provider Fn (UIx component) SCOPE-only (roots ensure; providers scope) — ($ rf.adapter.uix/frame-provider {:frame :session} child-1 child-2) provides an existing frame id; creates nothing; fails loud if absent (:rf.error/frame-provider-frame-absent); given an :id (the ENSURE key) fails loud naming frame-root (:rf.error/frame-provider-given-id); idiomatic $ trailing children v1 adapter 006 Also 002 §frame-provider.
rf.adapter.uix/frame-root Fn (UIx component) ENSURE — a commit-owned two-pass boundary — ($ rf.adapter.uix/frame-root {:id :session :images […]} child-1 child-2) creates the frame if absent in a client useLayoutEffect (not during render — a discarded render creates nothing), reuses it without re-seeding if present, provides its id to descendants; no destroy-on-unmount; takes make-frame opts; a missing/non-keyword :id → :rf.error/frame-root-missing-id, a stray :frame → :rf.error/frame-root-given-frame; idiomatic $ trailing children v1 adapter 006 Also 002 §frame-root.
rf.adapter.uix/set-hiccup-emitter! Fn (set-hiccup-emitter! f) — install render-tree → HTML fn (parity with the Reagent adapter's late-bind seam) v1 adapter 006 Also 011 §The render-tree → HTML emitter.
rf.adapter.uix/client-root Fn (client-root) → an inert handle — no DOM work at allocation, so it is defonce-safe at namespace load and Node-safe. Opaque: hold it, hand it to render! and unmount!, and nothing else (per Spec 006 §The client root) v1 adapter 006
rf.adapter.uix/render! Fn (render! handle element mount-point) / (render! handle element mount-point opts) → nil — the first call creates the React Root at mount-point and renders into it, or with {:hydrate? true} hydrates the server markup once; every later call updates that SAME Root. element is a React element built with uix.core/$ — CLJS data raises :rf.error/hiccup-on-element-render-slot, on the first render and every later one alike. No com.pitch/uix.dom dependency: the Root is minted by the shared React spine v1 adapter 006 Also 011 §Client-side hydration boot helper, for {:hydrate? true}.
rf.adapter.uix/unmount! Fn (unmount! handle) → nil — release the Root and return the handle to inert. Idempotent, and a no-op after rf/destroy-adapter! has already drained it; a later render! mounts afresh v1 adapter 006

Per Decision 1 the hook is named use-sub — ONE value-hook name for every React function component, the same spelling re-frame.fresco.native publishes for a Fresco island: the VERB subscribe returns a subscription, the NOUN use-sub returns its value. The adapter publishes no raw useContext frame reader and no source-coord wrapper Var; use-frame answers "which frame am I in" and reg-view* is how a view reaches the :adapter/wrap-view late-bind hook. Per Decision 3 there is no auto-injection — UIx components read via the hook and hold frame ops via use-frame (capture-frame in hook position: (let [{:keys [dispatch]} (use-frame)] …)); capture-frame is THE hold primitive, and reg-view injection / use-frame are its two ergonomic spellings. Per Decision 4 reg-view (the Reagent macro) does NOT cover UIx; UIx users register with rf/reg-view* if they need registry-keyed view addressing.

The shared React Context that backs frame-provider / frame-root lives in re-frame.adapter.context (CLJS-only file in core, factored out per Decision 2) — the Reagent and UIx adapters both consume the same createContext object so a mixed-substrate app's frame-provider chain composes across substrates.


Plain-atom adapter (Spec 006)

The headless adapter lives in re-frame.substrate.plain-atom (core), a .cljc namespace that loads on both hosts. JVM and Node tests, scripts and headless hosts :require [re-frame.substrate.plain-atom :as rf.substrate.plain-atom] and pass rf.substrate.plain-atom/adapter to (rf/init! …), or to make-reset-runtime-fixture as its :adapter. The namespace's other public vars are adapter plumbing and are not rowed.

API M/Fn Signature Status Tier Spec Notes
re-frame.substrate.plain-atom/adapter Var (map) the adapter spec map {:kind :make-state-container :read-container :replace-container! :subscribe-container :make-derived-value :render :render-to-string :register-context-provider :dispose-adapter!}, plus :add-on-dispose! / :dispose! on CLJS. app-db is a clojure.core/atom; derived values recompute on every read and cache nothing; there is no :flush-render!, and :render throws :rf.error/render-on-headless-adapter. (:kind (rf/current-adapter)) reads :rf.adapter/plain-atom (per Spec 006 §Plain-atom adapter) v1 adapter 006

Routing (Spec 012)

reg-route is rowed canonically in §Registration.

API M/Fn Signature Status Tier Spec
match-url Fn (match-url url) → {:route-id :params :query :fragment :validation-failed? ?:validation-error} or nil (:validation-error present only on a validation failure) v1 advanced 012
route-url Fn (route-url {:to route-id :params path-params :query query-params :fragment fragment}) → URL string (single address-map arity; strictly address-only — :url / policy / unknown keys reject loud) v1 advanced 012
re-frame.core/route-link Fn (registered view at :route/link) [rf/route-link {:to :route-id :params {...} :query {...} :fragment "..." :prefetch :intent & html-attrs} & children] v1 advanced 012
history-url-strategy Var (map) The DEFAULT :url-strategy — HTML5 History, path-form. re-frame.routing/history-url-strategy, NOT on the re-frame.core façade (routing bundle isolation) v1 advanced 012
hash-url-strategy Var (map) #-prefixed :url-strategy for no-server-rewrite static hosting / secretary-era v1 migrations. re-frame.routing/hash-url-strategy v1 advanced 012
with-base-path Fn (with-base-path strategy base) — STRATEGY COMBINATOR: wraps strategy (either shipped strategy, or a custom one) so a deployment sub-path (e.g. an app served from /realworld/) is stripped off every inbound URL and re-added to every outbound one, at all four egress/ingress consult points. A blank/nil base returns strategy unchanged. re-frame.routing/with-base-path v1 advanced 012

A :url-bound? true frame's browser URL-change listener is installed / removed by the FRAME LIFECYCLE, automatically — creation installs (strategy-aware: popstate for history, hashchange for hash, per :url-strategy), destroy removes. There is no imperative install/remove pair to call — no install-url-listener! / remove-url-listener! / install-history-listener! / remove-history-listener!.

reg-route's routing-owned metadata reserved keys: :doc, :params, :query, :query-defaults, :tags, :parent, :on-match, :can-leave, :can-enter, :scroll, :sensitive, :large. Two cross-feature bare keys are accepted beside them: :head, owned by SSR (011) and always accepted; and :resources, owned by the Resources artefact (016) and accepted only when that artefact publishes its :routing/extra-route-keys hook. The URL :path pattern is the third VALUE slot, not a metadata key. Canonical detail in 012-Routing.md §Reserved route-metadata keys; shape in Spec-Schemas §:rf/route-metadata.

route-link click rules: a plain primary-button click (no modifier keys, no defaultPrevented) calls .preventDefault and dispatches [:rf.route/navigate {:url <synthesised>}], plus any of the link's :replace? / :scroll / :bypass-leave? — no address key, because a raw URL IS the address (§The request grammar) and the handler re-derives the route from it, so route ranking resolves a click exactly as it resolves a reload of the href. Modifier-key clicks (cmd / ctrl / shift / alt) and auxiliary-button clicks (middle-click) defer to the browser so the native href opens in a new tab. A caller-supplied :on-click runs first; if it calls .preventDefault (or otherwise leaves defaultPrevented true) the framework's interception is skipped. Keys other than :to / :params / :query / :fragment / :prefetch / :on-click pass through to the underlying <a> element. Detailed semantics in 012-Routing.md §Linking from views.

Standard route-related events:

Event Notes Spec
:rf.route/navigate Navigate to a registered route. 012
:rf.route/handle-url-change URL-change handler for link / popstate / initial load / SSR; the cause rides :rf.route/cause (:link, :popstate, :initial, :ssr are the framework's own feeds); default scroll :top for :link, else :restore. 012
:rf.route/navigation-blocked A :can-leave guard rejected a navigation. 012
:rf.route/entry-denied A :can-enter guard rejected navigation into a route. TERMINAL — nothing commits and no pending value is created; dispatched exactly once per attempt, carrying {:destination :target :cause :requested-url :guard}. A framework no-op default handler ships, so denial is safe with no application handler. 012
:rf.route/continue User-dispatched event proceeding a blocked navigation. 012
:rf.route/cancel User-dispatched event abandoning a blocked navigation. 012
:rf.route/prefetch Warm-mode resource-only intent preload: [:rf.route/prefetch {address}] runs a named destination's effective resource plan ownerlessly WITHOUT navigating (no route state, guards, :on-match, or readiness change). 012
:rf.route/replan-resources Same-token replan of the ACTIVE route: [:rf.route/replan-resources {:cause <edn>}] reruns its effective resource plan against the current app-db WITHOUT navigating — same address, same nav-token, same owner; kept identities adopted, added ensured with the caller's :cause, dropped released; readiness re-projected. :cause is required. Not a reload; no guards, :on-match, URL or scroll work. 012

Standard route-related subs:

Sub Returns Spec
:rf/route The full :rf/route slice {:route-id :params :query :fragment :transition :error :nav-token} 012
:rf.route/id Current route id 012
:rf.route/params Current path params 012
:rf.route/query Current query params 012
:rf.route/transition :idle / :loading / :error 012
:rf.route/error Current error map (when :transition = :error) 012
:rf.route/fragment Current URL fragment (string or nil) 012
:rf.route/chain Vector of route ids from parent-most to current (per :parent links) 012
:rf/pending-navigation The pending-nav slot (per :rf/pending-navigation schema) when a navigation is blocked; nil otherwise 012

The route and the pending-nav slot are read as ordinary subscription vectors — @(rf/subscribe [:rf/route]) and @(rf/subscribe [:rf/pending-navigation]); the :rf.route/* granular subs above chain off [:rf/route]. There is no named-read-sugar fn: a runtime-db framework read is a subscription vector, one grammar (per Conventions §Reserved sub-ids).

Standard route-related fx (canonical detail in 012-Routing.md):

Fx Args Platforms
:rf.nav/push-url URL string :client
:rf.nav/replace-url URL string :client
:rf.nav/scroll scroll-spec map :client
:rf.nav/capture-scroll {:url <leaving-route-url>} :client
:rf.route/with-nav-token {:rf/reply-to <reply-target> :value <v> :nav-token <token> :route-id <route-id>} (per 012 §Threading the nav-token) universal

Standard route-related cofx (canonical detail in 012-Routing.md):

Cofx Delivers Spec
:rf.route/nav-token The active navigation epoch token (read from [:rf.runtime/routing :current :nav-token]), delivered flat under the coeffect key :rf.route/nav-token — declare via {:rf.cofx/requires [:rf.route/nav-token]} in an :on-match-reached handler to capture the epoch live at scheduling time for stale-result suppression. Per 012 §Navigation tokens. 012
:rf.route/route-id The current route id (read from [:rf.runtime/routing :current :route-id]), delivered flat under the coeffect key :rf.route/route-id. The capture-side companion to :rf.route/nav-token: a route loader declares both ({:rf.cofx/requires [:rf.route/nav-token :rf.route/route-id]}) so it captures the two facts the route-loader work id [:rf.work/route route-id nav-token loader-id] needs at scheduling time, rather than reading the route id from the live slice at stale-arrival — where a cross-route completion would read the superseding route's. Per 012 §Navigation tokens. 012

SSR (Spec 011)

Namespace: the surfaces below live in re-frame.ssr (artefact day8/re-frame2-ssr); consumers (:require [re-frame.ssr :as ssr]). The head surface is defined in the sibling re-frame.ssr.head, which consumers may (:require [re-frame.ssr.head :as head]) directly — re-frame.ssr re-exports head-model and head-model->html from it, so the whole read side of the head contract sits beside render-to-string. The reg-head and reg-error-projector REGISTRARS are rowed in §Registration and ride the re-frame.core façade like every other artefact's registration macro. The Ring host-adapter lives in re-frame.ssr.ring (artefact day8/re-frame2-ssr-ring); its two handler constructors are its entry points and are rowed below, namespace-qualified. The SSR query surface is NOT re-exported through re-frame.core: loading re-frame.ssr is what installs the SSR runtime — the :rf/hydrate event, the :rf.ssr/check-* fx, the seven :rf.server/* fx and the :rf.server/request cofx — so no SSR app can be one that has not named the artefact namespace, and the guided :rf.error/ssr-artefact-missing a façade copy would exist to give would be unreachable. The Ring host-adapter (re-frame.ssr.ring) is likewise reached at home; an SSR-aware host requires the namespace directly. Apps targeting SSR add the artefacts to their deps regardless.

The ssr-node crossing has three host-adapter namespaces, all requires-directly and none re-exported. re-frame.ssr.ring.node (in day8/re-frame2-ssr-ring) provides renderer, the one non-local :renderer the reference ships — the JVM→Node adapter over the bounded sidecar at implementation/ssr-node. re-frame.ssr.render-state (in day8/re-frame2-ssr) is the render-visible projection the seam runs — project / serialize / deserialize / restore! over the two-partition envelope. re-frame.fresco.server (in day8/re-frame2-fresco, CLJS) provides render-body, the body-only entry a server bundle calls, and node-module, which builds that bundle's whole render module around it. Their contracts are 011 §Client-side hydration boot helper; the recipe is Render on Node. These are :implementation-tier host-adapter plumbing on the same footing as re-frame.ssr.ring's own vars other than its two handler constructors, and are not rowed below; all three are carried by spec/api-manifest.edn at that tier — the two JVM-loadable ones by generator introspection, and re-frame.fresco.server by curated :cljs-only rows, since it requires react-dom/server and cannot be loaded on the JVM.

reg-head and reg-error-projector are rowed canonically in §Registration. The head-fn signature is (fn [db route] head-model); the projector-fn signature is (fn [trace-event] :rf/public-error), and a projector-fn that returns nil defers that error to the default projector.

API M/Fn Signature Status Tier Spec
render-to-string Fn (render-to-string view-or-hiccup opts) → HTML string v1 advanced 011
hydration-payload Fn (hydration-payload frame opts) → :rf/hydration-payload. The payload a host running its own request lifecycle ships: the frame's app-db through the fail-closed :payload policy and its :sensitive classification, plus the durable runtime-db projection, under ssr-handler's payload options (:payload, :client-frame-id, :schema-digest). A :payload entry is a top-level key or a path, and a path that names a classified leaf ships it raw. frame is a frame id or value, and must still be live. Per 011 §:rf/app-db projection v1 advanced 011
project-error Fn (project-error frame-id trace-event) → :rf/public-error. Applies the active error-projector (selected by the frame's :ssr {:public-error-id ...} metadata) for the named frame. Per 011 §Server error projection. v1 advanced 011
head-model Fn (head-model frame-id) / (head-model frame-id {:head-id id :route route}) → :rf/head-model. The ONE head read (there is no render-head / active-head). Selection: an explicit :head-id, else the effective route's :head, else default-head; a selected-but-unregistered id raises :rf.error/no-such-head. The effective route is :route when the key is present (an explicit nil means no route), else the frame's [:rf.runtime/routing :current] slice — and the head fn is evaluated against that SAME route. Re-exported on re-frame.ssr from re-frame.ssr.head. Head rendering is a frame-scoped read, so the frame is carried, not ambient: there is no no-arg form (EP-0002) and a nil frame-id raises :rf.error/no-frame-context rather than resolving against a synthesised default frame. v1 advanced 011
head-model->html Fn (head-model->html head-model) / (head-model->html head-model {:wrap? bool}) → inner-head HTML string. Re-exported on re-frame.ssr from re-frame.ssr.head v1 advanced 011
hydrate! Fn (hydrate! {:frame f :payload p :root-id id}) → the applied payload, or nil on a client-only first load and for a :frame that is not live. The supported client boot: READ the payload (from :payload, else the DOM's __rf_payload) → dispatch-sync [:rf/hydrate …] against :frame. Hydration mismatches are reported by the hydrating root's React adoption, not by hydrate!. Install is idempotent across a page's roots; a conflicting payload for the same id raises :rf.error/frame-payload-conflict. Per 011 §Client-side hydration boot helper v1 advanced 011
re-frame.ssr.ring/ssr-handler Fn (ssr-handler opts) → a synchronous Ring handler (fn [ring-request] ring-response) rendering one request per call: a per-request frame made from the app's :frame config, the drain of its :initial-events, the :root-view render in the HTML shell with the hydration payload. Per 011 §HTTP response contract v1 advanced 011
re-frame.ssr.ring/ssr-middleware Fn (ssr-middleware opts) → Ring middleware (fn [handler] wrapped-handler) that renders the requests its :match? predicate accepts through ssr-handler and passes the rest to the wrapped handler v1 advanced 011

Standard SSR-related events:

Event What it does Spec
:rf/hydrate Seed the client-side app-db from the server-supplied payload. Runs once on client bootstrap. 011

Standard SSR-related fx (server-only; :platforms #{:server}):

Fx Args Spec
:rf.server/set-status :int (per :rf.fx.server/set-status-args) 011
:rf.server/set-header {:name :value} (per :rf.fx.server/set-header-args) 011
:rf.server/append-header {:name :value} (per :rf.fx.server/append-header-args) 011
:rf.server/set-cookie :rf.server/cookie map 011
:rf.server/delete-cookie {:name ?:path ?:domain} 011
:rf.server/redirect {:location ?:allow ?:status} (default :status 302); truncates HTML. Relative-only by default: :allow names the hosts permitted beyond the request's own origin, and :allow :any waives the host check. A :location failing the URL gate (a javascript: / data: / vbscript: or other non-http(s) scheme, an unparseable URL, a host not permitted) is refused and no redirect is written. Open-redirect mitigation for attacker-controlled ?next= strings 011

Standard SSR-related fx (client-side hydration compatibility checks; :platforms #{:client}):

Fx What it does Spec
:rf.ssr/check-version Hydration-side framework version-compatibility check (emits :rf.ssr/version-mismatch on drift). 011
:rf.ssr/check-schema-digest Hydration-side schema-digest compatibility check (emits :rf.ssr/schema-digest-mismatch on drift). 011

Implementation-tier SSR internals (not rowed). The remaining client hydration lifecycle — read-server-payload, drain-blocking-resources!, and the per-request response / request accessors (get-response / peek-response / flush-response! / get-request / set-request! / clear-request!) — lives in re-frame.ssr but is :implementation-tier host-adapter plumbing. The supported client path is hydrate! (rowed above, :advanced); a host may instead seed with read-server-payload + dispatch-sync [:rf/hydrate …] (011 §Client-side hydration boot helper). Per the projection's implementation-tier policy (§Tier taxonomy) they are carried by the manifest but not rowed here — the per-request response accumulator (:rf/response) is read through these accessors, not a subscription.

Standard SSR-related subs: there are none. re-frame.ssr and re-frame.ssr.ring register no subscriptions at all — their only registrations are the :rf/hydrate event, the server-only and client-only fx above, and the :rf.server/request coeffect. In particular there is no :rf/head sub and no :rf/public-error sub; @(rf/subscribe [:rf/head]) cannot resolve. Both keywords name a data shape registered in Spec-Schemas (:rf/head-model and :rf/public-error), not a registry entry. Read them through fns instead: the head model via head-model (it RETURNS the model — there is no side-channel register to read back), and the public-error projection via project-error. (docs/api/re-frame.ssr.md §Subscriptions says the same. api_md_check skips keyword rows by design, so no gate checks this statement.)

Standard cofx (server-only):

Cofx Returns Spec
:rf.server/request The active HTTP request map 011

reg-fx's :platforms metadata key (a set containing :server and/or :client) gates fx execution by active platform; default #{:server :client} (universal) when the key is absent. Skipped fx emit a :rf.fx/skipped-on-platform trace event. Detail in 011 §:platforms metadata on reg-fx.

SSR error-projection policy is per-frame metadata (see Conventions §Configuration surfaces bucket 3): a frame opts in via the :ssr {:public-error-id ... :dev-error-detail? ...} map on its make-frame / frame-root config. See 011 §Server error projection for the keys.


HTTP requests (Spec 014)

:rf.http/managed is the canonical, optional HTTP-request fx — v1 (optional capability). CLJS reference ships it on Fetch (browser) and java.net.http.HttpClient (JVM). Args, behaviours, decode pipeline, retry semantics, abort surface, failure taxonomy, and reply addressing are normatively defined in 014-HTTPRequests.md; the surface below is the API-level summary.

API Kind Signature / shape Status Tier Spec
:rf.http/managed fx [:rf.http/managed args-map] — args per 014 §The args map and :rf.fx/managed-args v1 (optional capability) — (fx-id; follows the advanced HTTP artefact) 014
:rf.http/managed-abort fx [:rf.http/managed-abort request-id] — abort the in-flight request with the given :request-id v1 (optional capability) — (fx-id) 014
reg-http-interceptor M (reg-http-interceptor id interceptor-map) — register the frame-agnostic HTTP interceptor definition id under the registrar kind :http-interceptor (per 014 §Middleware). A façade macro (re-frame.core, capturing call-site source-coords; the fn form is re-frame.http.middleware/reg-http-interceptor). id is a keyword; interceptor-map carries at least one of :before (fn [ctx] ctx') and :after (fn [ctx response] response'), plus any :rf/registration-metadata (:doc / :tags / :schema); a :frame is :rf.error/http-bad-interceptor. A frame runs the definitions its :http-interceptors config names: :before in that vector's order, :after in reverse (014 §Activation). The surface mirrors the event-interceptor {:id :before :after} shape — symmetric request/response sides. v1 (optional capability) advanced 014
— — Clearing an HTTP interceptor is (rf/clear :http-interceptor id), rowed in §Clearing registrations — there is no clear-http-interceptor name. It removes the definition for every frame; a frame still naming the id fails its next request at issue with :rf.error/unregistered-interceptor. — — 014

Public API surface in re-frame.core for ports that ship Spec 014. Ports that omit it MUST NOT register :rf.http/* for any other purpose (per Conventions §Reserved namespaces).

The HTTP stub transports are NOT re-frame.core façade exports — they are test and demo infrastructure reached through their home namespace re-frame.http.stub-transport ((:require [re-frame.http.stub-transport :as rf.http.stub-transport])), so they carry no rows above. A stub transport replaces only the host send, so decode, :accept, retry and the privacy scrub run on its answers (014 §Stub transports). (route-transport routes) / (route-transport routes {:delay-ms n}) answers from a route map {[<method> <url>] response-or-fn} or one (fn [wire-request] response); (pending-transport) returns {:transport t :exchanges a}, holding each request until the caller answers it; (with-transport transport thunk) scopes a transport to thunk's dynamic extent, for a test; and (install-transport! frame-id transport) puts one on a frame until the frame is destroyed, for a demo's fake backend.

Managed HTTP ships no public var namespace of its own. The request surface is the keyword-addressed [:rf.http/managed args-map] fx and its siblings above; the interceptor surface reaches users through the re-frame.core façade, and the stub transports directly through re-frame.http.stub-transport. There is no per-verb call-site helper namespace re-frame.http — an app that wants a shorter call site writes its own request-builder fn over the args map (base URL, default headers, a default :decode — policy a per-verb fn cannot carry).

Reply-payload shape

Every reply lands as the canonical uniform reply envelope (one dialect, no {:kind :success/:failure} reshape): {:status :ok :value v …} on success, {:status :error :error {:kind <:rf.http/*> …} …} on failure, {:status :cancelled :error {:kind :rf.http/aborted …} …} on abort. Reply addressing is explicit: :reply-to is the unified app-facing spelling — one event vector for both the success and the failure reply, the envelope appended as the last event-vector arg (the app branches on (:status reply)); the same :reply-to key resources / mutations use. :on-success / :on-failure are the split routing sugar — a named target per branch, supplied as a pair, both receiving the identical envelope. The two styles are exclusive: a map carrying :reply-to beside either branch key raises :rf.error/http-bad-reply-target (:reason :mixed-addressing) at dispatch, and so does one split key without its partner (:reason :unpaired-split), both on key presence rather than value — an explicit nil partner silences that branch. All lower to the one internal / normalized :rf/reply-to descriptor (a conformance surface, not an everyday spelling). Omitting all reply targets raises :rf.error/http-no-reply-target at dispatch — the framework never addresses a reply implicitly (there is no co-located default routing the reply back to the originating event), because an implicit target can swallow failures. Detailed in 014 §Reply addressing and §Reply payload shape.

Failure categories (closed set)

The seven :kind values inside a failure reply, all reserved under :rf.http/* (per Conventions §Reserved namespaces). See 014 §Failure categories for tags-by-kind:

:kind Meaning
:rf.http/transport Network / DNS / connection error pre-HTTP (in the browser, any Fetch network rejection; :cross-origin? true marks one against a cross-origin URL, which may be a CORS refusal)
:rf.http/timeout Per-attempt timeout fired
:rf.http/http-4xx Non-2xx 4xx response
:rf.http/http-5xx Non-2xx 5xx response
:rf.http/decode-failure 2xx response but decode rejected the body
:rf.http/accept-failure :accept returned {:failure user-map}
:rf.http/aborted Request aborted via :request-id or :abort-signal

Trace events emitted by :rf.http/managed

:operation :op-type When
:rf.http/retry-attempt :info Per intermediate attempt that matched :retry :on, plus a terminal retry-sequence stop marker once the sequence ends (budget spent, or a later attempt failed outside :retry :on); carries :request-id, :url, :attempt, :max-attempts, :failure, :next-backoff-ms (nil on the terminal stop marker)
:rf.http.interceptor/registered :info A reg-http-interceptor succeeded; carries :id (per 014 §Middleware)
:rf.http.interceptor/cleared :info A (clear :http-interceptor id) removed an existing definition; carries :id
:rf.error/http-interceptor-failed :error An interceptor :before or :after threw; carries :frame, :interceptor-id, :url, :cause (plus :phase :after on the response side). Request side: the request is NOT dispatched; response side: the reply is suppressed (per 014 §Middleware §Failure mode)

Resources (Spec 016)

The Resources artefact (day8/re-frame2-resources, post-v1 optional) ships declarative cached server-state. Its full registration / event / sub / accessor surface is normatively defined in 016-Resources.md; this projection rows the facade exports classified by the standing diff-time rule (every new re-frame.core facade export is classified + justified when it lands). The resource/mutation registrars and the :rf.resource/* / :rf.mutation/* keyword-addressed events, subs, and trace ops follow the artefact's tier (optional capability) and carry no Tier column (they are not vars).

API M/Fn Signature Status Tier Spec Notes
reg-resource M (reg-resource resource-id metadata request-fn) — canonical 3-slot grammar: the :request fetch fn is the third VALUE slot, metadata the reflection + config map (:scope, :params-schema, :data-schema, …) post-v1 lib (optional capability) advanced 016 The :resource registrar kind. Late-bound by the Resources artefact. A :request left inside the metadata map is a loud :rf.error/resource-bad-spec. Front-room of the resources surface but advanced (not front-porch): an optional post-v1 capability a new app does not reach for on day one.
— — (rf/clear :resource resource-id) — rowed in §Clearing registrations; there is no clear-resource name. Registration-lifecycle removal (the registrar decrement per Conventions §Tear-down verb axis); disposes resource-runtime state per 016 §Registration. — — 016
reg-mutation M (reg-mutation mutation-id metadata request-fn) — canonical 3-slot grammar: the :request write fn is the third VALUE slot, metadata the reflection + config map (:params-schema, :invalidates, …) post-v1 lib (optional capability) advanced 016 The :mutation registrar kind — a named causal write. A :request left inside the metadata map is a loud :rf.error/mutation-bad-spec. :rf.mutation/execute carries the call-site :reply-to continuation target.
— — (rf/clear :mutation mutation-id) — rowed in §Clearing registrations; there is no clear-mutation name. Registration-lifecycle removal of a mutation. — — 016
reg-resource-scope M (reg-resource-scope scope-id metadata resolve-fn) — register a pure named db-derived scope resolver in the canonical 3-slot grammar: the :resolve fn is the value slot, and metadata carries the declared :inputs {name [:db <rf-path>]} (plus optional :doc). :inputs is REQUIRED and there is ONE arity: the resolver's first arg is ALWAYS the resolved inputs map. Reading the whole db is spelled {:inputs {:db [:db []]}}, from which the tooling-marked explicit-cost :whole-db? is DERIVED. A :resolve inside the metadata map is rejected loudly. The :resource-scope registrar kind. post-v1 lib (optional capability) advanced 016 Facade classification (EP-0016 D3): a re-frame.core export of the optional Resources artefact, justified as the single scope-resolution currency reused by resource registration, route resources, event ensure, subscriptions, invalidation descriptors, exact targets, and clear-scope. advanced — an optional-artefact authoring surface, not front-porch. Per 016 §Named resource-scope resolvers.
— — (rf/clear :resource-scope scope-id) — rowed in §Clearing registrations; there is no clear-resource-scope name. The registrar decrement counterpart of reg-resource-scope (per Conventions §Tear-down verb axis). — — 016
resolve-resource-scope Fn (resolve-resource-scope db scope-id) — resolve a named scope resolver against a supplied db value; returns the canonical scope or nil. A pure resolver helper, not an effect (no app-state / dispatch side effects) and no observability side effect — it routes through the trace-free pure evaluator, so unlike the causal resolution sites it does not emit :rf.resource/scope-resolved. post-v1 lib (optional capability) advanced 016 Facade classification (EP-0016 D3 / issue 7): a re-frame.core export justified as the ergonomic helper for the logout/account-switch idiom — resolve the concrete old scope from the handler's coeffect db (no :snapshot-db payload, which would be an egress-bearing record under EP-0015). A plain function over the resolver registry; no new effect-API surface, no resolution-timing ambiguity. It routes through the trace-free pure evaluator, so — unlike the causal {:from-db …} / route-entry / mutation-settle resolution sites — it does not emit :rf.resource/scope-resolved (a passive read advertised as pure has no observability side effect). Per 016 §clear-scope resolves the concrete scope from the coeffect db.
— — A registered resource's / mutation's spec map has no per-kind accessor: it is (:rf/resource (rf/handler-meta {:kind :resource :id id})) and (:rf/mutation (rf/handler-meta {:kind :mutation :id id})) — the generic registrar query plus the documented inner-key projection, rowed in §Public registrar query API. — — 016
re-frame.resources/resource-state Fn (resource-state {:resource … :scope … :params … :frame …}) — a resource instance's live runtime state (explicit frame target) post-v1 lib (optional capability) advanced 016 Inspection for tools and tests, so it is called in re-frame.resources, not through rf/ (§Conventions, the facade rule by role).
re-frame.resources/mutation-state Fn (mutation-state {:instance … :frame …}) — a mutation instance's durable runtime row ({:status :result :error …}), or nil (explicit frame target) post-v1 lib (optional capability) advanced 016 Inspection, called in re-frame.resources like resource-state.

A :revalidate-on #{:focus :reconnect} frame's focus/reconnect listeners are installed / reconciled / removed by the FRAME LIFECYCLE, automatically — creation installs exactly the declared subset (:focus covers window focus AND document visibilitychange-to-visible; :reconnect covers window online), re-registration reconciles (replace-don't-stack, and a re-registration that drops the key relinquishes), destroy removes. :revalidate-on is a frame-config key on make-frame / frame-root, not a call: there is no install/remove fn (no install-revalidation-listeners! / remove-revalidation-listeners!). An absent key or an explicit #{} installs nothing; declaring the key without the Resources artefact on the classpath fails loud with :rf.error/resources-artefact-missing. This is the same shape as :url-bound? — see §Routing.

Request decoration (EP-0016 Rider 3) reuses the HTTP facade. Auth/tracing/base-URL decoration for resources and mutations is not a resources surface — it is reg-http-interceptor / (rf/clear :http-interceptor id) (§HTTP requests), registered once and named in a frame's :http-interceptors, which applies it to every :rf.http/managed request the frame issues (resource reads, mutations, plain managed calls). An interceptor decorates the :request envelope only; retry is per request, a top-level :retry the resource or mutation :request fn returns. There is no decoration facade export; the doctrine is ownership (transport decoration lives in the managed-HTTP seam), per 016 §Request decoration belongs to the managed-HTTP seam.


Effect-map shape

Closed: three top-level keys — #{:db :rf.db/runtime :fx}. Ordinary app handlers return only :db + :fx; :rf.db/runtime is reserved by convention for framework / runtime-extension authority (it writes the runtime-db partition — non-framework handlers emitting it are surfaced by dev diagnostics, not silently dropped). See Spec-Schemas §:rf/effect-map. Top-level :dispatch / :dispatch-later / :dispatch-n from v1 migrate via MIGRATION.md §M-8.

A foreign top-level key REFUSES the event. Any other top-level key is caught pre-commit at the router's FINAL-effects boundary, emits :rf.error/effect-map-shape on the always-on error channel, and aborts the event — no :db, no :rf.db/runtime, no :fx. No partial commit, and nothing is silently dropped. App-db classification is a registration, not an effect: a handler returning :sensitive / :large / :clear-sensitive / :clear-large is refused this way, its :reason naming reg-classification (015 §Static app-db classification).

Key Notes
:db New app-db partition (replaces). The app-facing state key.
:rf.db/runtime New runtime-db partition (replaces). Reserved by convention for framework / runtime-extension authority — ordinary app handlers do not emit it.
:fx Vector of [fx-id args] pairs.

Standard :fx entries:

[fx-id args] Args Status Spec Notes
[:dispatch [event-id ...]] event vector v1 002
[:dispatch-later {:ms ms :event event-vec}] options map v1 002 An optional :id names the timer within the frame: re-arming it replaces the pending timer, and {:id id :cancel? true} cancels it (per 002 §:fx ordering and atomicity guarantees).
[:rf.http/managed args-map] args per 014 §The args map v1 (optional capability) 014 Framework-provided when the implementation ships Spec 014. CLJS reference: ships on Fetch + JVM HttpClient. See also :rf.http/managed-abort.
[:rf.nav/push-url url-string] URL string v1 012
[:raise event-vec] event vector v1 005 machine-only: reserved fx-id recognised by the machine handler; routes the event back into the same machine, atomic and pre-commit. Outside a machine action's :fx, this fx-id is unbound.
[:rf.machine/spawn spawn-spec] spawn-spec map (per :rf.fx/spawn-args: :machine-id (a registered machine TYPE), :id-prefix, :data, :start) v1 005 Canonical actor-lifecycle fx (registered globally by re-frame.machines); installs a new dynamic actor (whose snapshot lives at [:rf.runtime/machines :snapshots <gensym'd-id>]). On the declarative :spawn / :spawn-all path the reducer records the assigned id at the spawning machine's [:rf/runtime :spawned], which its callbacks read as the :spawned context key (per 005 §Recording the spawned id user-side). Emitted from any event handler's :fx (including machine actions and the :spawn desugar).
[:rf.machine/destroy actor-id] actor id (keyword) v1 005 Canonical actor-destroy fx (registered globally by re-frame.machines); runs the actor's :exit action, dissociates [:rf.runtime/machines :snapshots <actor-id>], and clears the actor's event-handler registration. Symmetric counterpart to :rf.machine/spawn.

Public registrar query API

For tooling, agents, story tools, 10x.

One query map. Each of registrations / handler-meta / ambiguous-registrations takes exactly one argument: a query map of :kind, the optional :frame and, for handler-meta, :id. Without :frame it reads the registration store; with :frame f it reads live frame f's resolved program.

(rf/registrations {:kind :event})            ; => {id metadata} or {}
(rf/registrations {:frame app-frame :kind :event})
(rf/handler-meta  {:kind :event :id :cart/add})  ; => metadata or nil
(rf/handler-meta  {:frame app-frame :kind :resource :id :articles/list})
(rf/ambiguous-registrations {:kind :event})  ; => {id {provenance-ns metadata}} or {}

The store read consults no bound frame view. re-frame.registrar's generation-aware reads consult the dynamic image generation FIRST, and that generation is bound around every subscribe build, dispatch, fx and view resolution against an image-loaded frame, so a store read routed through them from inside a sub computation or an event handler would silently read that frame's image. A query without :frame never routes through them: it reads the registration store's own view, so it answers the same inside any frame as it does at the REPL — registrar enumeration is frame-neutral (002 §Frame target resolution).

Failures. A key other than :kind, :frame and handler-meta's :id, or a non-map argument, throws :rf.error/registrar-query-invalid, whose ex-data names the offending keys — a misspelled :frame would otherwise read the store without a word. :kind :frame (the reserved-but-EMPTY registrar slot) throws :rf.error/registrar-kind-not-queryable, whose message names the real door — frame-ids / frame-meta — rather than returning an authoritative-looking {}. Any other non-registry kind throws :rf.error/unknown-registry-kind.

Projections are Clojure, not library operations. There is no :pred key and no predicate arity: (keys (rf/registrations {:kind :route})), or (into {} (filter (fn [[_ m]] (:rf/machine? m))) (rf/registrations {:kind :event})).

Inner-key projection is the documented contract. A registration that carries a feature SPEC exposes it under a reserved inner key on its own metadata, and reading that key is the supported way to reach it — there is no per-kind <kind>-meta accessor. A machine is an :event registration carrying :rf/machine? true, and its registered spec reads back as:

(:rf/machine (rf/handler-meta {:kind :event :id :auth.login/flow}))

which is nil unless that :event registration is a machine. See 005 §Querying machines.

The complete set of inner keys is one per registrar kind that nests a spec:

Kind Inner key Reads back
:event (with :rf/machine? true) :rf/machine the reg-machine spec
:resource :rf/resource the reg-resource spec
:mutation :rf/mutation the reg-mutation spec
:resource-scope :rf/resource-scope the reg-resource-scope spec

Every other kind — :route among them — carries its metadata at the TOP level of the registration, so handler-meta alone is the whole read. Enumeration is likewise generic: (keys (rf/registrations {:kind :resource})), and so on for every kind.

No realm coordinate. (registrations {:realm r :kind k}) and friends do not exist, there is no re-frame.realm namespace, and frame resolution routes directly through the process registrar (re-frame.registrar / re-frame.frame / re-frame.image).

Frame-targeted query. The map-shaped form — (registrations {:frame f :kind k}), (handler-meta {:frame f :kind k :id id}) — resolves the (kind, id) set through live frame f's own view — its sealed image generation ("target frame → resolved image generation → registration resolution"), or, for a frame made without :images, the live store as that frame resolves it (an ambiguous id: handler-meta throws :rf.error/ambiguous-registration, registrations omits it) — surfacing the :rf.provenance/ns + inline/image facts the resolved descriptors carry (cross-image overrides are reported on the generation's :rf.gen/shadows shadow report). :frame is a registered frame id (keyword) OR a frame value (rf/make-frame's return token) — the same target shape rf/frame-generation accepts; the public routing address is the frame id. This is the READ of the image→frame model: the public tooling surface a tool reaches instead of the internal re-frame.live-frame / re-frame.image-assembly namespaces. Fail-loud: a :frame that resolves to neither a live frame carrying a generation nor a frame made without :images throws :rf.error/frame-no-generation (no fallback to the default registrar — the read needs a live frame). The dedicated raw read frame-generation (below) returns the whole sealed generation. A query without :frame never touches a bound frame view — it resolves the process registration table exactly as a frame made without :images does (an id more than one namespace registers: handler-meta throws :rf.error/ambiguous-registration, registrations omits it, ambiguous-registrations lists it), so it answers the same from inside a frame-resolved context as it does from the REPL.

API M/Fn Signature Status Tier JVM-runnable? Spec
registrations Fn (registrations {:kind k}) / (registrations {:frame f :kind k}) → {id metadata-map}, {} if none. Use when you want metadata — registry walks that read source-coords, :sensitive, :rf/machine?, :platforms, etc. Without :frame it reads the process registration table as a frame made without :images resolves it, less any ambiguous id, and never consults a bound frame view. :frame f resolves through live frame f's own view (only the ids its sealed image generation carries, or, for a frame made without :images, the ids it can run from the live store, less any ambiguous id; with :rf.provenance/ns provenance facts); f is a frame id or a direct frame object, fail-loud on an unresolvable one. A key other than :kind and :frame, or a non-map, is :rf.error/registrar-query-invalid; filtering is filter over the result (no :pred). See the notes above. v1 tooling ✓ 002
handler-meta Fn (handler-meta {:kind k :id id}) / (handler-meta {:frame f :kind k :id id}) → registration-metadata map, or nil. View registrations include source-coord keys (:ns / :line / :column / :file) per :rf/source-coord-meta (Spec-Schemas); pair tools resolve data-rf2-source-coord DOM annotations to :file via this lookup. Without :frame it reads the process registration table as a frame made without :images resolves it — an ambiguous id throws :rf.error/ambiguous-registration, naming the namespaces — and never consults a bound frame view; it is also the form the two DERIVED machine kinds take — (handler-meta {:kind :machine-guard :id [machine-id guard-id]}). :frame f resolves through live frame f's own view (its sealed image generation, or, for a frame made without :images, the live store, where an ambiguous id throws :rf.error/ambiguous-registration), with :rf.provenance/ns provenance facts, or nil when that frame resolves no such [k id]; fail-loud on an unresolvable f. A key other than :kind, :id and :frame, or a non-map, is :rf.error/registrar-query-invalid. See the notes above. v1 tooling ✓ 002
ambiguous-registrations Fn (ambiguous-registrations {:kind k}) / (ambiguous-registrations {:frame f :kind k}) → {id {provenance-ns metadata-map}}, {} if none — the ids of k more than one namespace registers, each with every registration of it keyed by the namespace that made it: the ids registrations omits and handler-meta throws :rf.error/ambiguous-registration for. Without :frame it lists the process registration table's; :frame f lists those live frame f refuses — the table's for a frame made without :images, {} for one running a sealed image generation. Same grammar and failures as registrations. v1 tooling ✓ 002
frame-generation Fn (frame-generation f) → the sealed, resolved image generation live frame f is running — the inert image-assembly value with the four documented stable public keys :rf.gen/resolver ({[kind id] descriptor}), :rf.gen/images (the normalized images in :images order — later wins), :rf.gen/kinds, and :rf.gen/shadows (the cross-image shadow report — [{:registration [kind id] :image <defined-in> :shadowed-by <winner>} …]; [] when nothing was overridden — read the report directly off this key). f is a registered frame id or a direct frame object. The dedicated raw read over the frame→generation model — for describe-image-style views that want the whole generation (selected registrations, shadow report, provenance) without per-kind round-trips; the {:frame f …} form of the pair above gives per-(kind, id) resolution with provenance. Fail-loud :rf.error/frame-no-generation when f does not resolve to a live frame carrying a generation — a frame made without :images carries none (no nil-as-default, no realm fallback). (There is no :rf.gen/requires capability slot and there are no image-declared host capabilities.) The public tooling surface tools (Pair MCP, Xray) reach instead of re-frame.live-frame / re-frame.image-assembly internals. EP-0023 tooling ✓ 002
frame-ids Fn (frame-ids) / (frame-ids ns-prefix) v1 tooling ✓ 002
frame-meta Fn (frame-meta frame-id) v1 tooling ✓ 002
app-db-value Fn (app-db-value frame-id) → the app-db partition value (plain map) — the out-of-band value read. The front-porch read is subscribe; app-db-value is the non-reactive snapshot read for tools, tests, REPL, and fx/handler bodies. v1 advanced ✓ 002
frame-state-value Fn (frame-state-value frame-id) → the coherent frame-state projection {:rf.db/app <app-db> :rf.db/runtime <runtime-db>}. The full-frame read for SSR / epoch / time-travel / Xray (EP-0001), and the supported production read for persistence — the value an app saves and installs back with :rf/install-frame-state. The runtime-db-only read (there is no runtime-db-value) is (:rf.db/runtime (frame-state-value frame-id)). v1 tooling ✓ 002

The static subscription-topology query and the runtime sub-cache snapshot are subscription-tooling surfaces, not re-frame.core facade reads. Reach them through their owning namespace re-frame.subs.tooling — (sub-topology) (static dependency graph over the registrar) and (sub-cache-snapshot frame-id) (live cache state) — with subs/sub-topology / subs/sub-cache-snapshot as JVM aliases.

Schema-introspection accessors — app-schemas, app-schema-meta, app-schemas-digest — are rowed canonically in §Schemas.

compute-sub is rowed canonically in §Dispatch and subscribe (sub computation against a supplied db value — the event-handler read of framework state, and the pure test form).


App values and composition (EP-0013)

Not on the public facade. This section is a stable anchor for inbound cross-references; the app/realm/module composition surface is not public.

The composition vocabulary is not on the public facade. The app/realm/module composition surface — rf/app / rf/module (+ the rf/app-registrations / rf/app-requires / rf/app-owns inspectors), rf/install! / rf/reinstall!, rf/realm / rf/dispose-realm!, and the realm reads rf/realm-ids / rf/frame-realm / rf/installed-app — is not part of re-frame.core. The public model is image → frame → event stream: a feature namespace registers ordinary reg-* forms; an rf/image selects them by :select-ns provenance (or defines them inline via :registrations) and is supplied to make-frame (see §Registration) via :images (the later image wins; the generation's :rf.gen/shadows report — read via frame-generation — names cross-image overrides); re-calling make-frame against the SAME :id with a new :images vector hot-reloads a frame's image generation in place (preserving frame memory; the reload diff is a read — generation-diff over two frame-generation values, not a bespoke verb); and a frame is addressed by its process-local frame id (or a frame value — the routing ops normalize a value to its id, so it is accepted directly), with no realm coordinate. There is no re-frame.realm namespace, no installed-app value, and no realm coordinate on any wire record (see Spec-Schemas §:rf/realm): frame resolution routes directly through the process registrar (re-frame.registrar / re-frame.frame / re-frame.image), not a realm. The app/realm/module construction model — and the EP-0013→EP-0023 migration mapping — is documented in EP-0023 §Backwards Compatibility and the EP history; it is not a live data surface (there are no rf/migration-map / rf/migration-explain facade reads). For tooling that reads a frame's resolved registrations, use the frame-targeted {:frame f …} registrar queries + frame-generation (§Public registrar query API).


Schemas

Namespace: the introspection surfaces below live in re-frame.schemas (artefact day8/re-frame2-schemas); consumers (:require [re-frame.schemas :as schemas]). They are not re-exported from re-frame.core — apps targeting schemas add the artefact and require the namespace directly. The registration macros (reg-app-schema / reg-app-schemas) live in re-frame.core and route through the schemas artefact at registration time. Per the §Conventions per-artefact namespace table.

reg-app-schema is rowed canonically in §Registration.

The reads' :frame is a frame TARGET. The :frame slot of the reads app-schemas / app-schema-meta / app-schemas-digest accepts either a frame-id keyword OR a frame value (rf/make-frame's return token), the same target shapes the registrar query API's :frame accepts, and a frame value answers exactly as its id does. An explicit :frame that resolves to a non-keyword target (a string, a non-frame map, a vector) fails loud with :rf.error/app-schemas-bad-arg rather than silently reading nothing. The registration surfaces take no frame: a :frame there is :rf.error/app-schema-frame-target.

The READS take ONE map, and :frame is REQUIRED on it. app-schemas / app-schema-meta / app-schemas-digest each take a single opts map. There is no ambient-frame default — a schema read is a tooling question ABOUT a named frame, not an operation inside one — and no bare-frame-id or trailing frame-target sugar: a live frame VALUE is itself a map, so a type-sniffing positional argument could never be read locally (Principles §Name over place). A frameless or non-map call raises the catalogued :rf.error/no-frame-context with a message naming the {:frame f} spelling; there is no new error id. Each read answers from the named frame's own resolution, the set (registrations {:frame f :kind :app-schema}) reads. The frame need not be LIVE: an id naming no frame answers each path's latest registration, as the registrar query API does.

The validator port is a VALUE, reached through one door. To swap the validator / explainer / humanizer / compiler / printer at boot (e.g. drop Malli for a clojure.spec or Zod-style port), use set-schema-fns! — the single installer, which takes any subset of them from one map so they never drift mid-boot. schema-fns reads back what is installed, and default-schema-fns is the framework's own bundle as a value. Those three cover every use: swapping one fn is a one-key install, restoring the framework defaults is (set-schema-fns! default-schema-fns), and capture / stub / restore is a let + finally over (schema-fns).

API M/Fn Signature Status Tier Spec
app-schemas Fn (app-schemas {:frame frame-id}) → {path registration-metadata} — the WHOLE app-db schema registration map the frame resolves, or {}: what (registrations {:frame frame-id :kind :app-schema}) answers. Each value carries :path, :schema and the source-coords :ns / :line / :file. (update-vals … :schema) projects the schema values alone. A path more than one namespace registers throws :rf.error/ambiguous-registration for a frame made without :images. v1 tooling 010
app-schema-meta Fn (app-schema-meta {:frame frame-id :path path}) — return the full registration-metadata map (:path, :schema, plus source-coords :ns / :line / :file and the rest of :rf/registration-metadata) of the app-db schema ONE path resolves to in the frame, or nil. Both keys are required. Pair-tool and 10x consumers reach for this when they need the registration anchor (e.g. click-back-to-code); (:schema …) is the schema value alone. Per 010 §Schemas as a tooling/agent surface and Spec-Schemas §:rf/app-schema-meta. v1 tooling 010
app-schemas-digest Fn (app-schemas-digest {:frame frame-id}) → string — computed over the {path → schema} projection of the set the frame resolves, so its bytes are independent of the metadata riding alongside. v1 tooling 010
set-schema-fns! Fn (set-schema-fns! {:validate validate-fn :explain explain-fn :humanize humanize-fn :compile compile-fn :print print-fn}) — the one validator-port door. Install any subset of the validator / explainer / humanizer / compiler / printer bundle from one map. Each key is optional; an absent key leaves the existing registration in place, so a one-key call is the way to swap a single fn. The one-call substitute-Malli boot pattern (a Zod / clojure.spec port installs all three together so they never drift mid-boot). :compile is (fn [schema] _), which development-build reg-app-schema / reg-app-schemas call before registering so a malformed schema throws there; a call that installs :validate and names no :compile clears it, since a compiler belongs to the validator it compiles for. :print nil coerces to the default EDN canonicaliser so the digest is never undefined; :validate nil / :explain nil / :humanize nil / :compile nil disable that fn. Last-write-wins per key; writes are NOT transactional. Returns the installed bundle as a map {:validate … :explain … :humanize … :compile … :print …} reflecting the live state of every fn after the call — including keys the call did not touch, so a caller wanting back just the fn it installed selects that key. Also the restore path: install a value read from schema-fns, or default-schema-fns. Per 010 §Default validator and the validator-fn extension point. v1 advanced 010
schema-fns Fn (schema-fns) → {:validate … :explain … :humanize … :compile … :print …} — the READ half of the port. Returns the installed validator / explainer / humanizer / compiler / printer in the same shape set-schema-fns! accepts and returns, so (set-schema-fns! (schema-fns)) is a no-op. :validate / :explain / :humanize / :compile may be nil; :print never is. This is what test isolation is built from — capture, stub, and reinstate in a let + finally, without a dedicated snapshot or restore verb and without reaching the framework-internal atoms. Per 010 §Default validator and the validator-fn extension point. v1 advanced 010
default-schema-fns Var (bundle value) default-schema-fns — the framework's own validator bundle as a plain map: the Malli adapter's :validate (each schema compiled once, cached weakly on the schema value), :compile (compiling into the same cache), :explain and :humanize (development builds; nil in production), completed with the EDN canonicaliser as :print; the state the port holds before an app installs anything. (set-schema-fns! default-schema-fns) restores the framework defaults. Per 010 §Default validator and the validator-fn extension point. v1 advanced 010

See 010 §Schemas for :schema metadata, validation timing, and dev/prod elision. (The framework does not accept a :spec metadata key on reg-* metadata; the key is :schema. Per MIGRATION §M-54.)


Event-emit (always-on, production-survivable)

Per 009 §What IS available in production (#2). A minimal always-on listener surface that survives :advanced + goog.DEBUG=false and delivers one tight record per processed event. Parallel to (not a fallback for) the dev-only trace surface; per-event only — no per-sub, per-fx, or per-:rf.event/db-changed records. Record shape {:event :event-id :frame :time :outcome :elapsed-ms} (the always-on event-emit substrate's own record fields, distinct from trace :tags keys); the :event slot is projected once before fan-out, in two steps resolved in the record's frame: first the event registration's own :sensitive / :large paths (and the per-event-id mutation and route layer), then the size-elision wire-boundary walker against the frame's durable classification. Declared sensitive paths land as :rf/redacted and declared large paths as :rf.size/large-elided; schema :sensitive? props reach neither step. :outcome is one of :ok (clean settle), :error (the interceptor chain threw, or a coeffect supplier or recordable generator threw while the context was assembled, before the chain ran; either way nothing is installed and no :fx runs), :rolled-back (a candidate :db / machine-data transition was refused before install), :flow-error (a flow's :output threw), or :rejected (a :boundary? true handler's :schema refused the event's payload, so the handler never ran) — an aborted dispatch is never reported as a clean :ok. Two of those five behave differently under a release build, and the pair is worth reading together: :rolled-back has no producer in production (candidate validation is dev-only), while :rejected does, because boundary validation is ungated per 010 §Production builds. It is not the only schema check a release build keeps — C-000.35 settles that by what the check is for, not by who declared the schema it reads, and the framework's other load-bearing checks are ungated alongside it — but it is the only one this :outcome vocabulary names in its own right; the others throw on their own paths, and a throw reports :error. See 009 §What IS available in production for the full per-member contract.

This substrate is IMPLEMENTATION tier; the public door is the sink. The registry fans an UNPROJECTED record across EVERY frame and is not routed under any frame's egress policy, so it is not a surface an application registers on: register-listener! has no :events stream (see §Observation listeners). Production observation for hosted back-ends (Datadog / Honeycomb / Sentry / …) is the frame-owned :observability :handled-events sink (Spec 015 §Frame-owned observability sink policy), declared on the frame's make-frame / frame-root config — or once per process with (rf/configure! {:observability …}) (§Configure keys) — and wired with register-observability-sink!, which hands the sink an already-PROJECTED record. A cross-frame seat is the process default; a raw record is an explicit :rf.egress/local-raw profile on that entry.

Sensitive data marking is path-based per Spec 015. There is no boolean handler-meta :sensitive? annotation; a registration's :sensitive [paths] is honoured on this record as on every other observation surface.

The always-on event-emit registry is addressable via re-frame.event-emit for framework-internal consumers and for tests (which also own between-scenario clears). It carries no public registration verb: an application declares a :handled-events sink instead.

Error-emit (always-on, production-survivable)

Per 009 §What IS available in production. Sibling of the event-emit surface above; runs through the always-on error-emit substrate. Survives :advanced + goog.DEBUG=false.

This substrate is IMPLEMENTATION tier; the public door is the sink. The registry fans the record across EVERY frame UNPROJECTED — the :event vector is wire-elided, but the :exception rides RAW and no frame egress policy is applied, so raw owner-local data can leave a frame here. That fail-open posture is why it is not an application-facing surface: register-listener! has no :errors stream (see §Observation listeners), and independent corpus observation regardless of a frame's policy is not a public primitive. The off-box error-observation surface is the frame-owned :observability :errors sink (Spec 015 §Frame-owned observability sink policy), declared on the frame's make-frame / frame-root config — or once per process with (rf/configure! {:observability …}) (§Configure keys) — and wired with register-observability-sink!, which PROJECTS the record under the owning frame's classification + the sink's egress profile (sensitive paths redacted, :exception dropped under :rf.egress/public-error) BEFORE the sink sees it. :exception was never the discriminator between the two doors: the sink also delivers :exception under the default :rf.egress/off-box-observability profile — only :rf.egress/public-error drops it, and then only the record's top-level throwable. The process default carries the two cases no frame's sink can: a FRAMELESS record (:frame nil) and a record whose frame incarnation is already dead both reach (rf/configure! {:observability …}) (§Configure keys) under an explicitly nil governing frame, and a dead frame's id still never resolves to a same-id successor's sink. A post-mortem shipper that needs the host throwable + stack declares :rf.egress/local-raw on its entry.

The listener payload is a union of three record shapes: (a) the per-event error record — {:rf.error/id :event :event-id :frame :time :exception :elapsed-ms} — fanned out by dispatch-on-error! once per catalogued production-reachable per-event runtime :rf.error/* event, and also carried by the frameless records, stamped :frame nil because no frame is in scope (:rf.error/no-frame-context, which has no :event and adds :reason / :recovery, and fresco's compute-sub :rf.error/sub-exception); (b) the frame-teardown report — {:rf.error/id :rf.error/frame-teardown-failed :frame :hook-failures :reason :recovery :time} — one bounded record per frame destroy whose best-effort cleanup hooks threw (EP-0008 promotion criterion; see 009 §Observability channels and the promotion criterion); and (c) the five EP-0008-promoted SSR non-event categories (:rf.error/ssr-render-failed, :rf.error/malformed-hydration-payload — incl. a pre-frame frameless :frame nil sub-path — :rf.error/ssr-head-resolution-failed, :rf.error/sanitised-on-projection, :rf.error/ssr-ring-error-view-failed), flat union records {:rf.error/id :frame :time …category keys…}. The two non-event arms (b) and (c) ride the general dispatch-error-record! helper and carry no :event / :event-id; the teardown report carries a :hook-failures vector instead of the per-event :exception / :elapsed-ms slots. Listener bodies MUST branch on (:rf.error/id record) (or otherwise tolerate a record with no top-level :event / :exception, or a nil :frame) rather than assuming the per-event shape. The per-event record's :event slot, when it carries a dispatched event, is projected once before fan-out by the same two steps as the event-emit record: the event registration's own :sensitive / :large paths, then the size-elision wire-boundary walker against the frame's durable classification. This is the single error-observability surface; recovery is the framework's typed per-category default, not app-steerable (there is no per-frame :on-error recovery policy). Per-listener exceptions are isolated — a buggy listener cannot block siblings or the run.

Sensitive data marking on the error-emit substrate is path-based per Spec 015. There is no boolean handler-meta :sensitive? annotation — the event registration's :sensitive / :large paths and the per-path elision wire-walker are the redaction steps on this path.

The always-on error-emit registry is addressable via re-frame.error-emit + the :error-emit/register-error-listener! late-bind hooks for framework-internal consumers (router fan-out, the Fresco server's one-render window) and for tests, which also own between-scenario clears. It carries no public registration verb: an application declares an :errors sink instead.

Observation listeners

One stream-parameterized listener verb registers an observation callback across the two raw dev observation streams — the differentiator is data (which stream), so it rides in a leading required stream keyword (in place of per-channel register-(trace|epoch)-listener! pairs). The closed stream vocabulary is :trace / :epoch; an unknown stream throws :rf.error/unknown-listener-stream (no bare trace default, no compatibility aliases). Both members are raw and DCE-able, so the vocabulary carries its own tier: this verb means raw dev stream.

Production observation is a different verb. The frame-owned observability sink is NOT a :sink stream here — it is declared as frame policy, or once per process via configure!, and delivers an already-PROJECTED record. This vocabulary has no always-on :events / :errors stream: one would be a second, fail-open production door — unprojected, raw :exception, no frame policy, fanned across every frame. Independent corpus observation regardless of a frame's policy is not a public primitive; the substrates are the implementation-tier registries re-frame.event-emit / re-frame.error-emit, for the framework's own capture sites and for tests.

Stream Axis Record
:trace dev-only (DCE'd in production) one trace event per call
:epoch dev-only (DCE'd in production) one :rf/epoch-record per dequeued event
API M/Fn Signature Status Tier Spec
register-listener! Fn (register-listener! stream id listener-fn) — register listener-fn under id on stream (:trace / :epoch). Re-registering the same id on the same stream replaces. Returns id. Unknown stream throws :rf.error/unknown-listener-stream. v1 tooling 009
unregister-listener! Fn (unregister-listener! stream id) → nil. v1 tooling 009

There is deliberately no facade clear-listeners! verb: dropping every listener on a stream is a test-isolation concern owned by the fixture layer, not the public facade. re-frame.test-support's reset clears the registries through the lower-level sinks directly (re-frame.trace.tooling/clear-listeners!, re-frame.event-emit/clear-event-listeners!, re-frame.error-emit/clear-error-listeners!, and epoch's lifecycle :test-reset), and drops the observability sinks, the process-default :observability policy and the trace-disabled frames with them. A listener a framework namespace installs at load, such as SSR's error projection, comes back through that artefact's reset hook.

Tracing

All tracing is dev-only (elided in production). See 009 §Tracing for emit semantics and synchronous listener delivery. Trace-listener registration uses the stream-parameterized listener verb with the :trace stream.

API M/Fn Signature Status Tier Spec
emit-trace-event! Fn (emit-trace-event! op-type operation tags) → nil v1 (dev-only) tooling 009
re-frame.interop/debug-enabled? Var ^boolean. CLJS: alias of goog.DEBUG — constant-folded by Closure under :advanced, so :advanced + goog.DEBUG=false builds DCE every (when interop/debug-enabled? ...) branch. JVM: a def read ONCE at ns-load from the Java system property -Dre-frame.debug (winning on conflict) or the environment variable RE_FRAME_DEBUG; reads true/1/yes/on as on and false/0/no/off/empty string as off, case-insensitively with whitespace trimmed; unset or any other value takes the default, false on a JVM that hosts SSR (the ssr-ring artefact on the classpath) and true (dev parity) elsewhere. Set BEFORE re-frame.interop loads. An SSR JVM's dev opts in with -Dre-frame.debug=true; webhook receivers and other long-running JVMs facing untrusted input MUST set the gate false explicitly — per 009 §JVM builds, 011 §The debug gate on an SSR JVM and Security §Production gates. v1 tooling 009
re-frame.performance/enabled? Var ^boolean goog-defined (CLJS) / ^:const false (JVM). Set via :closure-defines {re-frame.performance/enabled? true} to bracket event dispatch / sub recompute / fx walk / view render, emitting a single options-bag performance.measure(name, {start, end}) per bracket (User-Timing measure entries rf:event:*, rf:sub:*, rf:fx:*, rf:render:*; no performance.mark entries are allocated). The measure is cleared by name after emit unless the companion re-frame.performance/retain-entries? goog-define (default false) is set — when set, entries persist in the host User-Timing buffer for one-shot getEntriesByType("measure") readers. Compile-time only — not a (rf/configure! ...) knob; runtime mutation has no effect. Default false; under :advanced + default the bracket DCEs and shipped binaries carry zero User-Timing instrumentation. CLJS-only — JVM is a no-op. See 009 §Performance instrumentation and Tool-Pair §Performance API consumption v1 tooling 009
trace-buffer Fn (trace-buffer frame-id) / (trace-buffer frame-id opts) → the named frame's event-keyed ring, oldest-first (event bundles by default; {:flat true} for raw trace events). [] for a destroyed / never-registered frame and in production v1 (dev-only) tooling 009
clear-trace-buffer! Fn (clear-trace-buffer! frame-id) / (clear-trace-buffer!) → nil — empty the named frame's ring, or every frame's. A data clear: retention policy is preserved (the :trace-buffer process default and each frame's explicit :rf.trace/events-retained override both survive). The fixture-grade reset that also resets policy is re-frame.trace.tooling's concern, not a public verb. No-op for an unknown frame and in production v1 (dev-only) tooling 009
(rf/configure! {:trace-buffer {:events-retained N}}) — See §Configure keys. v1 (dev-only) — (configure key) 009
re-frame.trace.projection/group-by-event Fn (group-by-event events) → vector of event bundles {:dispatch-id :event :handler :fx :effects :subs :renders :other}, sorted by emission order. Pure data; JVM-runnable. Not on the facade — require re-frame.trace.projection directly (see 009 §Event-bundle projection). v1 (dev-only) tooling 009
re-frame.trace.projection/domino-bucket Fn (domino-bucket trace-event) → #{:event :handler :fx :effect :sub :render :other}. Classifies a raw trace event into the six-domino slot used by group-by-event. Pure data. Same namespace, not on the facade. v1 (dev-only) tooling 009

Trace-emission opt-out (per-handler metadata)

Event-handler registration accepts a :rf.trace/no-emit? true metadata flag. When set, the runtime suppresses every trace emission and event-emit record within the handler's scope — the handler runs invisibly to the trace surface, the event-emit substrate, the frame-owned :handled-events observability sink, and (transitively) the epoch buffer. Used by framework-internal bookkeeping handlers (Xray, Story, re-frame2-pair-mcp, story-mcp) that would otherwise saturate the trace stream. Per Conventions §Reserved namespaces the :rf.trace/* namespace is framework-owned.

Metadata key Where Value Default Effect
:rf.trace/no-emit? reg-event metadata map boolean false When true, suppresses all trace + event-emit emissions inside the handler's scope, and the :handled-events sink route drops the handler's record. Per 009 §Trace-emission opt-out.
:rf.trace/frame-no-emit? frame config map (make-frame / frame-root) boolean false When true, marks the frame a tool / inspector frame: the runtime suppresses every trace emission tagged with that frame, so the inspector's own reactivity does not flood the shared ring it inspects. The frame-scoped sibling of :rf.trace/no-emit?. Per 009 §Frame-level trace-emission opt-out.

Epoch history (per Tool-Pair)

Per-frame epoch snapshots, recorded once per dequeued event (at each event's run-to-completion boundary — not once per drain: a drain that processes a parent event and the :fx [[:dispatch …]] child it queued commits two records) in dev builds. Used by pair-shaped tools for time-travel and post-mortem analysis. Production builds elide entirely.

All rows below are the Tool-Pair time-travel surface — pair-shaped dev tools (Xray, the pair-MCP servers), so they live in re-frame.epoch (§Conventions, the facade rule by role) and tier tooling (the classification guidance also calls epoch-listener registration "advanced power-user"; either way the surface is back-room and opt-in — never front-porch). The state-injection member is the ONE partial-map mutator replace-frame-state! (there are no per-partition replace-app-db! / reset-app-db! / replace-runtime-db! mutators): a present partition key replaces that partition, an absent key is preserved — a db-shaped key never silently replaces the other partition.

API M/Fn Signature Status Tier Spec
re-frame.epoch/epoch-history Fn (epoch-history frame-id) → vector of epoch records. Returns [] for an unknown / destroyed frame (per Tool-Pair §Surface behaviour against destroyed frames). v1 (dev-only) tooling Tool-Pair
re-frame.epoch/current-epoch-id Fn (current-epoch-id frame-id) → the id of the epoch whose :frame-state-after the frame holds — the newest settled :ok record, or after a restore-epoch! the restored epoch until the next event settles. Read-only. A restore records no epoch, so after one this, not the ring's newest record, names the frame's state. nil when nothing has settled, the ring is disabled, the frame is unknown / destroyed, or after a {:to :before} restore. v1 (dev-only) tooling Tool-Pair
re-frame.epoch/restore-epoch! Fn (restore-epoch! frame-id epoch-id) / (restore-epoch! frame-id epoch-id {:to :before}) → structured envelope, {:ok? true :frame … :epoch-id <restored> :to …} on success. Rewinds to the record's canonical :frame-state-after, or with {:to :before} its :frame-state-before (BOTH partitions — reviving machines / routes / elision / SSR), not just the app-db projection (EP-0001); {:to :before} on a replace-frame-state! record undoes the injection, even when it is the only record retained. Returns {:ok? false :reason <op> :frame … …tags} and emits that op's structured trace on any of its seven failure modes — an unknown / destroyed frame (:rf.error/no-such-handler, kind :frame), plus the six :rf.epoch/restore-* preconditions (drain-in-progress, unknown epoch, non-ok record, schema-digest mismatch, missing handler, machine version mismatch) enumerated in the epoch trace-events table below (per Tool-Pair §Surface behaviour against destroyed frames). false in a production (elided) build. v1 (dev-only) tooling Tool-Pair
re-frame.epoch/replay-epoch! Fn (replay-epoch! frame-id epoch-id) / (replay-epoch! frame-id epoch-id opts) → structured envelope. Re-drives the named retained epoch's recorded event through the frame's own handlers in ONE call as a strict replay — the raw :trigger-event, the recorded post-generation :rf.cofx under :rf.cofx/mint-policy :strict, and the record's own :fx-overrides / :interceptor-overrides, all resolved in-process (nothing exported or copied by hand). Same frame in and out; no implicit restore; records a new ordinary epoch. {:ok? true … :epoch-id <the new epoch>} on success; {:ok? false :reason …} decided BEFORE dispatch for an unknown / destroyed frame (:rf.error/no-such-handler, kind :frame), a drain in flight (:rf.epoch/replay-during-drain), an unknown / aged-out id (:rf.epoch/replay-unknown-epoch), a halted / synthetic / incomplete record (:rf.epoch/replay-non-replayable-record + :cause), or a recorded :rf/fn-override (:rf.epoch/replay-unreplayable-fx-override + :fx-ids) — refusals ride in the envelope, no trace is emitted; a declared fact absent from the token stays the canonical :rf.error/missing-required-cofx hard error. false in a production (elided) build (per Tool-Pair §Replay). v1 (dev-only) tooling Tool-Pair
re-frame.epoch/replace-frame-state! Fn (replace-frame-state! frame-id frame-state) → structured envelope — atomically install a PARTIAL frame-state map (any subset of {:rf.db/app … :rf.db/runtime …}); a present key replaces that partition, an absent key is preserved unchanged. The ONE frame-state write surface: an app-only map ({:rf.db/app v}) replaces app-db; {:rf.db/app {}} resets it; a runtime-only map ({:rf.db/runtime v}) replaces runtime-db; a both-key map is the full-frame atomic install for tool-driven replay / fixture install. Records a synthetic epoch and returns {:ok? true :frame … :epoch-id <the synthetic record, or nil>}. A refusal returns {:ok? false :reason <op> :frame … …tags} and emits that op's trace: a map with no recognized partition key, or an unrecognized key, is :rf.error/replace-frame-state-bad-keys (checked before frame resolution; its own diagnostic rides as :cause); an unknown / destroyed frame is :rf.error/no-such-handler (kind :frame) (per Tool-Pair §Pair-tool writes). v1 (dev-only) tooling Tool-Pair
(rf/configure! {:epoch-history {:depth N}}) — See §Configure keys. v1 (dev-only) — (configure key) Tool-Pair
app-db-value Fn Partition reader for app-db (cross-ref to §Public registrar query API). Returns nil for an unknown / destroyed frame (per Tool-Pair §Surface behaviour against destroyed frames). v1 advanced 002
frame-state-value Fn Reader for the coherent frame-state projection (cross-ref to §Public registrar query API); the runtime-db-only read is (:rf.db/runtime (frame-state-value frame-id)). Returns nil for an unknown / destroyed frame (per Tool-Pair §Surface behaviour against destroyed frames). v1 tooling 002

Epoch-settled listeners are the :epoch stream of the stream-parameterized listener verb — (rf/register-listener! :epoch id callback-fn) / (rf/unregister-listener! :epoch id) — not a separate facade fn (there is no per-channel register-epoch-listener! / unregister-epoch-listener! facade pair; the epoch stream registers through the one verb exactly like :trace). The callback is a record-publication notification, not a once-per-event clock: an ordinary handled event publishes one record when it settles, and the SAME record re-publishes — carrying the same :epoch-id — when a post-settle render / sub-run / unmount back-fills into that already-settled epoch; synthetic records publish too (:rf.epoch/db-replaced per replace-frame-state! write and :halted-depth at the depth ceiling, both ring-retained when depth permits, plus the terminal :halted-destroy — an already-started event interrupted by frame destruction, delivered to listeners only and never retained). Because the listener is process-global while :epoch-id is unique only within one frame, reconcile on the pair [(:frame record) (:epoch-id record)] — cache under that key and replace on re-publication rather than counting callbacks; :outcome is record state (:ok / :halted-depth / :halted-destroy), not identity. A dequeued event rejected before it runs (no handler) publishes nothing. id may be any comparable value and re-registering the same id replaces. Process-global; a callback whose frame is destroyed simply receives no more records for it, and a tool that wants to forget that frame listens for :rf.frame/destroyed (per Tool-Pair §Surface behaviour against destroyed frames). Returns id.

Trace events emitted by epoch-history machinery:

:operation Tags
:rf.epoch/snapshotted :frame, :rf.epoch/id, :rf.trace/event-id, :outcome (the detailed cause enum :ok / :halted-depth / :halted-destroy; :halted-handler-exception is schema-reserved, not currently emitted), :summary (the consumer-facing :ok / :blocked / :error projection of that cause)
:rf.epoch/restored :frame, :rf.epoch/id
:rf.epoch/db-replaced :frame, :rf.epoch/id
:rf.epoch/restore-unknown-epoch :frame, :rf.epoch/id, :history-size
:rf.epoch/restore-schema-mismatch :frame, :rf.epoch/id, :schema-digest-recorded, :schema-digest-current, :failing-paths
:rf.epoch/restore-missing-handler :frame, :rf.epoch/id, :missing
:rf.epoch/restore-version-mismatch :frame, :rf.epoch/id, :machine-id, :version-recorded, :version-current
:rf.epoch/restore-during-drain :frame, :rf.epoch/id
:rf.epoch/restore-non-ok-record :frame, :rf.epoch/id, :outcome, :halt-reason
:rf.epoch/replace-during-drain :frame
:rf.epoch/replace-schema-mismatch :frame, :failing-paths
:rf.epoch/replace-history-disabled :frame (a replace-frame-state! precondition failure when the history ring is disabled)
:rf.error/replace-frame-state-bad-keys :frame, :reason (:no-recognized-keys / :unknown-keys), :keys (a replace-frame-state! precondition failure — no recognized partition key, or an unrecognized key; checked before frame resolution)
:rf.epoch.cb/listener-exception :frame, :cb-id, :rf.epoch/id, :message (an :epoch-stream listener callback threw)
:rf.warning/restore-quiesce-hook-exception :hook, :frame, :exception (a restore-time async-quiesce hook threw)

Size-elision wire-boundary walker

Cross-reference: see Security.md §Privacy / secret handling — re-frame.elision/elide-wire-value is named there as the single normative emission site for the :rf/redacted sentinel. Every off-box egress (trace forwarders, MCP servers, error monitors) routes through rf/project-egress, which resolves a named boundary and delegates the per-slot walk to this walker; the trust-boundary surfaces catalogued in Security.md compose against that door.

The framework primitive that walks tree-shaped values at the wire boundary and substitutes elision markers for sensitive or large slots. Reached by every tool that emits wire data (the off-box error-monitor forwarders, the Xray-MCP / re-frame2-pair-mcp / story-mcp servers per Tool-Pair.md, the on-box dev panels) through rf/project-egress, never directly. The walker is the single normative emission site for the :rf/redacted sensitive sentinel and the :rf.size/large-elided marker; per-tool reimplementation is prohibited.

Internal mechanism, not a façade door. re-frame.elision/elide-wire-value carries no re-frame.core re-export and no manifest row. It reads no :rf.egress/profile, so a caller reaching it directly would have to hand-assemble the :rf.egress/* floor that a named boundary already carries — two spellings for one boundary, and the weaker of the two silent. rf/project-egress is the single projection door on the façade. The walker is public at its re-frame.elision home for the framework's own emit-time chokepoints (classification, event / error emit, flows, reply, route-sub egress, the Fresco tool read, and the projector's own slot walk) — every one of which passes {:frame f} and never a profile, because that internal walk is always the maximal floor.

The walker's opts contract — what project-egress resolves a profile into and passes down. (re-frame.elision/elide-wire-value v opts) → v or an elision-marker substitution. opts is a closed map — {:frame <frame-id> :path [...] :query-v [...] :rf.egress/include-sensitive? <bool> :rf.egress/include-large? <bool> :rf.egress/include-digests? <bool> :rf.egress/threshold-bytes <int>} and nothing else. Any other key raises :rf.error/bad-egress-opts naming the offending key(s); in particular a :rf.egress/profile is not accepted here — a profile names a boundary and is resolved by rf/project-egress, which passes the resolved :rf.egress/* opt-set down. Nor are the unqualified include-sensitive? / include-large? spellings: :rf.egress/* is the one egress vocabulary. Closing the map cannot widen egress — in an open map an unknown key would be a silent no-op. The :frame opt is read by key presence, not truthiness: an explicit :frame nil means no governing frame and fails closed; only an ABSENT :frame key falls through to the carried scope. Defaults: both include-* flags false (maximum elision); :rf.egress/threshold-bytes falls back to (rf/configure! {:elision ...}) then 16384. Walks v against the named frame's classification policy (015 §Static app-db classification); substitutes :rf/redacted for sensitive slots and :rf.size/large-elided markers for large slots. Composition rule (normative): when both predicates match the sensitive drop wins — the size marker is suppressed because it would leak :path / :bytes / :digest. Per 009 §Size elision in traces and Spec-Schemas §:rf/elision-marker.

Declaration path. Durable app-db classification is a reg-classification beside the feature — (reg-classification id {:sensitive [paths] :large [paths]}) — which every frame's egress resolves through its own view into its classification policy, claimed {:source :classification}. A reg-app-schema {:large? true} / {:sensitive? true} slot prop is not a route into durable classification: schemas describe shape and validation, not durable app-db egress policy (an app-db path's schema-validation-failure trace follows its classification, so the path is declared once; a transient value's schema and the machine [:schemas :data] and resource :params-schema per-slot props redact only their own validator's failure trace, and a resource :data-schema is a statically reflected shape fact with no runtime validation consumer). Durable machine and resource :data classification is likewise schema-independent: it rides the subsystem's own projection-relative :sensitive / :large declaration on the reg-machine / reg-resource spec, applied at egress to each instance present, per 015 §Subsystem projection-relative classification. The policy's declaration readers (re-frame.elision/declarations / sensitive-declarations) and the derived-tree value-match arms are reachable through the re-frame.elision home namespace; the user-facing façade exposes only project-egress. Per 015 §Static app-db classification and implementation/core/src/re_frame/elision.cljc.

Record-level egress projection (EP-0015 / Spec 015)

Cross-reference: 015 §Projection is the normative home; project-egress is the public, record-level boundary primitive layered over the internal re-frame.elision/elide-wire-value walker. The six-member :rf.egress/* profile enum is the named-boundary vocabulary; the boolean :rf.egress/* flags are the advanced override layer beneath it (EP-0015 §10/§11).

rf/project-egress is the required projection step before any off-box sink. It dispatches on a record's :kind — the three :rf.observe/* kinds plus :rf/epoch-record — to a private per-kind projector — only project-egress is public (EP-0015 issue 2: the name names the boundary, not a record kind) — and delegates every tree-shaped slot to the internal re-frame.elision/elide-wire-value walker. A profile resolves to a :rf.egress/* opt-set; an explicit :rf.egress/* boolean composes on top (the override wins).

API M/Fn Signature Status Tier Spec
project-egress Fn (project-egress record-or-value) / (project-egress record-or-value opts) → a value safe to ship under the resolved profile. opts conforms to :rf/project-egress-opts, which is closed: {:rf.egress/profile <closed six-member enum> :frame <frame-id> :path [...] :query-v [...] :rf.egress/include-sensitive? <bool> :rf.egress/include-large? <bool> :rf.egress/include-digests? <bool> :rf.egress/threshold-bytes <int> :rf.egress/include-fx-args? <bool> :rf.egress/include-runtime-db? <bool> :rf.egress/include-event-args? <bool>} — the walker's closed key set, the one key this layer owns, and the three trusted-local axes that govern keyspaces only an :rf/epoch-record has (effect :args, the :rf.db/runtime frame-state partition, trigger / trace event args; all default false, and each is stripped before the walker, which knows nothing of them). Those three belong to this door because it owns record-level projection (there is no rf/projected-record), and they share the :rf.egress/* namespace with the rest of the egress opts. An unrecognised key raises :rf.error/bad-egress-opts naming it, on every record kind and on the kindless value path alike. Dispatches on a record's :kind (:rf.observe/handled-event / :rf.observe/error / :rf.observe/derived-tree / :rf/epoch-record) to a private per-kind projector, falling back to walking a kindless input as a tree-shaped value (the direct-read path); delegates tree-shaped slots to the internal re-frame.elision/elide-wire-value walker. The three :rf.observe/* projectors are private to re-frame.projection; :rf/epoch-record's is late-bound (:epoch/project-record) because re-frame.epoch's own require graph reaches re-frame.projection — a recognised :rf/epoch-record whose projector is absent (a build that elides epoch) raises :rf.error/epoch-elided naming the kind rather than bare-walking the record (which would start at :path [] and ship declared-sensitive :db-* slots raw). Frame ownership resolves in three steps, each by key presence, not truthiness: (1) an explicit :frame key in opts wins, nil included; (2) else a recognised record's own top-level :frame slot, nil included — every recognised kind is frame-bearing, carrying the frame whose classification governs its tree-shaped slots; (3) else the carried scope. A record is recognised by its :kind, never by a loose shape test, so a bare value that happens to carry a :frame key is a VALUE and seeds nothing. An explicit nil at step 1 or 2 therefore fails closed rather than borrowing the ambient frame. Profile resolves to a :rf.egress/* opt-set; explicit :rf.egress/* booleans compose on top (override wins). Unknown profile raises :rf.error/unknown-egress-profile (closed enum); an unrecognised opts key raises :rf.error/bad-egress-opts. Fail-closed when no frame is known from any of the three steps — an explicit nil included (no :rf/default synthesis). Per 015 §project-egress. v1 tooling 015

Frame-owned observability sinks (EP-0015 §9 / Spec 015)

Cross-reference: 015 §Frame-owned observability sink policy is the normative home. The normal production observability story (Datadog / Sentry / Honeycomb): an app declares a sink under a frame's :observability config ({:handled-events [{:sink <id> :rf.egress/profile …}] :errors [...]}) and registers the concrete sink fn against that <id> with rf/register-observability-sink!. A sink entry is a closed map — :sink plus the optional :rf.egress/profile, nothing else; any other key fails loud at make-frame with the key named, so no slot is accepted and then dropped. Vendor configuration is the registered fn's own (it closes over it); per-frame branching reads the record's :frame. The runtime routes one :rf.observe/handled-event record per processed event and one :rf.observe/error record per :rf.error/* site through rf/project-egress — under the owning frame's classification and the entry's egress profile (default :rf.egress/off-box-observability) — to the declared sink. Sinks consume already-projected records only (no sink-local redaction). Always-on (survives :advanced + goog.DEBUG=false). This is the only production observation door: the same entry grammar declared once per process with (rf/configure! {:observability …}) covers a cross-frame seat and every record whose frame does not resolve. The lower-level corpus-wide event-emit / error-emit registries are implementation tier only — they carry no public registration verb. Routing is fail-closed: the runtime never synthesizes :rf/default and never borrows another frame's policy, and a throwing sink is isolated from its siblings.

The same policy can be declared once per process with (rf/configure! {:observability …}) — §Configure keys — and precedence is per stream: a frame that declares a stream uses its own entries for it, a frame that omits the stream inherits the process default's, and {:errors []} on a frame is that frame's opt-out (declaration is read by key presence, not truthiness). Exactly ONE source is consulted per record per stream, so a sink id named by both is invoked once. Inheritance moves the sink list, never the redaction authority: an inheriting frame's records are still projected under its own classification. A record with no frame authority — one of the frameless three, or one whose :frame no longer resolves — reaches the process default alone, projected with the governing frame explicitly nil (tree slots :rf/redacted, summary ids intact, a stale :frame id kept as a diagnostic and never re-resolved). With neither a frame policy nor a process default, nothing routes. 015 §The process default is normative.

API M/Fn Signature Status Tier Spec
register-observability-sink! Fn (register-observability-sink! sink-id f) — register the concrete observability sink fn f under the keyword sink-id the frame's :observability {:sink <sink-id> …} entry names. f receives one already-projected :rf.observe/handled-event / :rf.observe/error record (projected under the owning frame's classification + the entry's egress profile); its return value is ignored. Re-registering the same id replaces. Returns sink-id. The framework ships no Datadog / Sentry client (EP-0015 Non-Goals); the sink fn is an app / integration-library concern. Always-on. Per 015 §Frame-owned observability sink policy. v1 tooling 015
unregister-observability-sink! Fn (unregister-observability-sink! sink-id) → nil v1 tooling 015

DOM source-coord annotations (mandatory)

Per Spec 006 §Source-coord annotation and Tool-Pair §Source-mapping, every adapter whose host has a DOM-attribute concept MUST inject data-rf2-source-coord="<ns>:<sym>:<line>:<col>" on the rendered root DOM element of each registered view. Format and exemptions (Fragments, non-DOM roots) are documented in Spec 006 §Source-coord annotation. Annotation is gated on interop/debug-enabled? (the CLJS mirror of goog.DEBUG); production :advanced builds elide the attribute via dead-code elimination — there is no DOM-bytes cost in shipped bundles. On the JVM the same annotation is stamped at the reg-view* registration boundary — a debug-gated wrapper on the stored :handler-fn (re-frame.views.jvm-source-coord-annotation), the server-side twin of the CLJS substrate wrappers — not by the SSR emitter, which carries no annotation logic and merely serialises the hiccup the registration boundary already annotated; see Spec 011 §Source-coord annotation under SSR.

Error contract

Errors are emitted as structured trace events with :op-type :error (or :warning / :info / :fx / :flow / :frame) and a per-category :operation keyword. The complete normative catalogue — every :rf.error/*, :rf.warning/*, :rf.fx/*, :rf.cofx/*, :rf.ssr/*, :rf.epoch/*, :rf.flow/*, :rf.http/*, :rf.http.interceptor/*, :rf.frame/*, and :rf.route.nav-token/* event the runtime emits — lives at 009 §Error event catalogue (single source of truth for category names, :op-type discriminator, trigger conditions, default :recovery, and :tags payload keys). Per-category Malli :tags schemas are canonicalised at Spec-Schemas §Per-category :tags schemas — one schema per catalogue row.

Categories consumers should be aware of include :rf.ssr/version-mismatch, :rf.ssr/schema-digest-mismatch, :rf.ssr/compatibility-check-skipped (the SSR hydration compatibility-check trio), and :rf.cofx/skipped-on-platform (the platform-gating mirror of :rf.fx/skipped-on-platform). The catalogue at 009 is the single source of truth — do not duplicate the table here.

Per-Spec emit-sites: 002-Frames, 005-StateMachines, 006-ReactiveSubstrate, 010-Schemas, 011-SSR, 012-Routing, 013-Flows, 014-HTTPRequests, Tool-Pair. Each catalogue row's "Per [N]" cross-link names the owning Spec section.

Privacy (Spec 009 §Privacy / sensitive data in traces)

Cross-reference: see Security.md §Privacy / secret handling for the framework-wide pattern-level posture; the trust-boundary catalogue lives in Security.md. The cross-artefact inventory + composition order (every privacy surface in re-frame.core, re-frame.http, re-frame.schemas, re-frame.epoch, tools/mcp-base, with the data-flow from handler exit to off-box wire) lives in Privacy.md. The public classification model is reg-classification for durable app-db (see 015 §Static app-db classification) and registration-owned :sensitive payload classification on reg-event / reg-sub / reg-flow, projected at trust boundaries by project-egress and the :rf.egress/* profiles. There is no imperative add-marks / set-marks path-marks API, no re-frame.marks namespace, no frame :sensitive {:app-db …} annotation, and no positional redact-interceptor; the marks projection substrate lives in the marks-free re-frame.classification / re-frame.elision engine.

Per Spec 009 §Privacy the runtime stamps :sensitive? true at the top level of every trace event emitted inside the scope of a handler whose declared path overlap classifies sensitivity. (There is no handler-meta :sensitive? annotation; sensitive-data marking is path-based per the data-classification mechanism in Spec 015.) Framework-published trace consumers (Sentry/Honeybadger forwarders, re-frame2-pair server, Xray, Story, story-mcp, re-frame2-pair-mcp) MUST default-drop the stamped events at their egress boundary.

API M/Fn Signature Status Tier Spec
sensitive? Fn (sensitive? trace-event) → boolean. True iff trace-event is a map carrying a truthy :sensitive? stamp at the top level (not under :tags). The framework-published predicate every consumer composes against — Xray, Story and the pair-MCP server call it directly rather than reimplementing the same five-token check. Fail-closed: true is sensitive, false / nil / absent are not, and any OTHER truthy value (a string "true", a keyword :yes, a number) counts as SENSITIVE — the schema types the stamp as a boolean, so a non-boolean is a contract violation, and a (true? …) reading would forward the event in exactly the case where the producer has already proved unreliable. Matches the posture re-frame.mcp-base.sensitive/sensitive-stamp? applies on the MCP wire. v1 tooling 009

Spec-internal schemas

Per Spec-Schemas.md, the spec's own runtime shapes are described as Malli schemas registered at runtime. These are the conformance contract an implementation validates against.

Schema Describes Spec
:rf/dispatch-envelope Internal envelope wrapping every dispatch 002
:rf/dispatch-opts The user-facing opts map for dispatch / dispatch-sync / subscribe 002
:rf/registration-metadata Common metadata-map shape across reg-* 001 / 010
:rf/effect-map Return value of reg-event handlers — closed, three keys: #{:db :rf.db/runtime :fx} (:rf.db/runtime reserved by convention for framework authority; app handlers use only :db / :fx day to day) 002
:rf/trace-event Universal trace event shape 009
:rf/error-event Refinement of :rf/trace-event for :op-type :error / :warning (unified error/warning envelope) 009
:rf/handler-body-dsl Conformance corpus handler-body DSL (host-agnostic event/sub bodies; small-DSL grammar) 008 / Spec-Schemas
:rf/transition-table State-machine transition table grammar 005
:rf/machine-snapshot Runtime snapshot of a machine instance 005
:rf/hydration-payload Wire format for SSR hydration 011
:rf/response HTTP-response accumulator owned by the request frame during SSR 011
:rf.server/cookie Structured-cookie shape for :rf.server/set-cookie / :rf.server/delete-cookie 011
:rf/head-model SSR head/meta data model (title, meta, link, json-ld, html/body attrs) 011
:rf/public-error Sanitised, client-safe projection of an internal error trace event 011
:rf.fx.server/set-status-args / :rf.fx.server/set-header-args / :rf.fx.server/append-header-args / :rf.fx.server/set-cookie-args / :rf.fx.server/delete-cookie-args / :rf.fx.server/redirect-args Args of standard :rf.server/* SSR fx 011
:rf/frame-meta Returned by (frame-meta frame-id) 002
:rf/variant Story-variant artefact contract (post-v1 lib) — variants are data, no fn-valued slots 007
:rf/epoch-record Per-frame epoch snapshot record (Tool-Pair) Tool-Pair
:rf.fx/dispatch-args Args of standard :dispatch fx (and :raise, same shape) 002 / 005
:rf.fx/dispatch-later-args Args of standard :dispatch-later fx 002
:rf.fx/http-args Args of :http fx (user-owned recommendation) Pattern-RemoteData
:rf.fx.nav/push-url-args Args of :rf.nav/push-url fx 012
:rf.fx.nav/replace-url-args Args of :rf.nav/replace-url fx 012
:rf.fx.nav/scroll-args Args of :rf.nav/scroll fx 012
:rf.fx.nav/capture-scroll-args Args of :rf.nav/capture-scroll fx 012
:rf.fx/with-nav-token-args Args of :rf.route/with-nav-token fx wrapper 012
:rf.fx/spawn-args Args of :rf.machine/spawn fx (the canonical actor-lifecycle fx-id; emitted from any event handler's :fx) 005
:rf.fx/destroy-machine-args Args of :rf.machine/destroy fx (the canonical actor-destroy fx-id; a bare actor-id keyword or a {:rf/parent-id :rf/invoke-id} map) 005
:rf.fx/spawn-all-init-args Args of :rf.machine/spawn-all-init fx (machine-internal: seeds a :spawn-all join) 005
:rf.fx/after-schedule-args Args of :rf.machine/after-schedule fx (machine-internal: arms one :after timer) 005
:rf.fx/after-cancel-args Args of :rf.machine/after-cancel fx (machine-internal: releases an exited state's :after timers) 005
:rf.fx/managed-args Args of :rf.http/managed fx (request envelope, decode, accept, retry, timeout-ms, on-success/on-failure, request-id, abort-signal) 014
:rf.fx/managed-abort-args Args of :rf.http/managed-abort fx (request-id) 014
:rf.http/reply Canonical reply envelope {:status :ok :value v …} / {:status :error :error {:kind <:rf.http/*> …} …} / {:status :cancelled :error {:kind :rf.http/aborted …} …} appended to the reply target (:reply-to unified, or :on-success/:on-failure split sugar) 014
:rf/route-rank Structural-rank tuple for route-precedence sorting 012
:rf/pending-navigation Pending-navigation slot when :can-leave guard rejects 012
:rf/elision-marker Wire shape re-frame.elision/elide-wire-value substitutes for an elided large value (:rf.size/large-elided) 009
:rf/project-egress-opts The opts map rf/project-egress accepts (:rf.egress/profile + advanced :rf.egress/* overrides) 015

Schemas are open by default (consumers tolerate unknown keys; producers grow shapes additively); :closed true is opt-in at boundary-validation sites and on the effect-map.


Testing

The testing surface lives across three namespaces. re-frame.core carries the production primitives that double as testing entry points (make-frame, with-frame, with-new-frame, dispatch-sync, app-db-value, and compute-sub, rowed in §Dispatch and subscribe); the static sub-graph query sub-topology is not among them — it is subscription tooling, reached through re-frame.subs.tooling (see §Public registrar query API). Pure machine simulation is the one exception: machine-transition is owned by re-frame.machines and is not re-exported from re-frame.core (per the front-porch boundary above). re-frame.test-support ships the test-only fixture machinery and test-flavoured helpers. re-frame.test-helpers ships the view-assertion helpers (hiccup-walk + testid authoring). re-frame.test-support does not re-export from re-frame.core — a test file requires both [re-frame.core :as rf] and [re-frame.test-support :as ts], and additionally [re-frame.test-helpers :as th] for view-assertion tests. See 008-Testing.md for fixtures, framework adapters, and re-frame-test compatibility.

API M/Fn Signature Status Tier Spec Notes
assert-path-equals Fn (assert-path-equals path expected-val) / (assert-path-equals path expected-val opts) v1 testing 008 Path-form sync assertion. Mismatch fires clojure.test/is-style failure via do-report. Lives in re-frame.test-support. Mirrors the :rf.assert/path-equals event used inside a Story :script block — same name root so the fn-side and event-side are navigable without a translation table. The wider sibling event family (:rf.assert/sub-equals, :rf.assert/state-is, :rf.assert/dispatched?, :rf.assert/no-warnings, :rf.assert/effect-emitted, :rf.assert/path-matches) lives in 007 §Play functions; runner and reporting channel differ. Choose by test surface: assert-path-equals from a deftest body, :rf.assert/path-equals from a story variant's :script vector.
flush-views! Fn (flush-views!) / (flush-views! f) v1 testing 008 Runs f, then commits the views it re-rendered, synchronously, through the installed adapter's flush-render! — inside React's act() on CLJS — and returns nil. The one test flush for every React view adapter: a test names no adapter to flush. Synchronous only; queued work is waited for with poll-until. Where nothing renders live (plain-atom, SSR, the JVM) it runs f and commits nothing. Fresco mounted tests settle with re-frame.fresco.test.mounted/settle! instead, because act() diverts work away from the browser's scheduler. Lives in re-frame.test-support. Also 006 §flush-render!: the adapter contract it drives.
poll-until Fn (poll-until pred) / (poll-until pred opts) v1 testing 008 Bounded-deadline poll. JVM: synchronous — returns the truthy value, throws ex-info carrying :rf.error/id :rf.error/poll-until-timeout (the canonical discriminator, per Spec 009) on timeout. CLJS: returns a js/Promise resolving with the truthy value or rejecting on timeout. Opts: :timeout-ms (default 2000), :interval-ms (default 5), :label. Lives in re-frame.test-support.
with-fx-overrides M (with-fx-overrides {fx-id -> override, …} body+) v1 testing 002 Lexical-scope :fx-overrides binding. Every dispatch / dispatch-sync inside the body merges the supplied map into its envelope's :fx-overrides. Precedence: per-call opt > lexical with-fx-overrides > per-frame :fx-overrides. Composes with with-frame. Lives in re-frame.test-support. Also 008 §Built-in test-runner namespace.
make-reset-runtime-fixture Fn per docstring v1 testing 008 Fixture machinery: builds the :each fixture (registrar snapshot/restore + per-process reset). Lives in re-frame.test-support.
snapshot-registrar Fn (snapshot-registrar) v1 testing 008 Captures the registration table — every registration any frame resolves; with restore-registrar! it composes hand-rolled fixtures. Lives in re-frame.test-support.
restore-registrar! Fn (restore-registrar! snapshot) v1 testing 008 Resets the registration table to a snapshot-registrar capture, dropping registrations made since — a frame made without :images no longer runs them — and restoring those removed since. Lives in re-frame.test-support.
with-trace-recorder! M (with-trace-recorder! [recs-sym opts?] body+) v1 testing 008 Bracket body with a fresh trace-tooling listener that accumulates matching trace events into an atom bound to recs-sym; unregisters in a finally. Opts: :pred, :shape (:flat / :by-op), :key. Lives in re-frame.test-support.
with-emit-recorder! M (with-emit-recorder! [recs-sym opts?] body+) v1 testing 008 The always-on sibling of the row above. Brackets body with a fresh listener on one of the two IMPLEMENTATION-tier always-on registries — re-frame.error-emit (:stream :errors, the default) or re-frame.event-emit (:stream :events) — accumulating records into an atom bound to recs-sym; unregisters in a finally. Opts: :stream, :pred, :key. Those registries carry no public registration verb (register-listener! has no :events / :errors stream); a test is one of the two consumers they serve, and an application observes production records through an :observability sink instead. Lives in re-frame.test-support.

Testing — view-assertion helpers

re-frame.test-helpers ships the hiccup-walk view-assertion surface — call the view-fn directly, walk the returned hiccup, assert on content or invoke a handler. JVM-runnable; no JSDOM, no React, no act(). Pairs with render-to-string (the HTML-string view-test path per Spec 011): hiccup-walk for structure / handler assertions, render-to-string for HTML-markup assertions. Per 008-Testing §View-assertion helpers.

API M/Fn Signature Status Tier Spec Notes
expand-tree Fn (expand-tree tree) → tree v1 testing 008 Recursively expand fn-components and Form-3 class components inside a hiccup tree. After expansion every vector's first element is a keyword tag or a non-component value. Lives in re-frame.test-helpers.
attrs Fn (attrs node) → map? v1 testing 008 Return the attrs map of a hiccup node, or nil. Lives in re-frame.test-helpers.
children Fn (children node) → vector v1 testing 008 Return the child elements — everything after the tag (and optional attrs map). Lives in re-frame.test-helpers.
text-content Fn (text-content node) → string v1 testing 008 Recursively collect string leaves under node and join. Numbers coerce to strings; nils are skipped. Lives in re-frame.test-helpers.
extract-handler Fn (extract-handler node event-key) → fn? v1 testing 008 Return the value of event-key from node's attrs map, or nil. Lives in re-frame.test-helpers.
find-by-attr Fn (find-by-attr tree attr val) → node? v1 testing 008 First hiccup node whose attrs map carries attr == val, or nil. Generic over the attribute keyword (:data-testid, :data-test, :id, custom). Lives in re-frame.test-helpers.
find-all-by-attr Fn (find-all-by-attr tree attr val) → vector v1 testing 008 Every matching node, in depth-first order. Lives in re-frame.test-helpers.
find-by-attr-prefix Fn (find-by-attr-prefix tree attr prefix) → vector v1 testing 008 Every node whose attr value (a string) STARTS with prefix. Non-string attr values do not match. Lives in re-frame.test-helpers.
find-by-testid Fn (find-by-testid tree test-id) → node? v1 testing 008 Convenience over find-by-attr keyed on :data-testid. Lives in re-frame.test-helpers.
find-all-by-testid Fn (find-all-by-testid tree test-id) → vector v1 testing 008 Convenience over find-all-by-attr keyed on :data-testid. Lives in re-frame.test-helpers.
find-by-testid-prefix Fn (find-by-testid-prefix tree prefix) → vector v1 testing 008 Convenience over find-by-attr-prefix keyed on :data-testid. Lives in re-frame.test-helpers.
invoke-handler Fn (invoke-handler node event-key & args) → any v1 testing 008 Find the handler under event-key on node and call it with args. Returns the handler's return value. THROWS when node is not a hiccup vector, the node has no attrs map, or no handler is registered — the throwing failure mode is deliberate (a missing handler is almost always a test bug). Lives in re-frame.test-helpers.
testid Fn (testid id) / (testid id extra) → map v1 testing 008 Build an attrs map carrying :data-testid id. The 2-arity merges extra into the map; :data-testid always wins on collision. Authoring helper at the view call site. Lives in re-frame.test-helpers.

The single-frame view test is a composition, not a bespoke fixture: re-frame.test-support/make-reset-runtime-fixture (an :adapter + optional :init-fn) seats the ambient :rf/default frame and runs the install thunk; the walkers above call the root view fn directly and assert on the returned hiccup; re-frame.test-support/poll-until covers the async case. Per 008-Testing §Single-frame view test.


Standard interceptors

Under EP-0022 the public interceptor-authoring surface is reg-interceptor (§Registration), and event/frame :interceptors chains carry interceptor references (a bare keyword id or [id arg]), never inline values. The v2 framework-standard interceptor surface is exactly one interceptor — :rf.interceptor/path — referenced as [:rf.interceptor/path <path-vector>]. There is no public rf/path value constructor and no standard unwrap (EP-0022 §No standard unwrap — handler destructuring, or a project-registered interceptor for intentional chain-wide event reshaping, replaces it). The standard set is path-only because path is coupled to app-db commit no-op semantics and justifies the :factory mechanism; a trivial :before-only helper is not worth a standard slot. Five v1 interceptors are not in v2: debug, trim-v, on-changes, enrich, after (per MIGRATION §M-21).

inject-cofx / inject-cofx* — the v1 coeffect-injection interceptors — are not in v2 (no alias). They are not on the public re-frame.core facade and carry no API-manifest row (a removed surface is not part of the canonical public API): there is no public inject-cofx var to call. Coeffect delivery is not a chain member: a handler declares :rf.cofx/requires on its registration metadata and the value-returning supplier's result arrives flat in the coeffects map (§Registration — reg-cofx, 001 §:rf.cofx/requires). The removal alarm lives internally: a stale call raises the always-on hard error :rf.error/inject-cofx-removed naming :rf.cofx/requires.

Surface Shape Tier Purpose
[:rf.interceptor/path <path-vector>] interceptor reference (the one standard interceptor; the canonical :factory consumer) front-porch Focus a handler on an app-db sub-slice: :before stages the focused slice as :db, :after widens the returned slice back into full app-db. Preserves the frame-commit identical? no-op — an unchanged focused slice widens back to the original app-db object, not an assoc-in allocation (002 §Standard :rf.interceptor/path). A non-vector/malformed path arg is :rf.error/path-interceptor-bad-path.
reg-interceptor M (registrar) front-porch The public application-authoring form for any non-standard interceptor — analytics, logging, validation, ad-hoc context manipulation. The resulting interceptor is named, addressable, queryable, and referenced by id from chains. Rowed in §Registration.

The v1 public interceptor-authoring helpers and their v2 replacements:

v1 API v2 replacement
path (public value constructor) the standard reference [:rf.interceptor/path <path-vector>] (EP-0022)
unwrap handler destructuring (the M-19 canonical map-payload form), or a project-registered interceptor for intentional chain-wide event reshaping (EP-0022 — no standard unwrap)
->interceptor (public authoring macro) reg-interceptor (the lowering constructor is re-frame.interceptor/->interceptor*, framework-internal)
debug Trace surface (009) + 10x / re-frame-pair
trim-v Canonical map-payload call shape (M-19)
on-changes Flows (Spec 013)
enrich Flows (derived state) / :schema (validation) / a project-registered interceptor (escape hatch)
after Registered fx (:fx [[:my-fx ...]]) for side-effects; a project-registered interceptor for context-shaped work

reg-flow / (clear :flow id) (Spec 013)

reg-flow is rowed canonically in §Registration. Per the canonical 3-slot grammar it is (reg-flow flow-id metadata derive-fn): flow-id first, the pure :derive fn last, and metadata carrying :inputs / :output-path (both REQUIRED) plus optional :doc / EP-0025 classification keys. It registers at the top level, under the registrar's :flow kind, and names no frame; a frame runs the flows its :flows config names, checked at construction for unregistered ids, output overlaps and cycles over that frame's set (per Spec 013 §Activation):

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

[rf/frame-root {:id :rf/default :flows [:cart/total]} [cart-view]]

The inverse is the kind-keyed (rf/clear :flow id), rowed canonically in §Clearing registrations: it removes the registration, and each frame whose :flows names the id vacates the flow's output path on its next event (per Spec 013 §Re-registration). There is no runtime fx form: which flows a frame runs changes only by re-making the frame with a new :flows, and a feature gate is app-db data the flow reads as an input.

Frame-destroy teardown. destroy-frame! drops the destroyed frame's flow evaluation memo — every memo row it holds — per Spec 013 §Frame-destroy teardown. Definitions live in the registrar, so teardown touches none, and sibling frames keep their own memos.

Flow-eval failures in production. A throw inside a flow's :derive fn surfaces as :rf.error/flow-eval-exception on the always-on error-emit substrate, which survives CLJS :advanced + goog.DEBUG=false and routes the record to the owning frame's :observability :errors sinks, or the process default's (§Error-emit). The error is NOT trace-only. Per Spec 013 §Failure semantics rule 4 and 009 §Production builds.


Interceptor / context plumbing

The interceptor context accessors get-coeffect / assoc-coeffect / get-effect / assoc-effect are not on the public re-frame.core façade and carry no API-manifest row (a removed surface is not part of the canonical public API). They have no public audience: the intended interceptor model is to author with reg-interceptor and let the :before / :after fns receive and return the context map directly: read coeffects with (get-in ctx [:coeffects k]) and write effects with (assoc-in ctx [:effects k] v). The underlying re-frame.interceptor/{get,assoc}-{coeffect,effect} fns live in their owning namespace as framework-internal context helpers; they are not a public surface. See §Removed / not shipped.


Lifecycle / utility

API M/Fn Signature Status Tier Spec
init! Fn (init! adapter-map) — boot; idempotent for the seated adapter (same canonical :rf.adapter/* :kind, or the identical custom map), so a ^:dev/after-load re-call keeps the seated generation and republishes the reloaded map's capabilities over it. A different adapter raises :rf.error/adapter-already-installed and leaves the seated adapter untouched — destroy-adapter! first to swap. Required arg: the adapter spec map. Each adapter ns exports an adapter Var; consumers require the ns and pass the Var, e.g. (rf/init! rf.adapter.reagent/adapter). Calling (init!) with no args raises a language-level ArityException at compile/load time (there is no no-arg arity, so the missing-adapter mistake surfaces before runtime). Calling (init! nil) or (init! :reagent) raises :rf.error/no-adapter-specified at runtime. Per 006 §Adapter selection at boot. Installs adapters and runtime capabilities only — it creates no frame (EP-0002: no auto :rf/default); the app mounts its frame at the root with frame-root (ENSURE) — or constructs one programmatically with make-frame v1 front-porch 006
destroy-adapter! Fn (destroy-adapter!) — tear down the exact installed adapter generation. This is a one-way terminal boundary: disposes every live frame's sub-cache through the departing map's :dispose!, calls the adapter spec's :dispose-adapter! fn (if present), attempts all owned cleanup, clears the departing map's published capabilities, finally clears only the generation it claimed, and leaves adapter-disposed? true even when the primary cleanup failure is rethrown. Later cleanup failures remain secondary diagnostic evidence. A fresh adapter may install afterward; stale finalization never clears a replacement. Per Conventions §Tear-down verb axis — destroy- cluster (lifecycle boundary; symmetric with install-adapter! and with destroy-frame!). The adapter-spec map key :dispose-adapter! is an internal contract slot adapters implement, distinct from this verb. v1 advanced 006
current-adapter Fn (current-adapter) → the installed adapter SPEC MAP — the exact value passed to (rf/init! …) — or nil when no adapter is installed. Carries the adapter contract fns (:make-state-container, :replace-container!, :make-derived-value, …), the adapter's capability keys, and a :kind discriminator. ONE read, map-shaped (there is no keyword-returning spelling and no current-adapter-spec twin; a keyword form would be the :kind of this same map). Branch code reads the KEY: (:kind (current-adapter)) → :rf.adapter/reagent / :rf.adapter/uix / :rf.adapter/fresco / :rf.adapter/plain-atom / :rf.adapter/ssr, or nil for a custom map that picked no canonical kind — nothing is synthesised for it (:rf.adapter/reagent-slim / :rf.adapter/freehand / :rf.adapter/ui are reserved and never recycled, and nothing produces them). A PRESENCE check inspects the MAP, never :kind. Per 006 §Adapter introspection. v1 advanced 006
configure! Fn (configure! {key opts, …}) — runtime config from a single nested map; non-map arg fails loud, missing top-level key leaves that subsystem untouched. An unknown bare or rf-namespaced top-level key applies nothing and emits :rf.warning/unknown-configure-key in dev builds (dev-gated, DCE'd in production, :recovery :ignored — observational, never a refusal); an unknown user-namespaced key (e.g. :myapp/thing) passes through silently. :epoch-history applies nothing in a build that elides epoch (interop/debug-enabled? false), silently, while the other keys still apply. Key vocabulary in §Configure keys. One of three orthogonal configuration surfaces per Conventions §Configuration surfaces (configure! for process-level data knobs; set-! / install-! for adapter-pluggable hooks; per-frame metadata for frame-scoped overrides). v1 front-porch Conventions
current-config Fn (current-config) → the process-level config values currently in effect, in configure!'s own nested shape — the read twin of configure!, per Conventions §configure! vs current-config. PROCESS values only: no per-frame effective values, no transactional snapshot, and no promise of wire-serialisability. A subsystem key is ABSENT — not nil, not a fabricated default — when its own producer is unavailable, and the two optional keys are independent: :epoch-history is missing from a build that elides epoch (interop/debug-enabled? false), :trace-buffer is missing from a production bundle that DCEs the dev-only trace-tooling sibling, and neither absence implies the other; (get-in (current-config) [:epoch-history :depth]) reads nil when epoch is elided, not when trace tooling is. The user-namespaced pass-through carve-out is write-only: configure! accepts :myapp/thing in silence, and current-config does not reflect it back — the vocabulary is closed, so only the keys the runtime READS have live values to report. Key vocabulary in §Configure keys. v1 advanced Conventions

Feature inspection

re-frame2's optional capabilities ship as separate Maven artefacts (day8/re-frame2-<feature>) whose implementation namespaces core reaches through the late-bind hook registry at call time (per Conventions §Facade re-export, artefact require). The upside is bundle-isolation — an app that omits a feature does not carry its code. The downside this front-porch closes: the late-binding is otherwise invisible, so a developer who forgets to :require the impl artefact calls a re-exported fn that exists and is met with an opaque artefact-missing error. One surface — features — makes the optional-feature inventory self-explaining; the boolean is a lookup into the map it returns.

The known optional features are :schemas, :machines, :routing, :flows, :http, :ssr, :epoch, :resources (the closed per-feature split set per Conventions §Artefact tiers).

features ships to production. It is a runtime query, not dev-time instrumentation, so — unlike the trace / epoch surfaces — it is NOT gated on interop/debug-enabled? and does NOT elide under :advanced + goog.DEBUG=false. A production caller may legitimately read (get-in (rf/features) [:routing :loaded?]) before taking a routing-dependent path. The feature→coordinate mapping is static data in the always-loaded re-frame.core facade (a plain table of {:feature {:maven … :require … :spec …}} strings), never a live :require reaching into the optional impl namespaces — a live reach-in would create a hard facade→optionals reference that pulls every optional artefact into every production bundle, breaking bundle-isolation. Presence is detected by a pure keyword lookup in the always-loaded late-bind hooks atom against a representative key the impl publishes at ns-load — no reach into the optional namespace.

API M/Fn Signature Status Tier Spec
features Fn (features) → map of every optional feature keyword to its inspection entry: the static coordinate data (:maven / :require / :spec) merged with the live :loaded? boolean. E.g. {:routing {:maven "day8/re-frame2-routing" :require "re-frame.routing" :spec "Spec 012 (Routing)" :loaded? true} …}. Ships to production (NOT elided). v1 advanced Conventions

Reading the boolean, and the boot-time guard. features is the ONE door — data before magic. The per-feature boolean is a lookup into its map, and an unknown feature keyword has no entry, so the lookup reads nil, not false, for it.

(get-in (rf/features) [:routing :loaded?])   ;=> true

;; Want boot-time failure rather than first-call failure? Write the guard.
;; One line, no framework verb, and NOT an `assert` (asserts are elidable):
(when-not (get-in (rf/features) [:routing :loaded?])
  (throw (ex-info "re-frame.routing is not on the classpath"
                  (get (rf/features) :routing))))

That guard carries the copy-pasteable coordinate data, because the inventory entry is the throw's data.

Artefact-missing errors carry the require. This front-porch is paired with a hard rule: every artefact-missing error in the framework — including the late-bind facade throws (:rf.error/<feature>-artefact-missing, raised via re-frame.late-bind/require-fn! from the re-frame.core-<feature> wrappers) — carries the exact copy-pasteable Maven coordinate and the namespace to require at app boot in its :reason slot. The named pattern is documented once at Conventions §Facade re-export, artefact require.


Configure keys

Runtime configuration is uniformly via (rf/configure! {<key> <opts>, …}) — a single nested map keyed by the top-level keys enumerated here. The argument MUST be a map (a non-map argument fails loudly); a missing top-level key leaves that subsystem untouched; a present key delegates to that subsystem's configurator in table order (:epoch-history, :trace-buffer, :elision, :observability) preserving its slot-merge semantics; an unknown top-level key applies nothing (closed-and-additive). Keys are plural-noun-shaped; opts are an open map of per-key settings.

An unknown top-level key applies nothing, but it is not necessarily silent. The vocabulary above is CLOSED and its keys are BARE, so an unrecognised bare key reads as a typo of a real key rather than as an extension point — the shape Conventions §No silent swallow says MUST signal. A bare (:epoch-histroy) or framework-namespaced (:rf.foo/bar) unknown key therefore emits :rf.warning/unknown-configure-key in dev builds, naming every offending key and the full known set. A user-namespaced key (:myapp/thing) passes in silence — that is the extension-key carve-out the same section reserves. The warning is dev-gated (rf.interop/debug-enabled?, so the whole surface DCEs under :advanced + goog.DEBUG=false) and observational (:recovery :ignored): the call still returns nil and still applies nothing.

(rf/configure! {:epoch-history {:depth 100}
                :trace-buffer  {:events-retained 25}
                :elision       {:rf.egress/threshold-bytes 8192}
                :observability {:errors [{:sink :my-app.sinks/sentry}]}})
Key Opts shape Default Status Spec
:epoch-history {:depth N :trace-events-keep N} — :depth non-negative integer (0 disables the ring); :trace-events-keep non-negative integer caps raw :trace-events retention (per Security §Epoch privacy posture). There is no post-projection scrub hook: the in-process ring buffer and every :epoch-stream listener deliver the RAW record (EP-0010 causal replay material), and an off-box forwarder that wants a further scrub composes it — (-> r (rf/project-egress opts) scrub) — at the sink. Per Tool-Pair §Time-travel. {:depth 50, :trace-events-keep 50} v1 (dev-only) Tool-Pair
:trace-buffer {:events-retained N} — non-negative integer event-slot count (one slot per event / pipeline run); 0 disables retention (the surface stays live) {:events-retained 50} v1 (dev-only) 009
:elision {:rf.egress/threshold-bytes N} — non-negative integer; 0 disables runtime auto-detect (only declared / schema entries elide) {:rf.egress/threshold-bytes 16384} v1 009
:observability {:handled-events [<sink-entry>…] :errors [<sink-entry>…]} — the PROCESS-DEFAULT production observation sink policy, in the same closed grammar a frame's :observability takes (Spec-Schemas §:rf/frame-meta FrameObservability). Precedence is per stream: a frame declaring a stream uses its own entries for it, a frame omitting the stream inherits this default's, and {:errors []} on a frame is that frame's opt-out — so exactly one source is consulted per record per stream. Inheritance moves the sink list, not the redaction authority: an inheriting frame's records still project under its OWN classification. Records with no frame authority (the frameless producers, and any record whose :frame no longer resolves) reach this default ALONE, projected with the governing frame explicitly nil. Validated against that grammar at CALL time, fail-loud with :rf.error/bad-frame-classification and :where 'rf/configure! — one grammar, two doors. Unlike every other key here an explicit nil CLEARS it (a policy must be removable without knowing what installed it); a second call REPLACES rather than merges. Per 015 §The process default and §Frame-owned observability sinks. none declared v1 015

Reading the values back. (rf/current-config) returns the live values in this same nested shape — the read twin of configure! per Conventions §configure! vs current-config. Three properties are normative. (1) Process values only — a frame carrying its own :rf.trace/events-retained metadata is not reflected; this reads the same slots configure! writes and nothing else. (2) A subsystem's key is ABSENT when its own producer is unavailable, never nil and never a fabricated default. The two optional keys are independent — each has its own producer, so an absence never implies the other's: :epoch-history is absent from a build that elides epoch, and :trace-buffer from one that never publishes the dev-only trace-tooling sibling's hooks. (get-in (rf/current-config) [:epoch-history :depth]) reads nil when epoch is elided, not when trace tooling is. :observability follows the same absent-not-nil rule for a different reason and the distinction is normative: re-frame.observability is always loaded, so its key is absent when no process default has been declared — a statement about CONFIGURATION, never about the build — and it reports the declared policy verbatim, never any frame's effective policy, which is per-stream and resolved per record. (3) The user-namespaced carve-out is write-only — a :myapp/thing key configure! accepted in silence is not reflected back, because the vocabulary is closed and only keys the runtime READS have live values to report. The result is a key-by-key snapshot rather than a transactional one, and it is not promised to be wire-serialisable.

No :sub-cache key. There is no :sub-cache {:grace-period-ms N} knob: sub-cache disposal is synchronous on derefer-count → 0 (per 006 §Reference counting and disposal); there is no deferred-grace timer to configure.

SSR error-projection policy (:public-error-id, :dev-error-detail?) is not a configure key — it is per-frame metadata on the frame's :ssr map (see Conventions §Configuration surfaces bucket 3 and 011 §Server error projection). Different frames in the same process can carry different projector / dev-detail settings, so the natural lifetime is per-frame, not process-global.

Opts-key naming rule

The opts map for any configure key mixes two shapes — it encodes which contract owns the sub-key:

  • Framework-owned semantic sub-keys use a namespaced keyword under a reserved :rf.<area>/* sub-namespace (per Conventions §Reserved namespaces). The namespace identifies the cross-spec policy area the sub-key participates in — the same key shape appears verbatim wherever that policy is consumed, not only inside configure. Example: :elision carries {:rf.egress/threshold-bytes N} because :rf.egress/threshold-bytes is the same per-call policy key consumed by rf/project-egress and the wire-elision walker beneath it — one key, one fact, three readers, so the process-level knob and the per-call opt are literally the same keyword (per Conventions §Reserved namespaces — :rf.egress/*). The namespaced form makes the cross-surface identity grep-visible and prevents collision with adjacent per-knob settings.
  • Ergonomic per-knob sub-keys are unqualified bare keywords (:depth, :trace-events-keep). These sub-keys are local to a single configure key's opts map — they do not appear elsewhere in the framework's vocabulary, so a framework-owned namespace would add noise without adding identity. The bare form is the default at this leaf position; reach for it whenever the knob is unique to one configure key.

The discriminator is whether the sub-key names a cross-surface policy slot or a one-off knob. A sub-key earns a :rf.<area>/* namespace when it names a contract that lives in more than one place (:rf.egress/threshold-bytes is read by :elision, by the wire-elision walker, and by the MCP wire walker). A sub-key stays bare when it is local to its parent configure key (:events-retained under :trace-buffer and :depth under :epoch-history each tune one ring's slot count and live nowhere else in the vocabulary — separate knobs, no shared contract, so each stays bare).

New configure keys MUST apply the same rule: if a sub-key participates in a cross-spec policy area, qualify it under the area's reserved namespace; otherwise leave it bare. The rule is closed — there is no third shape (no :configure/depth, no :rf.configure/* prefix). A sub-key that would want a third shape is evidence the proposed knob is doing two things and should be split.

Fixed-and-additive

The configure-keys vocabulary is fixed-and-additive (Spec-ulation): existing keys cannot be renamed or removed; new keys are added by extending the table. Because configure! takes a single map and ignores unknown top-level keys, user code that wraps configure can pass a composed config value straight through — unknown keys no-op rather than throw. That pass-through is what the user-namespaced carve-out above protects: a wrapper's own :myapp/* keys ride through in silence. A wrapper composing BARE keys it invented will additionally see the dev-only :rf.warning/unknown-configure-key — which is the point, since a bare key is a name this vocabulary is entitled to own.


Machines

Split between the v1 machine-as-event-handler foundation and the post-v1 re-frame.machines scaffolding library — see 005-StateMachines.md §Disposition. The machine is the event handler: reg-machine / defmachine registers a machine into the same event-handler slot an ordinary reg-event writes.

API M/Fn Signature Status Tier Spec
reg-machine M (reg-machine machine-id machine-spec) / (reg-machine machine-id opts machine-spec) — registers a machine as an event handler. Walks the literal spec form at expansion time and co-locates per-element source on each :guards / :actions entry + a reference-site :source-coords on each :states-tree map node. The optional opts metadata map is the canonical Spec 001 MIDDLE slot; it carries an event-vector :schema — the machine + event-vector-schema shape. v1 advanced 005
re-frame.machines/reg-machine* Fn (reg-machine* machine-id machine-spec) / (reg-machine* machine-id opts machine-spec) — plain-fn surface beneath the macro. No source-coord walking. The optional opts metadata map is the MIDDLE slot (uniform with the macro); it carries the event-vector :schema. v1 advanced 005
defmachine M (defmachine name [docstring] spec) — def-shape that walks the literal spec at the definition site and co-locates per-element source onto the def'd value, for the def-then-register shape (defmachine m {…}) / (reg-machine :id m). Does not register. v1 advanced 005
re-frame.machines/machine-transition Fn (machine-transition definition snapshot event) → one plain map: {:status :ok :snapshot … :fx […]} on success, {:status :error :error {:kind …}} when a guard / action / :data fn threw or a depth limit tripped (per 005 §Level 1) v1 advanced 005
— — Enumerating registered machines has no per-kind accessor: it is (keys (into {} (filter (fn [[_ m]] (:rf/machine? m))) (rf/registrations {:kind :event}))) — the generic registrar query plus the :rf/machine? discriminator, rowed in §Public registrar query API. — — 005
:rf.machine/spawn (fx) — Canonical actor-lifecycle fx (registered globally by re-frame.machines). Args per :rf.fx/spawn-args. v1 — (fx-id) 005
:rf.machine/destroy (fx) — Canonical actor-destroy fx (registered globally by re-frame.machines). Args: an actor id. v1 — (fx-id) 005
:raise (fx) — Reserved fx-id inside a machine action's :fx (machine-internal, routed pre-commit). Args: an event vector. v1 — (fx-id) 005
:final? / :output-key (state-node keys) — :final? marks a leaf state as terminal — entering it completes the machine: a spawned actor reports to its parent's :on-done and is torn down, and a singleton rests there, readable and inert unless its definition declares a way out. :output-key (requires :final?) designates the child's :data slot reported back via the parent's :on-done. Capability axis :fsm/final-states. See 005 §Final states. v1 — (spec key) 005
:on-done (:spawn spec key) — (fn [{:keys [data result]}] new-data) on the parent's :spawn map. Fires on the parent's next macrostep when the spawned child enters a (non-error) :final? state; result is the child's :data slot named by the final state's :output-key (or nil). Returns the parent's new :data map. v1 — (spec key) 005
machine->xstate-json Fn (machine->xstate-json definition) → JSON post-v1 lib tooling 005
machine->mermaid Fn (machine->mermaid definition) → string post-v1 lib tooling 005

Canonical descriptions (factory purity, spec keys, snapshot location, registration-time validation, etc.) in 005-StateMachines.md and Spec-Schemas.

v1 transition-table grammar subset is enumerated in 005 §Capability matrix; shape in Spec-Schemas §:rf/transition-table.

Standard registered subs (machines)

Standard sub Returns Spec
[:rf/machine <machine-id>] The machine's snapshot {:state :data :tags} (or nil if not yet initialised) 005
[:rf.machine/has-tag? <machine-id> <tag>] true iff the machine's current snapshot's :tags set contains tag (false for an unknown / not-yet-initialised machine) 005

The canonical machine read is the registered [:rf/machine machine-id] subscription vector — see 005 §Subscribing to machines. It is read like any other subscription — @(rf/subscribe [:rf/machine machine-id]), @(rf/subscribe [:rf.machine/has-tag? machine-id tag]) — and named projections chain off it by declaring it under :inputs. There is no named-read-sugar fn: a runtime-db framework read is a subscription vector, one grammar.


Story / variant / workspace library (post-v1)

See 007-Stories.md.

All Story surfaces are tooling (a Storybook-shaped dev surface — registration, execution, introspection — not application logic). That statement is exact rather than a summary with an exception: every re-frame.story manifest row carries :tier :tooling, and the facade holds no implementation carve-out. All but five are JVM-introspected; those five are the #?(:cljs …) arm of a split-host facade (register-substrate! / registered-substrates / mount-shell! / unmount-shell! / active-shell), rowed under :cljs-only with :facade? true per row and held fully-rowed by the CLJS enumeration probe. See §Tiering of cross-tool surfaces.

API M/Fn Signature Status Tier Spec
reg-story M (reg-story id metadata) post-v1 lib tooling 007
reg-variant M (reg-variant id metadata) post-v1 lib tooling 007
reg-workspace M (reg-workspace id metadata) post-v1 lib tooling 007
reg-tag M (reg-tag id metadata) post-v1 lib tooling 007
reg-decorator M (reg-decorator id metadata) post-v1 lib tooling 007
reg-story-panel M (reg-story-panel id metadata) post-v1 lib tooling 007
run Fn (run target) / (run target opts) → promise/future of the unified run-result. target is a keyword (registered variant) OR a map (inline plan). The single execution verb. post-v1 lib tooling 007
is Fn (is target) / (is target opts) → runs target and reports each assertion to clojure.test / cljs.test. JVM blocks (bounded by :timeout-ms, default 30000) and returns the run-result; CLJS returns the run promise. post-v1 lib tooling 007
explain Fn (explain target) / (explain target opts) → the plan's :explain map (args-validation, sub-overrides, decorators, …) without running. post-v1 lib tooling 007
variants-with-tags Fn (variants-with-tags tag-set) → seq of variant ids post-v1 lib tooling 007
snapshot-identity Fn (snapshot-identity variant-id) → {:variant-id ... :content-hash "..."} post-v1 lib tooling 007
story-view Fn (story-view variant-id) → hiccup post-v1 lib tooling 007

The recommended execution surface is the three verbs run / is / explain (each accepts a registered-variant keyword OR an inline-plan map) — the vocabulary the Guide and the skills lead with, and what a Story author should reach for first. The lower-level variant lifecycle — run-variant / reset-variant / watch-variant / destroy-variant! / render-variant — is a supported tooling surface too, not implementation vocabulary: custom shells, test fixtures and one-shot screenshot pipelines call it directly, and the Story tutorials teach it. It is not rowed in the table above because this projection rows the slices that surface through re-frame.core or the per-feature artefacts; the complete Story surface is rowed in tools/story/spec/API.md and carried in the api-manifest. The unified run-result is the single execution-record boundary; read its verdict via result-status / result-passed? (:pass / :fail / :cannot-run / :error — there is no :passing? boolean). Canonical execution model: story spec 017 §Public execution API (the re-frame2-story library spec, the normative home for the verbs + run-result shape).


Removed / not shipped

These surfaces are removed or renamed — not part of the public projection and not a tier. This is a migration table: each row names what to use instead.

API What to do Reference
dispatch-with (master) Use (dispatch event {:fx-overrides {...}}) MIGRATION M-4
rf/install-adapter! / rf/adapter-disposed? / rf/current-adapter-spec Not on the re-frame.core facade. Seat the adapter with rf/init!, which throws :rf.error/adapter-already-installed on a DIFFERENT adapter, so a facade install-adapter! would add no observable difference. Read the adapter with rf/current-adapter, which answers the spec map; the discriminator is (:kind (rf/current-adapter)). The two owning-namespace surfaces live on re-frame.substrate.adapter — install-adapter! is the strict primitive (throws on ANY second call, carries the generation / rollback machinery) and adapter-disposed? is the breadcrumb that picks :rf.error/adapter-disposed over :rf.error/no-adapter-installed. No shim, no deprecation alias. 006
dispatch-sync-with (master) Use (dispatch-sync event {:fx-overrides {...}}) MIGRATION M-4
dispatch-to (not shipped) Use (dispatch event {:frame :todo}) 002
subscribe-to (not shipped) Use (subscribe query-v {:frame :todo}) 002
frame-dispatcher / bound-dispatcher / bound-subscriber (not shipped) Use (rf/capture-frame) (the keystone OPERATION BUNDLE — captures the frame at creation; safe during render and from async callbacks) 002
bound-fn (CLJS macro) Use (rf/capture-frame) — the keystone OPERATION BUNDLE captures the frame and carries dispatch / subscribe across the boundary. 002
frame-bound-fn (macro) / frame-bound-fn* (fn) Not on the facade — capture-frame is the ONE public carry primitive. The frame-rebinding closure semantics (re-establish *current-frame* around an arbitrary already-held fn) live internally as re-frame.frame/bind-fn for framework / test / tooling reach; not app-facing. 002
frame-value->id (fn) Not on the facade — every public surface accepts a frame VALUE or its id: the routing ops (dispatch / subscribe / app-db-value / frame-provider / …) normalize a value to its id interchangeably, and destroy-frame! accepts either but reads the value's exact-incarnation lifecycle authority (a stale value no-ops against a same-id successor; a keyword is address-directed). Either way there is no need to unwrap a value to its id. 002
dispatcher Use (:dispatch (rf/capture-frame)) or the dispatch injected in a reg-view body 002
subscriber Use (:subscribe (rf/capture-frame)) or the subscribe injected in a reg-view body 002
current-frame Renamed to current-frame-id (returns a frame-id keyword) 002
get-frame-db Renamed to app-db-value (returns the app-db VALUE, a plain map) 002
(dispatch frame event) / (dispatch-sync frame event) / (subscribe frame-id query-v) / (subscribe-once frame-id query-v) frame-first positional forms Not supported — every sig is [payload] / [payload opts]; target an explicit frame via the trailing {:frame …} opt (a frame-id keyword or a live frame value). 002
enable-performance-api-tracing! (not shipped) Performance-API instrumentation is gated on the compile-time re-frame.performance/enabled? goog-define, not a runtime toggle (see 009 §Performance instrumentation) 009
add-trace-listener / remove-trace-listener (not shipped) Use register-listener! / unregister-listener! 009
register-trace-listener / unregister-trace-listener (no-bang, not shipped) Renamed to register-listener! / unregister-listener! (bang form matches the side-effecting nature of listener registration) 009
Bare [:my-view "args"] keyword-tagged hiccup Use the Var form [my-view "args"] (canonical) or [(rf/view :my-view) "args"] for late-binding by id Conventions
h macro (not shipped) Use the Var form [my-view "args"] or [(rf/view :my-view) "args"] Conventions
reg-global-interceptor Use the frame-level :interceptors config key (frame-level is the canonical "global within this frame"). For cross-frame observation use register-listener!. MIGRATION M-17
clear-global-interceptor No replacement needed — re-declare the frame (make-frame with an updated :interceptors vector; absent-key semantics clear it). MIGRATION M-17
reg-sub-raw Use reg-sub (app-db reads), Pattern-AsyncEffect (non-app-db sources), state machines (lifecycle), or the 006 adapter contract (bridging external reactivity). MIGRATION M-18
reg-event-db Use reg-event (no alias) — destructure :db from the coeffects map and wrap the return in {:db …}: (reg-event id (fn [{:keys [db]} ev] {:db BODY})). A stale call raises the always-on hard error :rf.error/reg-event-db-removed naming reg-event. The ^:no-doc facade throwing stub carries no API-manifest row. 001 §The retired event-registration names
reg-event-fx Use reg-event (no alias) — reg-event IS the identical shape under the bare name (coeffects in, effects out); just rename the call. A stale call raises :rf.error/reg-event-fx-removed naming reg-event. 001 §The retired event-registration names
reg-event-ctx A framework-internal primitive. Express application full-context work as a registered interceptor (reg-interceptor with :before/:after, referenced by id from a reg-event chain). A stale public call raises :rf.error/reg-event-ctx-removed. 001 §The retired event-registration names
get-coeffect / get-effect Not on the façade (no audience; no manifest row). Inside a reg-interceptor :before/:after fn read the context map directly: (get-in ctx [:coeffects k]) / (get-in ctx [:effects k]). The owning-namespace re-frame.interceptor/get-coeffect / get-effect fns are framework-internal. 001, 002
assoc-coeffect / assoc-effect Not on the façade (no audience; no manifest row). Inside a reg-interceptor :before/:after fn write the context map directly: (assoc-in ctx [:coeffects k] v) / (assoc-in ctx [:effects k] v). The owning-namespace re-frame.interceptor/assoc-coeffect / assoc-effect fns are framework-internal. 001, 002
re-frame.alpha/reg The shipped per-kind registrars: reg-event / reg-sub / reg-fx / reg-cofx / reg-flow. (The v1 event trio reg-event-db / reg-event-fx / reg-event-ctx is not a v2 target — those are removed/withdrawn throwing stubs and migration inputs only, see the rows above and EP-0018; reg-event is the single event-registration form.) MIGRATION M-23
re-frame.alpha/sub Vector-form (rf/subscribe [::id arg]). MIGRATION M-23
re-frame.alpha/reg-sub-lifecycle and built-in lifecycle policies (:safe, :no-cache, :reactive, :forever) Sub-cache uses a single algorithm — synchronous ref-counting (dispose on derefer-count → 0), per Spec 006 §Reference counting and disposal. For specific edge cases file a follow-up bead. MIGRATION M-23
feature-loaded? Use the lookup: (get-in (rf/features) [:routing :loaded?]). Note the contract — an UNKNOWN feature keyword reads nil there, not false. §Feature inspection
require-feature! Write the guard: (when-not (get-in (rf/features) [:routing :loaded?]) (throw (ex-info "…" (get (rf/features) :routing)))). Not an assert — asserts are elidable. The inventory entry carries the Maven coordinate + require ns. There are no :rf.error/feature-not-loaded / :rf.error/unknown-feature ids. §Feature inspection
init-platform Platform is the HOST DEFAULT — :client on CLJS (including CLJS-on-Node), :server on the JVM — overridden PER FRAME: (rf/make-frame {:id :ssr/request :platform :server}). There is no process-wide marker and no setter; re-frame.interop/active-platform is a per-host constant the frame's :platform key wins over. A CLJS-on-Node SSR host tags its request frames :platform :server rather than setting a process-wide platform at boot. 002, 011
:preset :ssr-server Write the one key it would expand to: {:platform :server}. The closed preset set is :default / :test / :story. 002

Cross-references