How a hash-chained log secures audit continuity (EU AI Act Art. 12)
EU AI Act Article 12 requires high-risk AI systems to maintain «automatically generated event logs» resistant to tampering. AZTender Audit implements this via a hash-chained append-only log: each record contains the SHA-256 hash of the previous one, and a Postgres trigger physically forbids UPDATE/DELETE. Here is how it works and how to verify it.
What EU AI Act Article 12 says (in brief)
«High-risk AI systems shall technically allow for the automatic recording of events («logs») throughout the system's lifecycle. Logs shall enable the traceability of the system's functioning to an extent appropriate to the intended purpose.»
The key word is «traceability». The regulator must be able to reconstruct any decision made by the system months or years later. This means logs cannot be edited or deleted after the fact — not even by a database administrator.
Architecture: append-only + hash chain + Postgres trigger
1. Append-only table schema:
The audit_logs table has fields: id, tenant_id, sequence_number (monotonically increasing per tenant), actor_email, action, entity_type, changes (jsonb), timestamp, previous_hash, record_hash.
2. Each record hashes the previous one:
On INSERT we compute record_hash = SHA256(prev_record_hash || JSON(this_record_payload)). The previous_hash field stores the hash of the prior record. If anyone modifies a record mid-chain, the next previous_hash will no longer match the new record_hash of the previous record. Detected by a single SQL query.
3. Postgres trigger blocks UPDATE and DELETE:
CREATE FUNCTION raise_audit_immutable() RETURNS trigger AS $$
BEGIN
RAISE EXCEPTION '%s is append-only (TG_OP=%s)', TG_TABLE_NAME, TG_OP;
END $$ LANGUAGE plpgsql;CREATE TRIGGER audit_logs_no_update BEFORE UPDATE OR DELETE ON audit_logs FOR EACH ROW EXECUTE FUNCTION raise_audit_immutable(); ```
This trigger fires at the DATABASE LEVEL — even if the application is compromised, no UPDATE audit_logs or DELETE FROM audit_logs will succeed. Only root access to Postgres + DROP TRIGGER + restoring from backup can change anything — and any such attempt leaves its own trace in system logs.
How to verify chain integrity (one SQL query)
An auditor can run a read-only query at any time:
WITH chain AS (
SELECT sequence_number, record_hash, previous_hash,
LAG(record_hash) OVER (ORDER BY sequence_number) AS prev_actual
FROM audit_logs
)
SELECT COUNT(*) FILTER (WHERE previous_hash IS DISTINCT FROM prev_actual
AND sequence_number > 1) AS broken_links
FROM chain;
If broken_links = 0, the chain is intact. If > 0, tampering occurred somewhere.
Across all current AZTender Audit deployments, broken_links = 0 is automatically re-verified every 24 hours.
Why this matters for the regulator (HP / IFI)
Without a hash chain, logs are just text records. They can be edited via UPDATE audit_logs SET action='confirm' WHERE id=... and the trace will appear «correct» at read time.
With a hash chain, such an edit immediately breaks the next record. The regulator can reconstruct «what really happened» via snapshot or backup and compare the divergence. This provides proof of imperfection, not merely «consent to trust».
For Hesablama Palatası and similar audit institutions, this is critical — audit conclusions must withstand court review, parliamentary inquiry, donor due-diligence. Hash-chain is the technical standard required by the EU AI Act and being developed in PARALLEL in Germany (BSI), Estonia (X-Road), Finland (Suomi.fi).
What the hash chain does NOT guarantee
- ❌ Does not protect against FALSE data being entered at the very start. If an operator deliberately enters
actor_email='audit-team@hp.gov.az'to impersonate someone — that gets written and hashed. Hash-chain protects against POST-HOC modification, not against initial falsification.
- ❌ Does not protect against full database destruction (DROP DATABASE). That is solved by backups.
- ❌ Does not make AI decisions «correct» — it simply ensures that whatever decision AI makes is recorded immutably.
- ✅ Guarantees one thing: once a record is written, no one — including sysadmins, developers, AZTender as the vendor — can change it via the application. Any modification requires root Postgres access and leaves traces in the system.
Methodology
Implementation: Postgres triggers (migration 0019_procurement_notifications + earlier). Hash function: SHA-256 over the JSON-serialized payload. Sequence number is monotonic per tenant (uses Postgres sequence). Storage: tenant-isolated via Row-Level Security policies. Audit-on-audit-log: any UPDATE/DELETE attempt is recorded in Postgres logs (pg_log).
SQL for reproducibility
-- Re-verify the integrity of the audit chain (run any time, read-only):
WITH chain AS (
SELECT
sequence_number,
record_hash,
previous_hash,
LAG(record_hash) OVER (PARTITION BY tenant_id ORDER BY sequence_number) AS prev_actual
FROM audit_logs
)
SELECT
COUNT(*) AS total_records,
COUNT(*) FILTER (WHERE previous_hash IS DISTINCT FROM prev_actual
AND sequence_number > 1) AS broken_links
FROM chain;
-- broken_links MUST equal 0 — any non-zero result means the chain
-- was tampered with at the storage layer (which would require root
-- DB access bypassing the row-level trigger; recovery requires the
-- backup chain).Citation
AZTender Audit Methodology, Hash-chained audit log per EU AI Act Art. 12, accessed 2026-05-24. https://aztender.ai/data-storytelling/hash-chained-audit-log-eu-ai-act
Author
AZTender team
EM Consulting · Baden-Baden
Questions about this article?
Write to the authors — we reply within 1 business day. Request for deep-dive audit of a specific CPV category or organization — same channel.
consulting@elshanmusayev.com