Audit trail

Append-only, hash-chained evidence

For HACCP and Legionella the audit trail is the product. The PDF report is the deliverable; the audit trail is the thing an inspector trusts that the deliverable is not faked. This page describes how the trail is constructed, what is guaranteed, and what is not.

What is recorded

The audit trail records three kinds of events:

  1. Configuration changes — every CRUD on sites, devices, sensors, rules, channel thresholds, recipient lists, integrations.
  2. Alarm lifecyclearmed → grace → alarm → acknowledged → cleared. Includes the acknowledger's identity and free-text note.
  3. Access events — logins, magic-link issuance, support-team access invocations, API token rotations.

Each event is a row in an append-only table. Rows are never UPDATEd or DELETEd; corrections are new rows with correction_of = <id>. A corrected row is rendered with a strike-through in the dashboard and the PDF; the correction row appears immediately below.

Measurement audit

Measurements themselves are also append-only. A measurement that turned out to be wrong (sensor fault, calibration issue) is not deleted — it is annotated with a note row that an operator attaches:

{
  "channel_id": "chn_4f3c1a",
  "ts": "2026-05-15T03:11:00Z",
  "note": "Reading erroneous — probe was dislodged during cleaning. Discarded for compliance reasons; physical re-mount documented.",
  "by": "ops@cafe-bratislava.sk",
  "at": "2026-05-15T08:40:00Z"
}

The PDF report renders the original reading with a footnote pointing to the annotation. The auditor sees both. We never edit history silently — that is the entire point.

Hash chain

Each audit row carries a prev_hash and a hash. hash is SHA-256(prev_hash || canonical_json(row_payload)). The chain is per-account. Any tampering with a past row invalidates every hash after it.

The current head hash is signed nightly with a key whose public half is published at https://opensense.murzin.digital/legal/audit-pubkey.pem. The signed head is committed to a public log (https://opensense.murzin.digital/legal/audit-heads.txt) along with the day's date. The combination — publication + signature with a published key — gives any auditor an external check on whether we silently rewrote your history.

ok
Head hash today matches the row chain you can reproduce locally
warn
Mismatch — open a ticket
alarm
Mismatch + we can't explain it. Tell the inspector. This is on us.

What we can prove

  • No reordering of past rows — the chain forbids it.
  • No silent edits — corrections are new rows.
  • No silent deletions — corrections of corrections still leave the original visible.
  • Approximate clock honesty — server timestamps are NTP-synced to a publicly verifiable pool. Drift > 50 ms triggers an internal alarm.

What we can not prove

We are honest about the limits.

  • Pre-ingest data integrity. OpenSense cannot tell whether a reading is the real fridge temperature or whether someone aimed a hair dryer at the sensor. Probe placement and physical security are the customer's problem.
  • Real-time append. If an attacker drops the network for an hour and re-establishes it, our audit trail captures the gap honestly but cannot fill in what happened during it.
  • Cryptographic non-repudiation of acknowledgements. The "acknowledged by" stamp is identity-asserted by our session, not by the operator signing the acknowledgement with their own key. This is good enough for HACCP and Legionella as practised; not good enough for pharmaceutical GMP. For pharma, see the Enterprise tier roadmap.

How an inspector verifies

Three options, in increasing rigour:

  1. The PDF, by itself. Most common. The PDF cites the audit trail for each event. Inspector trusts the PDF.
  2. The dashboard, on-site. Open the audit log in the dashboard; inspector walks the chronology. Useful when the inspector is sceptical of the PDF.
  3. The hash-chain receipt. Download the JSON-LD export. Inspector (or their auditor) verifies the chain locally against the daily published head. Useful for high-stakes events.

We have not yet had option 3 invoked in a Slovak ŠVPS inspection. It exists because for legionella in care homes the question can be raised, and we want the answer to be "yes" not "uh, let me ask".

Sources