DEV Community

Beck_Moulton
Beck_Moulton

Posted on

Immutable by Design: Building Tamper-Proof Audit Logs for Health SaaS

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 (like I do), 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 decides to manually run 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.

Today, I want to walk through how we can move beyond "logging" and start architecting immutable ledgers.

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.

If I have a table called audit_logs, and I run this:

UPDATE audit_logs 
SET action = 'authorized_access' 
WHERE user_id = 'rogue_admin' AND action = 'access_denied';
Enter fullscreen mode Exit fullscreen mode

...there is effectively zero evidence that I changed history. The auditor asks, "Is this record accurate?" and you say, "I think so?"

That’s not gonna 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 idea is simple: Every new row in your audit table must contain a hash of the previous row's hash.

The Schema

It might look something like this:

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()
);
Enter fullscreen mode Exit fullscreen mode

The Logic

When you insert a record, you have to query the last record first.

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. 
  // Note: 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]
  );
}
Enter fullscreen mode Exit fullscreen mode

Why this works: If a rogue admin changes a row in the middle of the table (say, ID 500), the hash for ID 500 changes. Because ID 501 contains ID 500's original hash, the chain breaks. You can write a nightly script that traverses the chain to verify integrity.

Small caveat: This does introduce a serialization bottleneck (you can't easily parallelize writes to this table), but for audit logs, that's usually acceptable.

I’ve written a bit more about these kinds of backend patterns and safety checks over on my personal blog at wellally.tech, where I dive deeper into SaaS architecture.

Strategy 2: Dedicated Ledger Databases (QLDB)

If the DIY approach feels like too much maintenance (and honestly, handling the edge cases of hashing race conditions is annoying), you look at Ledger Databases.

Amazon QLDB (Quantum Ledger Database) is the heavy hitter here.

It effectively does the cryptographic chaining for you. It provides a transparent, immutable, and cryptographically verifiable transaction log.

How it changes your architecture

Instead of writing to a Postgres table, you send your critical audit events to QLDB using something like PartiQL (which feels a lot like SQL).

-- PartiQL looks familiar
INSERT INTO AuditTrail
VALUE {
    'userId': '12345',
    'action': 'VIEW_PATIENT_RECORDS',
    'timestamp': '2025-10-22T10:00:00Z'
}
Enter fullscreen mode Exit fullscreen mode

The magic is in the Digest. You can download a "digest" (a cryptographic signature) of your database at a point in time. If an auditor comes in 6 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?

  1. Startups / MVPs: Start with the SQL Chaining method (Strategy 1). It keeps your stack simple (no new infra to manage) and puts you strictly ahead of 90% of competitors who are just logging to text files.
  2. Enterprise / High Compliance: If you are signing BAA (Business Associate Agreements) with hospitals, look at QLDB or immutable tables in Oracle/Azure SQL. The "verifiable" feature saves so much time during audits.

Conclusion

Compliance is often seen as a burden, but in health tech, it’s actually 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.

Top comments (0)