Audit log
On Zkrinet: add the block under Building blocks in the panel — one click, your own code is untouched and a snapshot is taken first. The installation steps below apply if you run PocketBase yourself, for example after an export.
Who changed what, and when — for every record created, updated or deleted via the API (your app, the admin panel and batch requests). The log is tamper-evident: each row carries a SHA-256 hash of its own content plus the previous row's hash. If a row is changed or deleted afterwards, it shows immediately.
Tested against PocketBase 0.40.4.
#Installation
Copy pb_hooks/ and pb_migrations/ to your PocketBase and restart. Logging starts immediately.
#What is logged
One row in zk_granskning per change:
| Field | Content |
|---|---|
nr | Sequence number, without gaps |
samling, post_id, handling | What: create / update / delete |
vem_typ, vem_id, vem_samling | Who: superuser, anvandare or gast |
andringar | Created: all values. Updated: only fields that changed, with before and after. Deleted: the values it had. |
tid | When (UTC) |
ip | Only if turned on (see below) |
foregaende, hash | The chain |
Secrets are never logged. Hidden fields are shown as (dolt fält) and password changes as (lösenord) — neither the value nor the hash ends up in the log.
A change that changes nothing, and requests that fail, produce no row. A batch request that fails is rolled back completely — the log rows too — so the log never claims a change that did not happen.
#Verify the chain
GET /api/zk/granskning/verifiera (superuser)
→ { "ok": true, "rader": 1532, "sista_nr": 1532, "fel": [] }
If something is wrong, it states which row and what: "Raden har ändrats efter att den skrevs." ("The row has been changed after it was written.") or "Rad 40–41 saknas." ("Rows 40–41 are missing.")
The log cannot be changed or deleted via the API — not even by superusers.
#Settings (environment variables)
| Variable | Default | What |
|---|---|---|
ZK_GRANSKNING_SAMLINGAR | all | Comma-separated list if only certain collections should be logged |
ZK_GRANSKNING_IP | off | 1 = store the sender's IP. IP addresses are personal data — only turn this on if you have a reason, and mention it in your privacy policy. |
ZK_GRANSKNING_DAGAR | off (keep forever) | Delete rows older than that many days (every night at 03:17, or POST /api/zk/granskning/rensa). Only the oldest consecutive rows are removed, so the chain stays intact. |
The system's own collections (_superusers etc.) and plugin collections (zk_…) are not logged.
#Limitations — read before you promise a customer anything
- Only changes via the API. Changes made directly in server code (other hooks, cron jobs, migrations) are not logged.
- Tamper-evident, not tamper-proof. Someone with access to the database file itself can change rows — but cannot do so without breaking the chain. Someone who deletes the entire log or the oldest rows also cannot be detected by the chain itself; if you want to protect yourself against that, store
sista_nr+hashsomewhere else at regular intervals. - In a batch request the rows can come in reverse order (PocketBase runs the parts of the batch nested).
- If logging fails (e.g. full disk), the change has already been carried out — the error is written to the server log.
#Test
./bank.py granskningslogg
12 tests: who/what/when, guest and superuser, that secrets are never logged, empty changes and errors, selection, IP, that the log is locked, that the chain reveals both changed and deleted rows, batch (successful and failed), and cleanup. The security tests are verified with mutants.