Skip to content

9. Language & Substrate Selection Principle

The spec deliberately does not fix implementation languages per component. It fixes the rule by which they are selected, so the choice is auditable and survives changing tooling.

9.1 Selection rule — by defect blast radius

The cost of a defect dictates how much the language/substrate must prevent defects at compile time or by construction.

  • Safety-critical bucket (a defect can silently corrupt the record, mis-merge patients, leak data, or crash an unattended node): implement in Rust or in-database (SQL / PL-pgSQL / constraints). These make whole error classes unrepresentable — memory safety, exhaustive sum-type matching, no runtime metaprogramming, or database-enforced invariants that no buggy caller can bypass. Members: the sync/merge engine, the identity event algebra and projections (§5.7), HLC ordering, coherence checks, audit-log integrity, access-control enforcement.
  • Fit-for-purpose bucket (a defect is caught immediately, is advisory, or is cosmetic): optimize for iteration speed and ecosystem. Members: probabilistic matcher / record linkage (advisory — proposes candidates, humans/policy decide; Python's ML ecosystem is decisive here), FHIR façade and integration glue, tooling, UI backends.

In-database is a first-class member of the safety bucket, not a footnote: for some merge/projection logic, a constraint or PL/pgSQL routine next to the data is safer and more auditable than any application-layer code in any language, because the invariant is enforced unconditionally and cannot be bypassed.

9.2 Primary quality metric — reviewer-legibility

With AI-assisted development, the binding constraint shifts from authorship fluency to specification + review: comparable results are achievable with far smaller competent teams, and individual per-language coding skill matters much less between design spec and final review. Therefore: - The artifacts that gate quality are the specification and the review, not the typing. - Safety-critical layers are optimized for auditability / reviewer-legibility, even over authorship speed. Rust ("the types document the invariants") and in-database ("the logic sits next to the data it governs") both score high on this axis. - Concentrating safety-critical logic in a small, well-bounded set of restrictive-language components shrinks the audited surface — the part needing the most rigorous review is also the smallest. This directly serves the small-team reality.

9.3 Integration boundary

Polyglot is expected ("horses for courses"). To avoid fragile coupling, the language boundary is the database boundary: each component talks to its node's PostgreSQL; Postgres is the integration substrate. (E.g. the Python matcher writes link-candidate events; the Rust/in-database core consumes them — loose coupling, no FFI.) The same boundary, extended outward to external clients and a plurality of UIs, is §9.5.

9.4 Merge / projection boundary — fat Postgres, thin Rust daemon

Resolves former open question §11.11 — see ADR-0001.

The §9.1 safety bucket is divided concretely. Because every autonomous node runs full PostgreSQL (§2), in-database logic runs everywhere — so safety-critical merge/projection logic is placed where it is unbypassable and reviewer-legible: next to the data.

In Postgres (constraints / PL-pgSQL / triggers — unbypassable, on every node incl. Pi): - Append-only enforcement: the application role is granted INSERT/SELECT only on event tables; a trigger raises on any UPDATE/DELETE as defense-in-depth. - Idempotent apply: INSERT … ON CONFLICT (uuid) DO NOTHING makes sync a clean set-union; a PK collision whose content hash differs is routed to the repair/quarantine queue, never silently merged (data-model §3.2 backstop). - Structural invariants: HLC monotonicity / causal-ordering checks, the closed event_type enum, FK integrity. - The identity event algebra (deterministic application of link / unlink / reattribute / identify / repudiate / dispute, §5.7) and all projections: the chart projection (union of member-UUID event streams), the §3.3 mutable-list unions, the §4.2 demographic projection, the golden-identity connected-component (§5.1), the coherence check (§5.2, demotes to under-review on a §4.2 conflict), the chart trust states (§5.7), and the reattribution overlay (§5.5). - Projections are trigger-maintained incremental tables (AFTER INSERT only — the INSERT-only log means no update/delete maintenance path), not periodic REFRESH MATERIALIZED VIEW; this keeps per-write cost low on Pi-class hardware.

In Rust (the thin sync daemon, §6.1ships and applies, never decides): logical-decoding consumer, scope-predicate evaluation, the §6.1 priority queue, resumable mTLS/HTTPS transport and store-and-forward, idempotent apply. No merge logic.

In Python (advisory only, §5.2 / §10): the probabilistic matcher proposes link candidates by writing candidate events; it never decides. Authoritative application and projection are in-DB. The Python↔in-DB seam is exactly the §9.3 database boundary.

Warning

The load-bearing bet: that in-DB projections stay cheap enough on Pi-class hardware to keep chart reads local and fast (the §1.2 paper-parity floor). A Pi serves only a handful of workstations with little concurrency, so the risk is single-operation latency on a weak CPU + SD/USB storage, not throughput. The first implementation spike is a Pi benchmark that validates or falsifies it.

Escape hatch — an in-database escalation ladder, never leaving Postgres (see ADR-0002):

  1. PL/pgSQL — the default; most legible for set-oriented projection logic; no build step.
  2. Rust via pgrx (in-database) — when a function is hot or algorithmically complex (the identity connected-component is the prime candidate). Compiled-Rust speed and type-safety while the function stays a Postgres function — next to the data, unbypassable, invoked by the same triggers, inside the §9.3 database boundary. So "Rust" and "in-database" are one bucket, not two.
  3. External Rust — only if logic genuinely cannot be a database function (not expected for projections).

The decision is per-projection and criteria-gated (Pi single-op latency, reviewer-legibility, bypassability), not an upfront blanket split. The thin sync daemon still carries no merge logic.

9.5 Layering, the node API, and UI pluralism (uniform core, plural edges)

See ADR-0021. Extends the §9.3 integration boundary outward to external clients and UIs, and is the home of founding principle 12. No new event stream.

The whole mission rests on one guarantee — any Cairn node must interoperate with any other, regardless of which UI or policy a deployment runs — and that guarantee must survive a plurality of front-ends (the design goal: small teams or individuals building bespoke UIs quickly and safely, with the steward's own opinionated "best-of-breed" UI just one citizen among them). The resolution is to recognise that the inter-node contract is the signed event core, below UI and policy, and to enforce the floor where no client can bypass it.

Four layers, with the compatibility boundary below the application layer:

Layer Holds Uniform / plural
L0 Wire/event core — the compatibility contract the signed-event format (ADR-0015), set-union sync + HLC (§6), the identity/actor algebras (§5.7/§3.12), additive-only evolution + the legibility twin (ADR-0012), federation peering (ADR-0017) uniform (non-negotiable)
L1 Node core — the enforcement floor fat Postgres + in-DB/pgrx safety logic (§9.4): validation, signature/content-address checks, the algebras, projections, access-control (RLS), append-only uniform (unbypassable)
L2 Policy / application thin Rust: the stable node API, deployment hard policy cleaner in Rust than SQL, fit-for-purpose orchestration plural
L3 UI many UIs — soft policy, workflow, presentation; the reference UI is one citizen plural
  • The contract is L0, and nothing above it sits on the inter-node path. The only thing that crosses a node boundary is signed events over the sync protocol; API, policy and UI are forbidden from that path by construction. Compatibility is therefore a property of the core, not of the application.
  • The floor is in the database — so direct DB access is safe, and the bypass tension dissolves. Every safety/compatibility-critical invariant is enforced in the DB (the validated write path + RLS + constraints), never only in L2: a client talking raw SQL still cannot write a malformed/unsigned event, mis-attribute authorship, bypass access control, or break the algebra. UIs never INSERT into event tables — they call a small set of validated submit functions (the §9.4 grant model extended: the UI role gets EXECUTE on submit-functions + SELECT on projection views, not raw INSERT). "Via the API" vs "DB directly" is thus a privilege gradient, not a contradiction — the more trusted/co-located a component, the lower it may bind; the floor is identical at every level. L2 is ergonomics + deployment hard policy, never the sole wall; residual Rust-only hard policy is enforced by role-gating raw access for untrusted clients.
  • Hard vs soft policy = §9.1 blast radius applied to policy. Hard policy (a deployment must not bypass it from the UI — an attestation requirement, a retention floor, two-person break-glass) lives anchored in the DB, or in L2 with raw access role-gated. Soft policy (presentation, workflow, salience, prefetch heuristics, optional confirmations, layout) lives in L3, swappable with zero blast radius. This is principle 9 (mechanism vs policy), located on the stack; how hard policy is authored, audited, and projected is §7.9.
  • The native API is additive, versioned, capability-described — the anti-drift guarantee. A UI is a pure producer/consumer of events over a contract it cannot alter, so it can produce content wrong for its clinic but never a wire-incompatible event: the UI never owns serialization/signing (the node canonicalises + signs, ADR-0015); the API evolves additively only (the ADR-0012 discipline applied to the API — principle 11 on the contract); it is capability-described so a UI degrades gracefully against an older/newer node (the §3.13 min(...) ladder); and a published conformance suite makes "any node talks to any node" executable for a small team before it ships (§9.7). The contract properties are fixed; the wire transport (REST/gRPC/SQL-over-the-wire/…) is a later fit-for-purpose choice (§9 fixes the rule, not the tech).
  • Native API ≠ the FHIR façade. FHIR is a boundary skin for external/legacy interop (§3.4); the native API is richer (events, projections, the identity algebra, possession/write-context, notifications-as-projections) and is what Cairn-native UIs bind to. Two surfaces, two jobs.
  • The reference UI is built only on the public API — anti-capture turned inward. The steward's opinionated UI gets no private back door; it consumes the exact contract every third-party UI consumes, so it can never privilege itself or become a chokepoint.

Warning

The validated submit-function surface + RLS + the role/grant model are now part of the §9.1 trusted base for external clients too — they are the floor for direct-DB callers. The one thing that must be complete is that surface: every legitimate write must be expressible through it, or UIs are pushed to raw access and the bypass re-opens. The submit-function → validated-append seam is the safety-critical path (the recurring seam motif); its shape is §9.6.

9.6 The validated submit surface (the write path)

See ADR-0022. Details the §9.5 submit surface — the authoring counterpart to the §9.4 apply boundary. The completeness of this surface is the bet ADR-0021 rests on.

The submit surface must be both small (the smallest audited trusted base — principle 8) and complete (every legitimate write expressible, or UIs are pushed to raw access and the §9.5 bypass re-opens). Both hold because the system is append-only: almost every write is the same operation.

  • One generic validated-append, not one per type. submit_event(envelope, body) is generic in signature and type-validated by dispatch to a per-(event_type, schema_version) validator registered additively (ADR-0012) — a new event type adds a validator behind the same door, so the surface never grows. Clinical, identity (§5.7), actor (§3.12), demographic (§4.1), overlay (sensitivity / responsibility-tags / acknowledgments / the ADR-0020 visibility-suppression overlay), federation, and audited-config events are all this one append. A set of co-produced events (ADR-0020 order + note-line) commits atomically.
  • The non-append surface is a small closed set, each a narrow specially-audited door because it is not a pure append: erasure / key-custody (ADR-0005 — destroys/re-custodies a key, emits an append-only erasure-declaration audit event; irreversible rung carries the ADR-0020 forced-rationale gate), author-scoped export (ADR-0019 — read+package + audit event), and blob byte-tier put (§6.6 — content-verified on store). Everything else is submit_event; reads are SELECT on projection views (outside the surface); drafts (§3.10) are local mutable never-synced state behind a separate API, becoming a submit_event on commit.
  • submit_event is the in-DB convergence of every write-time seam, atomic and in order: verify the author-attestation token (ADR-0008 — never the DB session, §3.10; this is why a direct-DB client cannot forge authorship) → authorize (§7) → envelope well-formedness + the §3.6 Tier-1 ceiling → body validation (dispatched; coherence §4.2; Tier-2 clash → flag, never resolve) → hard-policy gates (the ADR-0010 suppressing owner-gate; the §5.9 sensitivity auto-grade + seal-time safety projection — reading the §7.9 effective-policy projection) → canonicalize + derive the plaintext twin (ADR-0012) + sign (ADR-0011/ADR-0015) + digest → idempotent append (ON CONFLICT DO NOTHING; AFTER INSERT projections, §9.4).
  • The suppressing owner-gate (step 5) is now enforced, self-only on human-authored content: a suppressing overlay (salience.downgrade / visibility.suppress) targeting another human author's event is refused at the floor — disagreement is expressed additively instead; an agent-authored/un-owned advisory stays dismissable by any enrolled human. Enforced identically at both write-time doors (submit_event and remote-apply) via one shared helper (ADR-0043, refining ADR-0010).
  • Signing must be reachable from the in-DB path — else a direct-DB caller could not produce a signed event and the ADR-0021 floor would be incomplete. So the node's trusted base includes the database process (fat Postgres): signing is in-DB (pgrx + node-keystore) by default, or delegated to a co-located trusted signer the in-DB submit invokes — never required to sit in L2.
  • Authoring ≠ applying. submit_event is the authoring path (mint, validate, sign, append). The apply path (§6.1) ingests already-signed peer events — verify signature/content-address + idempotent append, never re-sign or re-run authorship/authz. Both end in the same append; only authoring signs.

Warning

submit_event becomes the most safety-critical code in the system (mis-sign / mis-attribute / mis-validate / mis-gate → corruption or leak) → reviewer-legible, the prime pgrx candidate (ADR-0002). The author-attestation-token verification and the in-DB signer/keystore access are the two sub-seams; the validator-dispatch registry must itself be additive-only and tamper-evident.

Projections register the same way — and register-by-row makes healing generic. (ADR-0057, refining ADR-0048 and ADR-0045.) The §9.4 AFTER INSERT projections are wired the ADR-0048 way: a projection lives only in its registered apply function — one fn(event_log) RETURNS void per (event_type, projection) row in a locked cairn_projection_apply table (load-time fail-closed validation, REVOKEd, two-place row-count guards) — and a single dispatcher trigger on event_log replaces every per-type projection trigger. Because a CREATE OR REPLACE heals only future inserts, and because every projection is arrival-order-independent by construction (§6.1, #115), a bug fix or new projection heals its already-materialised rows by generic replay through that same registered dispatchcairn_reproject(prefix), run automatically by the loader when (and only when) the recorded schema generation changes (§6.5 code-plane update), before the generation is stamped so a failed heal self-retries rather than leaving projections silently stale. Heal mode is insert-or-better (no deletes; converges the wrong-winner class; skips honestly-flagged non-idempotent counter projections); rebuild mode (TRUNCATE + replay, for the wrote-garbage class heal cannot reach) refuses to truncate a table any out-of-scope type also writes. The replay routes every candidate through a cairn_replay_eligible(event_log) seam so a reprojection can never grant power to a deferred, not-yet-re-adjudicated event (ADR-0056 decision 4). This retires the former per-slice bespoke backfill: a projection change ships inside a schema-generation bump and heals automatically, or a new projection fails CI for want of a registry row.

9.7 The native API contract: capability description and conformance

See ADR-0023. Specifies the §9.5 native API's "additive, versioned, capability-described, conformance-tested" properties — the practical anti-drift tool for small teams building bespoke UIs.

API compatibility is the same problem as schema evolution (ADR-0012): permanent, unbounded version skew across an offline fleet, where a UI may meet a node newer or older than itself. So the contract uses the ADR-0012 machinery — additive-only evolution + the §3.13 min() ladder — not a monotonic version number.

  • Capability flags over a mandatory baseline, not version gates. A node advertises a set of capabilities — the (event_type, schema_version) validators it serves (§9.6) and its optional features — over a small mandatory core every conformant node has. Feature growth is additive flags, never version bumps that strand old UIs; a coarse baseline marks the floor, everything above is set-based and partially ordered.
  • The capability descriptor is a served, self-describing projection of local-node-properties — not new state: capabilities are a function of a node's installed schema versions, loaded validators/extensions, and config, all already local node properties (ADR-0012). It is additively evolvable, legible across time (a future UI reads an old descriptor and vice versa), and transport-independent (describes operations/capabilities, not REST endpoints — the same contract binds REST/gRPC/in-process; §9.5's "properties fixed, transport later").
  • Stateless description + client-side graceful degradation, not a handshake. The node serves its descriptor (cacheable); the client adapts — no stateful round-trip that could fail during a partition (principle 5). A UI lights up optional capabilities when present and degrades to the floor when absent (the min(what the UI needs, what the node offers) ladder). Degradation may reduce experience, never correctness or safety: the mandatory core is the floor, so the §5.9 safety projection and honest assembly-state are present on every conformant node.
  • The conformance suite is the executable contract — two faces. Wire/node conformance (does this node correctly participate in L0 — canonical signed events ADR-0015, set-union, the §5.7/§3.12 algebras, additive-only) — is the "any node talks to any node" guarantee made checkable, and a federation may require it at admission (ADR-0017, the technical gate distinct from the trust gate). API conformance (does the L2 API honor the contract for the capabilities it advertises) is what lets a small team trust "any conformant node serves my UI." It is capability-partitioned (conformant = core + the tests for every claimed capability) and additively versioned (new capability → added tests, never removed — a dropped test silently strands the guarantee).
  • Self-runnable and self-verifiable — no Cairn-owned gatekeeper. The suite is published like the spec and the ADR-0014 registry (open, signed, content-addressed, mirrorable); a node proves conformance by running it and publishing a signed result, never by a certificate the steward could grant or withhold — anti-capture turned inward (ADR-0021 denied the steward's UI a private API; this denies a conformance chokepoint). The suite is also the spec's executable form (principle 11): a team that cannot read every ADR can run it. Descriptor and suite are two views of one capability set — what a node claims vs what it can prove.

Note

The load-bearing call is the mandatory core: small enough for a Pi-class node to fully conform, yet rich enough that "conformant" is a meaningful promise to a UI (the ADR-0001 cost tension, now for the contract). The remaining native-API follow-on is how hard policy is expressed (DB-anchored config vs role-gated L2, the identity §5.10 expressible-policy rung).

9.8 The advisory-actor integration contract

See ADR-0030. Promotes the ecosystem/0001 evaluation to a decision; extends the §9.5§9.6§9.7 application-layer arc to external advisory agents. Demonstrated by Spike 0002 (C1–C5 PASS, 2026-06-21).

An external advisory actor — a triage agent, a pathology-import pipeline — is an L2/L3 client (§9.5), never on the inter-node path, that authors only through the validated submit surface (§9.6). It is one registered actor dropping advisories through the same public floor every UI uses — many front-ends, one record applied to machine authors. The contract:

  • Authors only through submit_event; the grant floor makes direct DB access safe by construction. Event-log write privileges are revoked; the agent's role is granted only EXECUTE on the submit door + read on projections, so even a raw-SQL caller cannot bypass the floor (ADR-0021 made checkable, Spike 0002 C5.4).
  • A registered actor that authors un-vouched by construction. It enrolls with its own signing identity (ADR-0011 / ADR-0029) and contributes with a contributory role (e.g. triaged) carrying no responsibility — "AI-generated / un-vouched" is emergent from the contributor set (ADR-0007), never an is_ai flag (Spike 0002 C1).
  • Additive, never suppressing. An advisory raises salience; a suppressing event from an un-attested actor is refused in-DB (ADR-0010). Suppression and any asserted responsibility require a human attestation token bound to the event's content-address (signature ≠ attestation; the conservation-of-responsibility loop). A human attests an advisory by overlay; the agent never auto-acts on the irreversible.
  • Provenance-anchored and recallable. An advisory cites its source by content-addressed reference (ADR-0013, reference-eager/byte-lazy); contributions bound to an actor + skill-epoch (ADR-0029) are recalled via the contamination overlay, never erased (principle 2).
  • Enforced unbypassably at the L1 floor — fails closed, legibly. Verify, actor-resolution, classification, the attestation gate, and provenance binding run in-DB in the single write door (ADR-0022; the in-DB Ed25519 verify is the ADR-0002 move). A buggy or hostile agent gets a legible rejection, never silent corruption (Spike 0002 C5).

Note

Honest ceiling: Spike 0002 validated the rejection half of the attestation contract (un-attested suppress/responsibility refused); the attestation success path — a valid human token accepted, correctly bound to the event — is the first thing to exercise atop this contract before a production advisory deployment. The agent-action gate (a CASSANDRA-style "no irreversible act without a human") is complementary and out of scope — it governs what an agent may do, while this contract governs what may become record; the two compose through additive, un-attested authorship.