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:
- Configuration changes — every CRUD on sites, devices, sensors, rules, channel thresholds, recipient lists, integrations.
- Alarm lifecycle —
armed → grace → alarm → acknowledged → cleared. Includes the acknowledger's identity and free-text note. - 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.
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:
- The PDF, by itself. Most common. The PDF cites the audit trail for each event. Inspector trusts the PDF.
- The dashboard, on-site. Open the audit log in the dashboard; inspector walks the chronology. Useful when the inspector is sceptical of the PDF.
- 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".