ReferenceVersion 1.012 October 2026
User-defined apps: reference architecture
ClearMetric defines a user-defined app as an app one person builds for their own work, usually with AI, that runs with that person's access on the organization's shared, governed data, metrics, rules and actions.
The definition
Most software gives everyone the same screen. A user-defined app inverts that. The screen belongs to the person; what is shared sits underneath. One governed core holds the data, one definition per metric, the rules for every action and the state of every process. Each person describes the app they want as a spec, a document the core can check, and the core runs that spec as that person, with that person's access and nothing more.
The term has other uses: network firewalls use "user-defined application" for a custom traffic signature, and research projects use "user-defined apps" for apps an AI agent generates for one person on their phone. ClearMetric uses it for the architecture on this page: the definition above, five guarantees (section 6) and a reference architecture.
The claim
Not claimed
- A new category. Enterprise data platforms ship much of the pattern inside their own runtime.
- New diff mechanics. Model-level data diff tools are prior art.
- That accruals or open-period revisions are diffed. They are not.
- That the core is outside the trusted base. It is in it, everywhere.
- "No lock-in." It moves to the contract format and the kit.
1One core, many specs
Three existing architectures each solve half the problem. Personal software lets anyone build their own app, each with its own database; nobody shares a number. BI has one definition per metric, but every screen goes through an analyst and there are no writes. Low-code and data-platform apps govern writes, but a few builders make team apps inside a licensed runtime, as code the core cannot check.
A user-defined app keeps all three halves by splitting the system: a core everyone shares, a spec per person. (A team or org app is a personal app promoted to wider visibility; the unit of the definition is the personal app.) The spec carries no credentials and no policy. The core applies the person's access on every call, so copying a spec to someone else changes what it shows, not what it can reach.
Lab 1: build one
The controls write Ana's spec. Switching the person copies it to them: the core re-promotes the copy as that person and renders it with their access. Nothing in the spec names a region.
The spec did not change when the person did. Groups with too few rows are suppressed (min-cell) on read and before any snapshot is stored.
2The capability contract
An app reaches the core only through the capability contract: a generated, versioned description of what the core offers, in four kinds. It is shaped like the business, not like screens, because the screens are not known in advance.
- Entities: fields, filters, the scope dimensions the core fixes (names, never the expression), a binding class the core derives.
- Metrics: one definition, a grain list, an owner, a version. Reads return
(value, metric, version, grain, scope label). New versions are parallel definitions; an app pins one in its lockfile. - Actions: inputs the core resolves to in-scope entities, rule names (expressions stay in the core), a single-bit preflight, idempotency, declared compensation, audit always.
- Processes: states and transitions compiled to Temporal. Workflow structure is pinned per run; rules are evaluated by a thin RPC to the core, so a rule change applies at the next transition and is a change event.
entity: invoice
fields: {id, customer_id, amount, due_date, status}
filters: [customer_id, status, due_date]
scope: {fixes: [region]} # shape for the builder; the expression stays in the core
class: state # derived by the core; natural key: id
entity: invoice_status_history
fields: {invoice_id, status, valid_from, valid_to}
class: time # period column valid_from
bindable: false # feeds as_of metrics; binding it directly is forbidden
metric: overdue_amount
definition: "Sum of amount for invoices past due_date and not paid as of the date,
from invoice_status_history"
grain: [customer, rep, region, month]
as_of: date
scope: {fixes: [region]}
owner: finance
version: 3 # v4 is a parallel definition during a window
class: as_of # from the owner's as_of parameter
action: approve_credit
inputs: {customer_id: in_scope_entity(invoice.customer_id), amount}
rules: [within_credit_limit, customer_active] # names ship; expressions stay in core
preflight: single_bit # allowed / not allowed; rate-limited per user and app
idempotency: key(customer_id, request_id)
audit: always
process: credit_request
states: [requested, under_review, approved, posted, reconciled, rejected]
transitions:
requested -> under_review: auto
under_review -> approved: action approve_credit, approver role credit_manager, escalate 48h
approved -> posted: action post_credit
posted -> reconciled: nightly job
engine: temporal
Lab 2: preflight is one bit
Preflight answers allowed or not allowed, nothing else. Reasons come back only on submission, only for in-scope inputs. The customer list is already resolved to the person's scope: a customer outside it does not exist to this app.
Rules, as the builder sees them: within_credit_limit, customer_active. The limits never leave the core.
- —nothing yet
- —no entries
A reason-rich preflight enumerates: "over limit by 3,200" reveals the limit; "customer inactive" confirms an unseen customer exists. One bit still leaks by repeated calls, so the per-person, per-app rate limit sets the leakage per window; in-scope resolution before any write closes the second channel. Constraint errors that survive resolution come back generic, because foreign-key checks bypass row security.
3Build-time isolation
The person's own AI builds the spec. It must never see production data, and must never learn production data through side channels while building. Two planes:
- The edit plane: a throwaway container per session. Shape-only catalog (scope dimensions and rule names, no expressions), pinned kit and typed SDK, synthetic seed (distributions, null rates, cardinalities from profiling; categorical values are synthetic tokens). No credentials, no route to the run plane.
- The run plane: the client's cloud. Signed specs run as their people. No MCP server, no inbound AI path.
The only bridge is promotion. A change bundle (spec diff, evals, lockfile) goes through six steps. The AI's verdict is fixed at step 1; nothing after it can change what the AI hears.
- Structural check against the lockfile's versions and the person's permissions, on capacity isolated from shadow runs. Verdict:
pass, orstructural failwith binding names. Emitted at submission time plus a fixed delay D, in batches ordered by submission id. Not finished by D:timeout, which carries no information, and the bundle is re-queued. - Kit check.
- Shadow run as the person on real data, server-side, no AI. Empty states, nulls, errors, p95, query cost: all of it goes to the human owner only. The edit plane's input filter strips numbers from anything the owner pastes back.
- Custom components reviewed by a named person, at any tier.
- Approver by tier for team and org apps.
- Signed spec and lockfile. Rollback pins the previous one. The edit plane is not told.
Lab 3: two channels, one bundle
Left: everything the AI ever hears. Right: what the owner hears. The timestamps on the left depend on when the bundle was submitted, never on what the checks found.
- —press Play
- —press Play
D = 60 s here. Publish state never reaches the AI.
Any data-dependent bit, or data-dependent timing, is an oracle: a verdict that waited for the shadow run would let a builder binary-search production with a clock. The structural verdict can reveal the person's own grants, which are not out-of-scope data.
4The change lifecycle
The engineering claim, and the part no surveyed system does.
Apps on a shared core break when the core changes: a field is renamed, a metric's meaning changes, a backfill restates last quarter. Today you break silently, notify everyone about everything, or freeze the core. Here every change gets a pre-image (what the app showed before), lineage finds the dependent apps, the core diffs old against new at each app's grain and scope, and the owner gets the diff.
- Code changes (model SQL, metric definition, policy, rule, kit): the pull request is the pre-image. Structural breaks are checked against every spec: compatible ships, migratable gets an AI-proposed spec change for the owner to approve, breaking is blocked until a migration or a window exists. Auto-migration only for renames whose compiled expression is identical modulo alias.
- Data changes: the governed snapshot store is the pre-image. After every promotion and load, the core stores each app's bound results in its own Postgres under row security. Only restatements fire.
What is a restatement
A restatement is a change to, or disappearance of, a cell already in the pre-image. An accrual (a new key, period or as-of date) is not one; neither is a revision inside an open period. Which cells count depends on the binding's class, and the core assigns the class, never the app.
- Ya closed period's cell changes or disappears
- Na new period
- Na cell in an open period
- Yan existing key's values change
- Yan existing key disappears
- Na new key
- Yany pre-image cell changes or disappears
- Na new cell
- Ya past as-of date's cell changes
- Nthe current as-of date's value
- Na new as-of date
Lab 4: fire a change
Five bindings, three apps, four classes. Pick a change; see which bindings fire, why, and what each owner receives. Then follow one app through the window.
| App | Bound to · grain · scope | Class | |
|---|---|---|---|
| A1 | Ana personal | overdue_amount v3customer · West | as_of |
| A2 | Ana personal | entity invoicerows · West | state |
| B1 | Ben personal | billed_amount v2rep × month · East | time |
| F1 | Finance team | overdue_amount v3month · all regions | as_of |
| F2 | Finance team | active_customersregion · all regions | snapshot |
Window and endgame
For a change of meaning, old and new definitions serve in parallel for a deprecation window (14 days by default) with the version on every value. Owners migrate or do nothing; nothing semantic is auto-migrated. After the window, an app still pinned to the retired version renders read-only from its snapshot, only while the person's current policy version and resolved visibility still match the snapshot's key; otherwise "scope changed" or "policy changed", no values. Exports and scheduled deliveries stop. After 30 days read-only, personal and team apps are archived. Org apps never get here: the data team migrates them before the change ships.
Snapshots are kept until 30 days after the last lifecycle that references them: 74 days for an app that reaches the endgame, up to 44 if it migrated in the window. The latest snapshot of every live binding is always kept; it is the next pre-image.
5The architecture
Click a layer; the rest dims.
Green marks the mechanisms the claim rests on; dashed lines are trust boundaries.
Determinants. The snapshot store is a second copy of protected data, so it must match the database's row policy exactly. Each policy declares the relations it reads; lint rejects anything undeclared, including now(). The policy version hashes the expression and those relations' schemas. A person's determinant is their resolved visibility, the set of policy-column values they can see, recomputed on any write to a declared relation. A mapping change that moves an account between regions changes the determinant without touching the person's own row. Snapshots are keyed by (policy version, determinant hash); two people with the same visibility share a key, which is what lets diffs dedupe. A policy that cannot resolve to a finite set is marked per_user.
Browser containment. The main realm runs only the static kit under a hash CSP (no nonce, no 'strict-dynamic'), with Trusted Types as defense in depth. Custom components run on a separate origin in iframes with exactly sandbox="allow-scripts", each behind its own MessagePort, under connect-src 'none'; the parent's frame-src controls navigation. WebRTC ICE and DNS prefetch stay open in every current browser; their only control is managed-browser policy and device-level egress rules, outside the page.
6Five guarantees
A system is a user-defined app system when it holds all five.
G1One definition, legible version and scope
A governed value is (metric, version, grain, scope). Every rendered value shows the metric, the version and a scope label the core emits; the label never contains an expression or an out-of-scope quantity. In a window, two people on different versions see different tags.
G2No escalation; the core in the trusted base everywhere
Apps carry no credentials or policy; every call runs as the person through the core. Row policy on the source is enforced in the database under an identity the core asserts (Postgres set_config per transaction; Snowflake session attributes, one session per person, torn down on attribute sync, result cache off) or the person's own (Databricks on-behalf-of). The snapshot store is the core's Postgres under FORCE ROW LEVEL SECURITY, keyed by (policy version, determinant hash). Rebuilds are safe: policies are applied inside the materialization transaction, views are security_invoker and checked recursively, materialized views are rejected, the pre-serve check fails closed. The core's role owns no tables and has no BYPASSRLS. A copied app is re-promoted as its new person.
G3Build-time isolation
The AI builds on synthetic seed with no production path. The channel back to it carries no data-dependent bit: the structural verdict is fixed at step 1 on isolated capacity, sent at submission plus a fixed delay or as timeout, batched by submission id, and the edit plane never learns publish state. Everything else goes to the human owner.
G4No silent restatements
Code changes carry the PR as pre-image; data changes carry the governed snapshot. The core assigns each binding's class. A restatement is a change to, or disappearance of, a cell already in the pre-image; accruals and open-period revisions are not events. Every code change and restatement is diffed at the app's grain and scope and sent to its owner. Nothing semantic is auto-migrated: the owner acts or the app goes read-only, with the pre-image kept through the whole read-only period.
G5Declarative apps
A spec rendered deterministically by a pinned kit. Because the app is a document, the core can check it, diff it, sign it and re-promote it as someone else.
7Compared
Vendor documentation as of October 2026. Pick a system to compare it with UDA alone.
What is different: row 10 (the lifecycle and the in-core scope diff), row 11 (operating model), and rows 7 and 8 together (per-person ownership of checkable specs). The rest is table stakes somebody already meets.
8Limits
Part of the definition. Leaving them out is overclaiming.
- Semantic drift inside an app is not solved. Lineage, version and scope labels, review and per-app diffs are the mitigations.
- Accruals, open-period revisions and the current as-of date are not diffed.
- Snapshot equivalence to the source policy holds for declared-determinant policies;
per_userpolicies do not dedupe. - The core is in the trusted base everywhere; on-behalf-of narrows it for the source only.
- Human fan-out is the operating cost. Windows and tiers bound it; the acceptance test measures it.
- Catalog quality is the product, measured by build success and shadow pass rates.
- Scoped apps, the snapshot store and per-person warehouse sessions cost more than one cached dashboard.
- Lock-in moves to the contract format and the kit.
- The scope-level diff is built, not bought. Model-level diff engines are optional.
- A data change with no snapshot has no pre-image and is reported as "no baseline."
- A rule change reaches in-flight process runs at their next transition; only workflow structure is pinned.
- WebRTC ICE and DNS prefetch stay open in every current browser; the control is outside the page.
- Unevaluated. The acceptance test (20 real apps from three MCP clients, 280 simulated bindings, a blind change set applied by a third party, adversarial tests, kill criteria fixed in advance) has not run. Any kill criterion failing means the engineering claim does not hold.
9Terms
Fixed senses. A later version may add terms; it will not change these in place.
- user-defined app
- A user-defined app is an app one person builds for their own work, usually with AI, that runs with that person's access on the organization's shared, governed data, metrics, rules and actions.
- governed core
- The shared half: data with row policy, semantic model, policy engine, action runner, process engine, snapshot store, scope-diff harness, catalog, ledger.
- spec
- The document that is the app: pages, components, bindings, layout, settings, lockfile. Rendered deterministically by a pinned kit.
- capability contract
- The generated, versioned description of what the core offers: entities, metrics, actions, processes.
- lockfile
- The contract and metric versions an app pins. Signed at promotion.
- edit plane
- The throwaway build environment: shape-only catalog, pinned kit, synthetic seed, no credentials, no route to the run plane.
- run plane
- The client's cloud, where signed specs run as their people. No inbound AI path.
- change bundle
- Spec diff, evals and lockfile submitted for promotion.
- promotion
- The six-step bridge between planes.
- scope label
- The name the core emits for a person's scope on every value. Never an expression or quantity.
- policy version
- Hash of a row policy's expression and the schemas of the relations it declares.
- determinant
- A person's resolved visibility under a policy: the set of policy-column values they can see. Recomputed on any write to a declared relation.
- binding class
time,state,snapshotoras_of. Assigned by the core. Decides which cells are pre-image cells.- pre-image
- What an app showed before a change: the pull request for code, the governed snapshot for data.
- restatement
- A change to, or disappearance of, a cell already in the pre-image. Fires an event.
- accrual
- A new key, period or as-of date. Never fires.
- scope-diff harness
- The core's own diff: old versus new per (capability, grain, determinant), run as each person, deduped by determinant.
- deprecation window
- 14 days by default, old and new definitions in parallel, version shown.
- endgame
- Read-only from snapshot under the person's current (policy version, determinant) only; exports and schedules off; archived after 30 days.
- tier
- Personal (owner, automatic checks), team (lead approves), org (data owner approves; migrated before a change ships).
CITECite this page
Cite the version you read. A DOI will be added when the first archived version is deposited.
@misc{kim2026uda,
author = {Kim, Jon},
title = {User-defined apps: reference architecture},
year = {2026},
month = oct,
version = {1.0},
publisher = {ClearMetric},
url = {https://www.clearmetric.ai/user-defined-apps/reference},
note = {Definition and reference architecture. Design, unevaluated.}
}Versions
| Version | Date | Change |
|---|---|---|
| 1.0 | 2026-10-12 | First version. Definition, guarantees, reference architecture v8, four labs, comparison, limits, terms. No results. |
Next: results of the acceptance test; corrections from external review.