Immutable by Design: Building Tamper-Proof Audit Logs for Health SaaS
Source: Dev.to
Let’s be real for a second: nobody wakes up in the morning excited to build an audit logging system. We’d all rather be shipping new features, optimizing React renders, or frankly, just sleeping in.
But if you work in HealthTech, you know the drill. HIPAA, GDPR, SOC2… they aren’t just acronyms; they are the gatekeepers of your business.
The old‑school way of “logging”? Dumping JSON to a text file or an S3 bucket. The problem? rm -rf /var/logs. Or even worse: the rogue admin scenario—what happens if a database administrator with root access runs an UPDATE command to cover their tracks? If your audit log is just a standard SQL table, you can’t prove to an auditor that the history wasn’t rewritten.
The Problem: Mutable Databases
In a standard Postgres or MySQL setup, data is mutable by design. That’s a feature, not a bug. But for an audit trail in a regulated environment, mutability is the enemy.
UPDATE audit_logs
SET action = 'authorized_access'
WHERE user_id = 'rogue_admin' AND action = 'access_denied';
There is effectively zero evidence that the history was changed. The auditor asks, “Is this record accurate?” and you say, “I think so?” That’s not going to fly in healthcare.
Strategy 1: Cryptographic Chaining (The “DIY Blockchain”)
If you aren’t ready to adopt a specialized database, you can implement a “chain” within your SQL database. This is basically how a blockchain works, but centralized.
The Schema
CREATE TABLE strict_audit_log (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
action VARCHAR(255),
payload JSONB,
prev_hash VARCHAR(64), -- The hash of the previous row
curr_hash VARCHAR(64), -- The hash of this row (including prev_hash!)
created_at TIMESTAMP DEFAULT NOW()
);
The Logic
async function logAction(user, action, payload) {
// 1. Get the last record
const lastLog = await db.query(
'SELECT curr_hash FROM strict_audit_log ORDER BY id DESC LIMIT 1'
);
const prevHash = lastLog ? lastLog.curr_hash : '0000000000'; // Genesis block style
// 2. Create the hash for the new record
// We use SHA-256 here. Ensure your serialization is deterministic!
const dataString = `${user.id}${action}${JSON.stringify(payload)}${prevHash}`;
const currHash = crypto.createHash('sha256')
.update(dataString)
.digest('hex');
// 3. Insert
await db.query(
`INSERT INTO strict_audit_log (user_id, action, payload, prev_hash, curr_hash)
VALUES ($1, $2, $3, $4, $5)`,
[user.id, action, payload, prevHash, currHash]
);
}
Why this works: If a rogue admin changes a row in the middle of the table (e.g., ID 500), the hash for ID 500 changes. Because ID 501 contains the original hash of ID 500, the chain breaks. You can run a nightly script that traverses the chain to verify integrity.
Small caveat: This introduces a serialization bottleneck (writes can’t be parallelized), but for audit logs that’s usually acceptable.
I’ve written more about these backend patterns on my personal blog at wellally.tech.
Strategy 2: Dedicated Ledger Databases (QLDB)
If the DIY approach feels like too much maintenance (and handling hashing race conditions is annoying), look at Ledger Databases. Amazon QLDB (Quantum Ledger Database) is the heavy hitter here. It performs the cryptographic chaining for you and provides a transparent, immutable, and verifiable transaction log.
How it changes your architecture
Instead of writing to a Postgres table, you send critical audit events to QLDB using PartiQL (SQL‑like syntax).
-- PartiQL looks familiar
INSERT INTO AuditTrail
VALUE {
'userId': '12345',
'action': 'VIEW_PATIENT_RECORDS',
'timestamp': '2025-10-22T10:00:00Z'
}
The magic is in the Digest. You can download a cryptographic signature of your database at a point in time. If an auditor comes in months later, you can use that digest to mathematically prove that nobody at your company—not even the CTO—altered the data.
Which one should you choose?
- Startups / MVPs: Start with the SQL Chaining method (Strategy 1). It keeps your stack simple (no new infra to manage) and puts you ahead of most competitors who are still logging to text files.
- Enterprise / High Compliance: If you are signing BAA (Business Associate Agreements) with hospitals, consider QLDB or immutable tables in Oracle/Azure SQL. The verifiable feature saves a lot of time during audits.
Conclusion
Compliance is often seen as a burden, but in health tech it’s a feature of trust. Being able to look a client in the eye and say, “Even I cannot change this data,” is a powerful sales tool.
Whether you roll your own hash‑chain or spin up a Ledger DB, the goal is the same: immutability by design.