Platform Architecture Modules Developers Security Integrations Articles
Articles / Permissions & Security
Permissions & Security 3 min read Published Mar 11, 2026

Why Audit Logs Matter in Business-Critical Software

Designing immutable, tamper-evident audit logs: data schemas, asynchronous ingestion, forensic accountability, and compliance architecture.

A
Alegor Architecture Team
Platform Engineering & Architecture

In business-critical software, a standard operational database only represents the current snapshot of reality. When an invoice total changes from €1,000 to €10,000, or a user’s bank account IBAN is redirected, the database overwrites the previous row.

Without an immutable, append-only Audit Log, organizations cannot answer essential operational questions:

  • Who made this mutation?
  • What was the exact state before the change?
  • From what IP address and through what API key did the change originate?
  • Did this modification trigger as a side-effect of an automated background job or human intervention?
+------------------+          +----------------------------------------------------+
|  DOMAIN MUTATION | -------> |               ASYNC AUDIT QUEUE                    |
| (Invoice #10492) |          |  [Actor, IP, Context, Before/After Diff, HMAC Sig] |
+------------------+          +----------------------------------------------------+
                                                        |
                                                        v
                                      +------------------------------------+
                                      |      APPEND-ONLY STORAGE ENGINE    |
                                      |  (Postgres Table with No UPDATE /  |
                                      |   No DELETE Grants + Cold S3 Sync) |
                                      +------------------------------------+

Anatomy of a Robust Audit Record#

A production audit record must capture full context without leaking unredacted secrets or credit card numbers:

{
  "audit_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "tenant_id": "tenant_eu_west_884",
  "actor": {
    "type": "USER",
    "id": "usr_78192",
    "email": "finance.lead@enterprise.com",
    "impersonator_id": null
  },
  "action": "invoice.updated",
  "target": {
    "type": "Invoice",
    "id": "inv_9921"
  },
  "context": {
    "ip": "194.237.112.45",
    "user_agent": "Mozilla/5.0...",
    "request_id": "req_88129034",
    "route": "PATCH /api/v1/invoices/inv_9921"
  },
  "changes": {
    "status": { "old": "draft", "new": "issued" },
    "discount_cents": { "old": 0, "new": 5000 },
    "tax_id": { "old": null, "new": "SE559009105301" }
  },
  "signature": "hmac_sha256_hash_verifying_record_integrity",
  "created_at": "2026-03-26T14:22:01.442Z"
}

Performance: Asynchronous Non-Blocking Ingestion#

Audit logging must never degrade user-facing request latency. Writing a synchronous log entry with large JSON diffs during a web request adds database lock overhead.

In the Alegor Audit Module, auditing operates via lightweight lifecycle observers that publish audit payloads to Redis queues. Dedicated background workers batch-insert audit entries into PostgreSQL tables utilizing high-throughput COPY or multi-row INSERT operations.

// Automatic model mutation observer with sensitive data redaction
namespace App\Domain\Audit\Observers;

use App\Domain\Audit\Jobs\IngestAuditEntryJob;
use Illuminate\Database\Eloquent\Model;

class AuditableObserver
{
    public function updated(Model $model): void
    {
        $dirty = $model->getDirty();
        $original = array_intersect_key($model->getRawOriginal(), $dirty);

        // Strip passwords, API tokens, and private keys
        $redactedKeys = $model->getAuditHidden();
        $cleanedOld = array_diff_key($original, array_flip($redactedKeys));
        $cleanedNew = array_diff_key($dirty, array_flip($redactedKeys));

        if (empty($cleanedNew)) {
            return;
        }

        IngestAuditEntryJob::dispatch([
            'tenant_id' => $model->tenant_id ?? null,
            'actor_id' => auth()->id(),
            'action' => strtolower(class_basename($model)).'.updated',
            'target_type' => get_class($model),
            'target_id' => $model->getKey(),
            'changes' => [
                'old' => $cleanedOld,
                'new' => $cleanedNew,
            ],
            'ip' => request()->ip(),
            'created_at' => now()->toIso8601String(),
        ])->onQueue('audit-telemetry');
    }
}

Immutability & Database Grants#

To guarantee non-repudiation during compliance audits:

  • The application’s standard database user possesses INSERT and SELECT privileges on the audit_logs table, but zero UPDATE or DELETE privileges.
  • Database triggers or cryptographic checksum chains (Merkle trees) prevent silent tampering by database administrators.
  • Nightly export routines stream cold audit records to write-once-read-many (WORM) S3 Glacier buckets.

Explore the Alegor Audit & Compliance Module or read about Designing Software That Can Evolve for Ten Years.

Building a business-critical system?

Evaluate how Alegor can serve as your foundation.

Explore Platform →