08 — Schemas and data classification¶
Schemas in re-frame2 are Malli schemas attached to app-db paths. You register them with reg-app-schema (path-keyed, not id-keyed — the only reg-* that breaks that pattern, deliberately); the runtime validates app-db writes against the matching schemas in dev; production builds elide the validation at the call sites; and a small set of marks (:sensitive?, :large?) on the schemas drive automatic redaction and size-elision at every wire boundary.
The payoff is that the same schema declaration drives three separate surfaces: dev-time validation, observability redaction (Causa, story-mcp, off-box error forwarders), and bundle-time size protection. You don't write the privacy rules three times in three different places; you declare them once on the schema, and the framework's wire-boundary walker enforces them everywhere.
This chapter covers the registration macros (rowed in 01 — Core, summarised here), the introspection surface in re-frame.schemas, the validator-extension seams (set-schema-validator! etc.), the boundary-validation interceptor, and the data-classification mechanism (add-marks, set-marks, plus the egress-side sensitive? / redact-interceptor / elide-wire-value surface). For the canonical contract, see 010-Schemas.md and Privacy.md.
Registration¶
reg-app-schema¶
- Kind: macro
- Signature:
- Description: "Attach this Malli schema to this
app-dbpath." Path is the registration id — the:app-schemaregistry kind is path-keyed because schemas-at-paths matches the dataflow grain.(app-schema-at [:user])looks up by the same path vector. - Example:
- In the wild: 7Guis
reg-app-schemas¶
- Kind: macro
- Signature:
- Description: Bulk plural form. Feature-modular apps registering 5–20 paths against the same prefix reach for this. Each entry routes through the singular form and is stamped with this call's source coords. Returns the vector of paths registered.
- Example:
- In the wild: realworld
The path-keyed-not-id-keyed asymmetry is principled. Paths are first-class in get-in / assoc-in / update-in; schemas-at-paths matches the dataflow grain; the lookup site (app-schema-at [:user]) reads the same way the write site ((assoc-in db [:user] ...)) reads. Spelling it as (reg-app-schema :user/schema schema) would have shifted the registration's id away from the dataflow grain.
See Conventions §reg-* return-value rule for the wider convention this row participates in.
Introspection¶
The introspection surfaces 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 — the registration macros live in re-frame.core and route through the schemas artefact at registration time, but the read-side surface stays in its own namespace.
app-schemas¶
- Kind: function
- Signature:
- Description: "Hand me every registered schema-at-path for this frame." Returns
{path schema}. Tools and agents walk this to enumerate the app's schema surface.
app-schema-at¶
- Kind: function
- Signature:
- Description: "Schema for this exact path." Returns the schema value or
nil.
app-schema-meta-at¶
- Kind: function
- Signature:
- Description: "Full registration-metadata map for this path." Returns
:path,:schema,:frame, plus source-coords (:ns/:line/:file) and the rest of:rf/registration-metadata. Pair tools and 10x reach for this when they need the registration anchor for click-back-to-code. The lighterapp-schema-atis the right call when only the schema value is needed.
app-schemas-digest¶
- Kind: function
- Signature:
- Description: "Single hash over the frame's whole schema surface." Used by SSR hydration compatibility checks and by tools that want to know "has the schema corpus changed?" without diffing schema-by-schema.
Validator-extension seams¶
The default validator ships Malli's validate / explain pair. These seams let apps swap in their own validator — typically to drop the Malli dep entirely, or to add a custom explainer that formats failures for the app's domain.
set-schema-validator!¶
- Kind: function
- Signature:
- Description: "Install the validator the framework uses at every dev-time schema-validation site."
nildisables validation entirely. The default ships Malli's pair; this seam is for apps that want to swap to a different validator without forking the framework.
set-schema-explainer!¶
- Kind: function
- Signature:
- Description: "Install the explainer the framework uses to enrich
:rf.error/schema-validation-failuretraces':explainkey." Companion toset-schema-validator!.
set-schema-printer!¶
- Kind: function
- Signature:
- Description: "Install the schema-print companion the digest pipeline hashes."
(fn [schema-value] canonical-string). Must be pure and deterministic across runtimes.nilfalls back to the default EDN canonicaliser, so the digest is never undefined. Parallel to the validator / explainer setters: non-Malli ports register their own serialiser so cross-runtime digest comparison reflects their port's contract.
The three setters answer three different questions: validation correctness (validator), human-readable failure messages (explainer), and stable canonical printing for digest (printer). Most apps use the defaults; ports and apps swap them selectively.
The boundary interceptor¶
validate-at-boundary-interceptor¶
- Kind: Var (interceptor value)
- Signature:
- Description: A pre-built interceptor value, not a fn (interceptor
:idis:rf.schema/at-boundary). Add it to areg-event-*'s positional interceptor vector for production-boundary validation. Do not call it as a fn — it has no fn arity; invoking(rf/validate-at-boundary-interceptor ...)raisesArityException.
(rf/reg-event-db ::receive-from-server
[rf/validate-at-boundary-interceptor]
(fn [db [_ payload]] (assoc db :data payload)))
The pattern: dev-time validation runs at every commit by default; production-time validation runs only at handlers wearing validate-at-boundary-interceptor. Use it on handlers that ingest data from outside the app's trust boundary (HTTP replies, websocket frames, postMessage handlers).
Data classification¶
The same schemas that drive validation also drive redaction and size-elision at every wire boundary. The mechanism: schemas carry :sensitive? and :large? flags on the paths that need them; the framework's egress-side walker elide-wire-value consults the registered marks; sensitive paths render as :rf/redacted and large paths render as :rf.size/large-elided summaries.
The mark-set¶
add-marks¶
- Kind: function
- Signature:
- Description: Frame-scoped path-marks. Additively merges into the frame's existing mark-set — paths not mentioned keep their prior state. Schema-attached marks per
reg-app-schema:sensitive?/:large?are preserved and union at lookup time. Pure declaration — does not mutateapp-db. Returnsframe-id.
set-marks¶
- Kind: function
- Signature:
- Description: Frame-scoped path-marks. Wholesale replaces the frame's prior mark-set — paths not mentioned are CLEARED. Schema-attached marks are preserved. Pure declaration. Returns
frame-id.
The two-verb shape (add vs set) follows the Conventions §Tear-down verb axis — add- merges; set- replaces. Most apps reach for add-marks because path classifications accumulate (an audit reveals a new sensitive path; you add-marks the path without affecting the rest of the corpus). Reach for set-marks when you're declaring the entire authoritative classification at once (a server-pushed policy update; a feature-flag toggle that swaps the whole privacy posture).
The egress-side surface: elide-wire-value¶
This is the framework primitive that walks tree-shaped values at the wire boundary and substitutes elision markers for sensitive or large slots. Every tool that emits wire data — off-box error-monitor forwarders, the Causa-MCP and re-frame2-pair-mcp and story-mcp servers, the on-box dev panels — routes through this walker. The walker is the single normative emission site for the :rf/redacted sentinel and the :rf.size/large-elided marker. Per-tool reimplementation is prohibited.
elide-wire-value¶
- Kind: function
- Signature:
- Description: Walk
vconsulting[:rf/elision :declarations]and[:rf/elision :sensitive-declarations]of the named frame'sapp-db. Substitute:rf/redactedfor sensitive slots and:rf.size/large-elidedmarkers for large slots.optsmap:{:rf.size/include-large? :rf.size/include-sensitive? :rf.size/include-digests? :rf.size/threshold-bytes :path :frame}. Defaults: bothinclude-*flagsfalse(maximum elision);:rf.size/threshold-bytesfalls back to(rf/configure :elision ...)then16384.
elision-declarations¶
- Kind: function
- Signature:
- Description: "What paths has the frame nominated for elision?" Returns the current
[:rf/elision :declarations]map for the frame (or{}). Pair-tool and introspection reader.
populate-elision-from-schemas!¶
- Kind: function
- Signature:
- Description: Boot-time hydrator that walks the frame's registered app-schemas and writes
{:large? true :source :schema}declarations for every path whose Malli schema carries:large? true. Idempotent. No-op when the schemas artefact isn't on the classpath.
Composition rule¶
When both predicates match (:sensitive? AND :large? apply to the same path), sensitive drop wins — the size marker is suppressed because it would leak :path / :bytes / :digest information from a sensitive slot. The walker's composition rule is normative; per 009 §Size elision in traces.
Schema-only declaration path¶
The [:rf/elision] registry has exactly two slots: :declarations (schema-derived :large? paths, populated by populate-elision-from-schemas!) and :sensitive-declarations (schema-derived :sensitive? paths). There is no runtime declaration API — apps declare :large? / :sensitive? on the Malli schema and rf/reg-app-schema it; the boot-time hydrator does the rest.
The single normative reference for "schemas are the only path" lives in Guide ch.25 — Large blobs.
Privacy: the always-on predicate and the interceptor¶
The trace runtime stamps :sensitive? true at the top level of every trace event emitted inside the scope of a handler whose schema-derived path overlap declares sensitivity. (The legacy handler-meta :sensitive? annotation has been removed — sensitive data marking is path-based per the data-classification mechanism above.) Framework-published trace consumers — Sentry / Honeybadger forwarders, the re-frame2-pair server, Causa, Story, story-mcp, re-frame2-pair-mcp — MUST default-drop the stamped events at their egress boundary.
sensitive?¶
- Kind: function
- Signature:
- Description: The framework-published predicate every consumer composes against. Replaces per-consumer reimplementations of the same five-token check.
redact-interceptor¶
- Kind: function
- Signature:
- Description: Build a positional interceptor that overwrites the named keys in the event vector's payload map with the
:rf/redactedsentinel before the handler chain runs. The handler body itself sees the UNREDACTED payload via the regular:eventcoeffect slot; the redaction is for the trace surface only.pathsis a vector ofget-in-style key paths into the payload map.
The composition pattern: schema-derived :sensitive? marks drive elide-wire-value at egress, and redact-interceptor scrubs in-place where the trace surface needs to see only a partial view. The two surfaces stack — the interceptor scrubs the trace; the walker enforces redaction at the wire boundary.
See Security §Privacy / secret handling for the framework-wide pattern-level posture, and Privacy.md for the cross-artefact inventory and composition order.
See also¶
- 01 — Core —
reg-app-schema/reg-app-schemas/add-marks/set-marksrowed in registration. - 03 — Effects and interceptors —
validate-at-boundary-interceptorrowed in the interceptor table. - 11 — Instrumentation —
elide-wire-valueand the trace-surface privacy posture. - Spec 010 — Schemas, Privacy.md, Security.md.