Zum Inhalt springen
Docs

Befehlspalette

Zu einer Seite oder Ansicht springen oder ein Todo per Name finden.

Änderungen

Wundklar · clinical trail

A hash-chained, per-Organization trail written in the same transaction as each clinical event, sealed every tick and verified daily.

Markdown

Every clinical insert and every correction append one trail event in the same transaction. The trail is per-Organization, insert-only and cryptographically chained so a replayed or dropped event is detectable. Decision: ADR-0021; code: packages/api/src/features/trail.

Chain

hash_0 = SHA-256("wundklar-trail-genesis:" + organizationId)
hash_n = SHA-256(hash_{n-1} ‖ 0x0a ‖ JCS(event))

JCS is RFC 8785 canonical JSON (via canonicalize); hash_{n-1} is the previous event's prev_hash. The chain starts with the Organization's provisioning event, so an org that never acted still has a verifiable genesis.

The event payload includes the actor (user and device), the entity touched, a stable kind from TRAIL_ACTIONS and the before / after shape required for corrections, plans and share-link accesses. Nothing in the payload is clinical text in the clear: patient and photo references stay as ids, while free-form reasons are kept inside the cryptographic hash so an audit can still prove them without exposing the text in logs.

Writing

Trail rows live in three tables (trail_event, trail_link, trail_verification) granted system-only. The tenant handle never writes them directly; every clinical handler calls appendTrail inside the same transaction as the clinical insert — so if the handler rolls back, the trail does too, and if the trail write would fail the clinical write fails with it. The link rows are added by the sealer under a per-org advisory lock so parallel ticks cannot interleave.

Sealing and verifying

The cron tick runs two jobs:

  • trailSeal every tick seals unsealed events per Organization under the advisory lock, writing the trail_link rows that cover [previous_last_hash, current_last_hash].
  • trailVerify runs daily at 03:00 Europe/Berlin and recomputes the chain, writing a trail_verification row with the result; a mismatch raises an alert, nothing is overwritten.

Both are registered in packages/api/src/platform/jobs/run.ts (ADR-0022).

Reading

trail.list returns events for an entity, newest first, with the chain reference each row sits on. The desktop case page shows them inline; the MD-package export includes them verbatim under the same hashes so a court can replay the chain independently.

Auf GitHub bearbeitenDie Dokumentation ist auf Englisch.