# Wundklar · clinical trail

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

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](/docs/adr/0021-hash-chained-clinical-trail);
code:
[`packages/api/src/features/trail`](https://github.com/CarlosZiegler/wundklare/blob/main/packages/api/src/features/trail/trail.chain.ts).

## Chain [#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 [#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`](https://github.com/CarlosZiegler/wundklare/blob/main/packages/api/src/features/trail/trail.chain.ts)
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 [#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`](https://github.com/CarlosZiegler/wundklare/blob/main/packages/api/src/platform/jobs/run.ts)
([ADR-0022](/docs/adr/0022-background-work-stays-in-the-cron-tick)).

## Reading [#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.

