Skip to content

4.3.1

Choose a tag to compare

@github-actions github-actions released this 16 Sep 21:28
· 58 commits to main since this release
Immutable release. Only release title and notes can be modified.
v4.3.1
4229db8

Added

  • A fourth backup CronJob for the audit domain (#3399). The Helm chart
    renders backup.audit as soon as database.audit.existingSecret gives the
    audit trail a database of its own, with its own claim, credential and
    schedule, and drops --schema=audit from the clinical dump in the same
    render. Until then nothing changes: the trail shares the clinical database
    and travels with that dump. Before this, an audit database placed elsewhere
    was backed up by nothing and the operations page told the operator to dump it
    by hand.
  • An erasure tombstone on the party change-event stream (#3417). A
    physically deleted party now leaves one contribution-less row on the party
    domain's own outbox before the delete commits, naming the party and every
    PARTY_RELATIONSHIP erased with it, so a consumer of the party stream is
    told to delete what it derived. GDPR Art. 19 makes the controller
    communicate an erasure to each recipient the data reached, and the clinical
    outbox has carried the same row since #3416.
  • A dispatch-only storage benchmark lane (#3370). It runs the storage
    benchmark against a PostgreSQL 18 service with durability on, compares every
    operation with the record committed for the same schema generation, and fails
    the run when a p50 or p99 rose further above it than the dispatched tolerance.
    The summary lists each operation's baseline, measurement, delta and verdict
    and names both machines; the fresh record leaves as a run artifact and the
    committed baseline changes only in a pull request. Exploration, never a
    conformance record: it earns no performance class.
  • Restriction of processing, at whole-EHR or single-object grain (#3324).
    A restriction register records who asked, on what ground and when, and the
    mark it drives stops every path that would process the object: a point read,
    a versioned read and a revision history answer 403 with a body naming the
    restriction, AQL omits it at every scope, an EHR Extract skips it and refuses
    outright for a restricted EHR, the change-event stream neither emits nor
    delivers an event about it, and a write to it is refused. The stored rows are
    untouched — storage is the only processing GDPR Art. 18(2) leaves. Lifting
    stamps the register rather than erasing it, so the sequence Art. 18(3) turns
    on survives, and a whole-record lift leaves an object-scoped restriction
    standing. POST/GET {base}/admin/restriction and
    POST {base}/admin/restriction/lift drive it, each act an access record of
    its own.
  • A per-EHR objection to research processing (#3325). While it stands, the
    record leaves every full-population AQL query, every export and the event
    stream, and a query naming the ehr_id or a read for care is untouched: GDPR
    Art. 21(6) reaches research processing, not the care record. The
    public-interest override the article admits is recorded beside the objection
    and says on whose authority. POST {base}/admin/research-objection.
  • A retention register, and a list of what has fallen due (#3346). The
    period per content category and jurisdiction with the legal citation it rests
    on, a per-EHR anchor, a whole-record hold and a per-object hold, and a view
    that lists the EHRs whose period has run with the counts of objects due and
    objects exempted. Nothing is deleted on a timer: an openEHR record is
    indelible, so acting on the list is the controller's decision, taken through
    the physical delete. Five routes under {base}/admin/retention/, and a new
    book page, Retention, restriction and
    objection
    .
  • An access-log retention ceiling beside the floor (#3346). Set
    [audit.store] sgb_v_309_controller = true where the deploying organisation
    is, or acts for, one of the controllers SGB V § 307 names for a German
    telematics-infrastructure application, and boot refuses any
    retention_days above the three-year period § 309 Abs. 1 sets — including
    0, keep forever. A floor and a ceiling that contradict each other are
    refused outright rather than silently resolved in favour of one.

Fixed

  • A date with text after it compares as no date (#3436). openehr_date_days
    read the leading four, six or eight characters of a value and ignored the
    rest, so 20210102XYZ, 2021-01-02 BC, 2021-01-02 Europe/Amsterdam and
    2021a compared and ordered as dates in AQL. BASE foundation_types
    master06-time_types.adoc §Iso8601_date admits none of them, and each now reads
    as NULL, which is a comparison miss rather than an error. 2021-01-02T10:30:45
    and its space-separated and lowercase-t variants are unchanged.

Changed

  • The release profile links with thin LTO (#3098). [profile.release]
    gains lto = "thin", the whole-binary optimisation the Cargo book describes
    for a CPU-bound path; the overflow-checks, panic = "unwind" and line-table
    posture is unchanged. codegen-units = 1 was tried in the same cycle and
    withdrawn: the arm64 release build exhausted its runner twice. A release
    build takes longer; the throughput before/after is measured under #3452 once
    the box can be idle.

  • LATEST_VERSION under CONTAINS is one key probe per object (#3453).
    The AQL emitter used to express "this row is the current trunk version" as
    two correlated EXISTS subqueries over the head table, which let the planner
    reach the version row by vo_id alone through the commit-time index and walk
    every trunk version of the object for every node row: 75 601 of the 93 255
    buffers a class-s population CONTAINS read. The head row is now joined on
    its primary key and the version row reached through its own, sys_version
    bound; the plan-shape tests pin the index and a ceiling of eight buffers per
    probe. Results are unchanged.

  • The server needs only btree_gist, which it installs itself (#3433). The
    second-generation schema calls nothing from uuid-ossp, pgcrypto or
    pg_trgm, so the PostgreSQL image no longer installs them and the CI step no
    longer creates them. btree_gist backs the temporal key on
    linkage.subject_ehr and the server creates it at boot, so a plain
    PostgreSQL 18 database with a migration credential that may create it serves
    with nothing preinstalled.

  • The conformance statement is signed by a named person for the company
    (#3438). A company signs through someone: the party statement's signatory is
    now "Ruben Talstra, for Vernum Projecten B.V." with the date of the change,
    and the rendered statement follows. AI_STATEMENT.md names Ruben Talstra as
    author and maintainer, since the owner of the work is the company.

  • The Licensor and copyright holder is Vernum Projecten B.V. (#3435). Every
    statement of the holder names the company: LICENSE and the per-crate
    licence texts, REUSE.toml, the SPDX header of every first-party file
    (emitted for the generated crates, so they were regenerated), the licensing
    page, the landing footer and the conformance party statement, whose rendered
    statement and certificate were regenerated with it. The licences themselves
    are unchanged — the application and the three hand-written engines stay
    Business Source License 1.1 with the same Additional Use Grant, Change Date
    and Change License, the five generated model crates stay Apache-2.0, and
    releases up to v4.0.17 and crate versions up to 0.0.56 stay MIT as published,
    under the holder they were published with. Ruben Talstra remains the
    maintainer credit the banner, MAINTAINERS.md, the OCI authors label and
    the chart maintainers carry, and the urn:rubentalstra:ferroehr conformance
    identifier is an identifier rather than a holder statement and stays.

  • The production deployment profile refuses the two compatibility
    defaults
    (#3323). The per-EHR EHR_ACCESS default is open and the audit
    trail fails open, both chosen so the conformance instrument and the
    development stacks work out of the box. Neither default changes; what changes
    is that deployment_profile = "production" now refuses to start while either
    stands, naming the key, the value and the remedy, unless the deployment
    accepts it by name in deployment_accepts — the mechanism the profile
    already uses. The two new gap tokens are open_ehr_access_default and
    audit_fails_open. GDPR Art. 25(2) asks that by default personal data not be
    made accessible without the individual's intervention to an indefinite number
    of natural persons; the obligation binds the controller's production posture,
    which is what the profile expresses.

  • The deployment posture reads the audit pool and every domain database
    (#3398). shared_cluster now includes the audit pool's own cluster identity,
    so an audit database co-located with a pseudonymisation domain is reported
    instead of being invisible (Swiss DSV Art. 4 Abs. 5 keeps the log on a system
    separate from the one processing the data). migrate_on_runtime_credential
    is evaluated per domain database rather than only from [db] migrate_url: a
    domain relocated to a database of its own is prepared on that domain's
    runtime DSN, which is the same gap under a different key.

  • ext.openehr_timestamp refuses a zone name and a BC era (#3427).
    PostgreSQL's own date/time input reads 2021-01-02T10:30:45 Europe/Amsterdam and 2021-01-02T10:30:45 BC, and the separator-position
    gate in front of it let both through. BASE foundation_types
    master06-time_types.adoc §Iso8601_date_time ends the value at the offset, so
    both now read as NULL: an AQL comparison against such a stored value misses
    rather than matching a date the record does not carry. The check costs the
    helper 93 ms per 50 000 readings against 65 ms without it, measured on the
    same corpus where the first generation took 117 ms.

  • The archive export lists multimedia blobs from the reference index
    (#3420). It used to walk every exported body in process to find the blob
    keys; it now reads blob_ref, the index the node write path maintains in
    the same transaction as the nodes it describes, once per domain. The archive
    it writes is unchanged.

  • The ext value helpers are each in the form that measured fastest, and read
    fewer things than PostgreSQL's own date parser did
    (#3351). Five of the
    seven no longer open a subtransaction per row: they guard their casts ahead of
    them instead of trapping an error. The two whose whole body is parsing a date
    or a time keep the trap, because measurement makes it the cheapest validator
    available — over 50 000 readings they cost 58 ms and 70 ms, where the same
    bodies validating by hand so the cast can run untrapped cost 65 ms and
    235 ms — and each carries that measurement in its function comment. Every
    helper reads faster than the one it replaces, and a value any of them refuses
    reads as NULL. Three readings tighten as a result, all of them
    values openEHR does not define: a date whose year carried leading whitespace
    or a sign is now unreadable, a date handed to the time parser no longer
    yields a number, and ext.openehr_timestamp refuses a date that is not
    zero-padded (2021-1-2), an ordinal date (2021-002), and the words
    PostgreSQL accepts (now, today, yesterday, epoch, infinity). The
    first two made a stored value read as the current time. In the
    other direction it now reads the lowercase t and space separators on a
    reduced-precision date, which it used to read only at full precision.

  • A physical EHR delete reaches everything the erasure has to reach
    (#3347, #3403). DELETE {base}/admin/ehr/{ehr_id} runs one clinical
    transaction that removes the EHR and, through the foreign-key graph, its
    versions over both storage tiers with their nodes, attestations,
    contributions, commit audits, item tags, folder memberships, restriction and
    retention marks, multimedia blob references and pending change events; the
    subject proxies of a subject whose only record this was go with it, and an
    erasure tombstone is appended to the change-event stream, which each
    registered reader receives and applies by deleting what it derived (GDPR
    Art. 19). The cross-reference row naming the subject is then erased in the
    linkage domain, on the bulk route as well as the single one, and the
    externalized blobs no surviving version references leave the object store.
    The audit records naming the ehr_id stay, because Art. 17(3)(b) withholds
    erasure where a legal obligation requires the processing and the
    access-logging periods are that obligation.

  • Multimedia blob collection is an index lookup rather than a scan
    (#3347). clinical.blob_ref and its party twin record which stored version
    references which externalized blob, written by the node write path, carried
    across the archival tier move by their foreign key and removed with the
    version. The collector reads that index instead of pulling every node body of
    the deleted record into the process and substring-matching it against every
    node of both domains.

  • An OPT 1.4 template whose ordinal symbol has no <value> element is
    refused with the fix in the message
    (#3395). The ITS-XML profile schema
    types C_DV_ORDINAL.list as the RM DV_ORDINAL, whose symbol is a
    DV_CODED_TEXT carrying a mandatory value, so such a template is not a
    document the format admits and the refusal stands. Beside the element and
    line it already reported, the 422 now says to add <value/>, that an empty
    element is valid, and that the rubric is resolved from term_definitions.
    CKM's exporter writes the element; Archetype Designer's omits it.

  • The migration-immutability rule binds SHIPPED files only (owner ruling
    2026-09-15). A migration is immutable once it is in a release (present at
    the latest vX.Y.Z tag); a file on main that no release carries has been
    applied by no installation and is fixed in place. The guard
    (scripts/checks/migration-immutability.sh) judges the pull request's diff
    against the release tag's tree and re-arms at every cut; nothing changes for
    an installation on a released version.

  • BREAKING — the storage schema is rewritten, and a database created by an
    earlier release must be recreated
    (#3342, #3344). The rewrite is greenfield
    (owner ruling 2026-09-14): the new migration sets replace the old ones
    outright, nothing is upgraded in place, and the server refuses at boot a
    database whose migration record predates the rewrite. Take a dump of anything
    worth keeping before upgrading, recreate the database, and let the server
    migrate it. What changed, and why:

    • The ehr schema becomes clinical and demographic becomes party, each
      laid out as a sequence of natural migration files with one concern apiece
      rather than one squashed baseline.
    • version is append-only: it carries no validity interval, so a
      supersession writes no close-out statement, and the validity of a version
      is derived from its committed_at. One mutable vo_head row per versioned
      object answers "what is current", and none of the columns a commit updates
      appears in an index, so that update is heap-only.
    • version, node and vo_attestation are partitioned by storage tier, so
      archiving an EHR is row movement inside one relation set rather than a copy
      into mirror relations: the cold and cold_demographic schemas, every
      *_all union view and the vo_archive marker table are gone, the archive
      marker is two columns of vo_head, and a foreign key holds across the tier.
    • node gains the promoted name_code and name_terminology the AQL node
      predicate needs, and drops the unread citem_num.
    • The clinical and party change-control and node relations are rendered from
      one shared DDL template, so the two domains cannot drift.
    • The party domain gains party_relationship_target, the target-side index
      behind PARTY.reverse_relationships.
    • The archive file format carries a version's committed_at instead of a
      validity interval. An archive written by an earlier release does not load.
  • BREAKING — the EHR id / subject cross-reference moves to the linkage
    domain, and erasure reaches it
    (#3345). linkage.party_ehr becomes
    linkage.subject_ehr and absorbs ehr.ehr_index: one temporal relation
    holding both the party-to-EHR map and the openEHR Service Model's EHR Index
    associations (I_EHR_INDEX), which is what that service is for — an EHR
    persisted with only an EHR id, and a separate index to obtain the subject
    identifier. The EHR_INDEX operations and the subject-proxy subject
    resolution now run on the linkage pool; the clinical schema keeps only the
    promoted ehr.subject_id/subject_namespace pair, under the pseudonym
    guard, because the wire binds ehr_get_by_subject and the one-EHR-per-subject
    409 to EHR_STATUS content. The subject-proxy relations (sp_subject,
    sp_variable, sp_data_set, sp_sample) key on a derived opaque
    subject_key instead of the caller's own subject identifier, so no
    unguarded subject identifier remains in the clinical domain (GDPR Art. 4(5)),
    and a catalog test asserts it. Deleting an EHR now removes the
    cross-reference rows naming it, through linkage.erase_ehr — a
    SECURITY DEFINER function the ferroehr_linkage role may execute while
    still holding no DELETE on the table — so an erased record leaves no row
    behind saying whose it was (Art. 17(1)). Subject-proxy registrations and
    index associations made under an earlier release do not carry over; the
    storage rewrite above requires a fresh database in any case.

  • BREAKING — one pool and one DSN per storage domain, and the runtime roles
    are named after them
    (#3343). Each of the four storage domains — clinical,
    party, linkage, audit — now has its own configuration table,
    [storage.<domain>] with url/url_file, and its own connection pool
    carrying only that domain's schema on its search_path. Every domain defaults
    to the shared [db].url, so a configuration file that names no [storage]
    table at all, and the compose quickstart, behave exactly as before. What
    changed for a deployment that had already separated its credentials:

    • [db].demographic_url, [db].demographic_url_file, [db].linkage_url and
      [db].linkage_url_file are gone. Move them to [storage.party] and
      [storage.linkage]; the old keys are refused at boot by name rather than
      ignored.
    • The chart's database.demographicExistingSecret,
      database.linkageExistingSecret and their *Key/*Url siblings become
      database.<domain>.{existingSecret,existingSecretKey,url} for clinical,
      party, linkage and audit. The mounted files are
      storage.<domain>.url and the environment names are
      FERROEHR__STORAGE__<DOMAIN>__URL_FILE.
    • The runtime roles are the domain roles and nothing else:
      ferroehr_clinical, ferroehr_clinical_reader, ferroehr_party,
      ferroehr_party_reader and ferroehr_linkage, beside the DDL-only
      ferroehr_migrator. The first generation's ferroehr_ehr,
      ferroehr_ehr_reader, ferroehr_demographic, ferroehr_demographic_reader
      and the generic ferroehr_app/ferroehr_reader pair are gone: no
      migration creates or grants to them, the compose database image no longer
      provisions them, and a DSN that authenticates as one of them holds nothing.
      A login role is granted the domain role of the pool it serves
      (GRANT ferroehr_clinical TO <login>); the clinical role also writes the
      local Audit Record Repository.
    • The compose backup service ferroehr-backup-demographic is
      ferroehr-backup-party, its directory variable
      FERROEHR_BACKUP_DEMOGRAPHIC_DIR is FERROEHR_BACKUP_PARTY_DIR, and the
      chart's backup.demographic.* values are backup.party.*.

    A DSN naming another host moves that domain to a database or cluster of its
    own, which is the separation a schema inside one database cannot make: a base
    backup, WAL archiving and physical replication carry every schema of a
    database together (PostgreSQL 18, Backup and Restore). Schema preparation
    follows: the ext set is applied per database by whichever domain reaches it
    first, so two co-located domains share one copy and a relocated domain carries
    its own. One layout is refused rather than half-applied — the linkage
    migration set revokes a function the party set creates, so those two are
    prepared in the same database.

    The boot gate grew two refusals. Two domains configured on DIFFERENT DSNs that
    authenticate as the SAME database role are refused, because a separation that
    exists only in the configuration reads as one that holds. And a domain role
    that does not exist is a warning under deployment_profile = "sandbox" and a
    refusal under production, where absent roles mean absent grants and the
    check would otherwise pass by having nothing to measure. ferroehr config check prints the resolved domain layout, naming no DSN.

  • The Helm chart is 10.1.1, a major bump (#3342, #3343, #3344, #3378). The
    values contract changed in three ways a values file can notice: the per-domain
    database.<domain> blocks above replace the flat demographicExistingSecret
    and linkageExistingSecret keys, config.tenancy no longer exists and is
    refused at render with the reason, and the per-domain backup CronJobs dump the
    generation-2 schemas (clinical, ext, audit and party) instead of the
    retired mirror schemas. The docker-compose backup
    services dump the same sets, and all six jobs now pass --strict-names, so a
    --schema pattern matching nothing fails the dump instead of writing an
    artefact that silently lacks that half (PostgreSQL 18, pg_dump §Options).

  • A newer runtime base image (#3439). The server and the viewer image build
    on gcr.io/distroless/cc-debian13:nonroot at digest sha256:54df941e, the
    digest that tag resolves to today. The
    org.opencontainers.image.base.digest label moves with it in both
    Dockerfiles and in the two publishing lanes, which is what the image-labels
    guard checks.

  • Four runtime dependencies move to their current patch (#3442). lapin
    4.11.0 retries the initial AMQP connection properly when recovery is on,
    which is the mode the events transport runs in; reqwest 0.13.5 fixes proxy
    authentication picking the wrong credentials when several proxies match a
    request, and a blocking-client timeout panic; tokio-rustls 0.26.5 returns
    more data per poll_read on a TLS stream; fancy-regex 0.19.2 cuts pattern
    build time. No API a caller here uses changed.

  • jsonschema 0.55.1 (#3443). It compiles the vendored ITS-JSON schema
    behind openehr-its's schema-validation feature. The releases since 0.53.0
    correct the keyword location an error names and several schema
    canonicalisation defects; the 626-test openehr-its suite, the ITS-JSON
    corpus gate among it, passes unchanged. The eight published openehr-*
    crates step to 0.0.67 with it, since the requirement is part of their
    packaged manifest.

Removed

  • The Subject Proxy service (#3431). It realised SM
    I_SUBJECT_PROXY_SERVICE and I_DATA_BINDING over the sp_* tables, had no
    REST route and no consumer outside its own configuration. Gone with it: the
    [subject_proxy] configuration section and its FHIR-frame executor, the
    sp_* tables, the erasure reach into them, the Helm config.subject_proxy
    values and the Subject Proxy book page. No openEHR specification obliges a
    CDR to realise every SM component; SM master02 presents the platform as a
    set of services a deployment composes. docs/architecture.md lists the
    component as not realised.
  • Multi-tenancy (#3378). Multi-tenancy is achieved by running separate
    instances: one instance, one database, one set of domain roles per
    organisation. That is where openEHR puts it. An openEHR system is "a
    distinct logical repository corresponding to an organisational entity that is
    legally responsible" for the data, and it is "distinct from any underlying
    virtualisation infrastructure or cloud computing facility, which may house
    multiple logical EHR systems in a multi-tenant fashion" (BASE
    architecture_overview/master06-design_of_the_ehr.adoc §The EHR System) — so
    tenancy belongs to the layer that hosts several systems, not inside one. The
    same chapter's §System Identity is why housing several organisations in one
    system was wrong rather than merely unnecessary: system_id names that one
    legally responsible system, it "becomes embedded in the version identifiers of
    committed -- and possibly signed -- content", and it "cannot easily be changed
    afterwards". No relation carries a tenant column, no row policy or session GUC
    scopes a read, and the [tenancy] configuration, the tenant middleware, the
    /admin/tenant routes and the Helm and compose tenancy keys are gone. The
    pseudonymisation domains and their role barriers are unrelated to tenancy and
    stay.

Security

  • rustls 0.23.45 (RUSTSEC-2026-0285): the TLS stack no longer accepts TLS 1.3 handshake messages sent at the wrong encryption level after a key change (RFC 8446 §5.1). Lockfile bump only; no configuration changes.