Why Application-Level Isn’t Enough
Most audit log systems prevent modification through application code — controllers that don’t expose update endpoints, models without update methods. This is necessary but insufficient. A compromised application, a rogue employee with database access, or a SQL injection vulnerability can bypass all of it. Immutable enforces append-only at the database engine level.The Trigger
A PostgreSQL trigger namedenforce_append_only_events fires on every UPDATE and DELETE operation against the events table. It raises an exception, aborting the transaction.
- Fires before the operation, preventing it from completing
- Applies to every row individually
- Runs for all database connections using the application connection, including the application itself
- Cannot be bypassed by application code, ORMs, or raw SQL through the application connection
What Happens When Someone Tries
The Archival Exception
Immutable supports data retention policies — events older than your plan’s retention period are archived to compressed storage and then removed from the primary database. This is the only legitimate reason to delete events. The archival process uses a session-local setting to bypass the trigger:SET LOCAL scopes the setting to the current transaction only. It cannot leak to other connections or persist after the transaction commits.
The archival process runs as an automated background job. Before any events are deleted, they are exported as gzipped JSON-L files to object storage (Cloudflare R2) with integrity verification. The deletion only proceeds after the archive upload is confirmed.
”What About Your DBA?”
A common question: if a database administrator has superuser access, can they disable the trigger? The trigger fires for all users using the application’s database connection. Direct database access with elevated privileges — the kind required to modify triggers — is separately audited through infrastructure access logs, and any such access would be detectable through the hash chain (Layer 1) and blockchain anchoring (Layer 2). The point is defense-in-depth:| Scenario | What stops it |
|---|---|
| Application code tries to UPDATE | Trigger blocks it |
| SQL injection tries to DELETE | Trigger blocks it |
| DBA modifies data via application connection | Trigger blocks it |
| DBA disables trigger and modifies data | Hash chain detects tampering, blockchain anchors don’t match |
| DBA rewrites data AND recomputes all hashes | On-chain Merkle roots don’t match, public verification fails |