ADR-003: Audit Logging Strategy
Status: Accepted Owner: @bilal @deen Date: 2025-12-06
Context
Envo needs comprehensive audit logging for accountability, debugging, GDPR compliance, and security monitoring (especially Envo support access to customer data).
Requirements
- Log all writes on sensitive tables
- Log READ access for Envo support users only
- Capture user identity, old/new values, timestamp
- Retain logs for 2 years
- Configurable granularity per table
- Abstracted for future externalisation
Decision
Custom audit.log table with triggers, designed for future migration to external audit services (Immuta, Drata).
Design Principles
- Separate schema —
auditschema, notpublic - Abstracted interface — Application uses
AuditLoggerinterface - Configurable per table — Three levels: full, standard, minimal
- Partitioned by month — Efficient 2-year retention via partition drops
- Future-ready — Schema designed for easy export
Schema
CREATE SCHEMA IF NOT EXISTS audit;
CREATE TABLE audit.log (
id UUID PRIMARY KEY,
table_name TEXT NOT NULL,
record_id UUID NOT NULL,
action TEXT NOT NULL CHECK (action IN ('INSERT', 'UPDATE', 'DELETE', 'READ')),
organisation_id UUID,
user_id UUID,
user_role TEXT,
old_values JSONB,
new_values JSONB,
changed_fields TEXT[],
request_id UUID,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);Audit Levels
| Level | What’s Logged | Tables |
|---|---|---|
| Full | All INSERT, UPDATE (with diff), DELETE | issues, user_organisation_access, tenants |
| Standard | INSERT, DELETE, selected UPDATEs | properties, vendors, organisations |
| Minimal | DELETE only | attachments, vendor_ratings |
| None | Nothing | events, audit.log |
Implementation
- Write logging: Database triggers (automatic, no developer discipline needed)
- Read logging: Application layer middleware (for
envo_supportusers only) - Retention: 2 years, monthly partitions dropped via cron/pg_partman
Abstraction Layer
interface AuditLogger {
logWrite(params: WriteAuditParams): Promise<void>
logRead(params: ReadAuditParams): Promise<void>
getAuditHistory(params: QueryParams): Promise<AuditEntry[]>
}Can be backed by PostgreSQL (current) or external service (future).
Future Considerations
- External audit stores: Immuta, Drata, AWS CloudTrail + S3, TimescaleDB
- Hash chaining for tamper evidence (blockchain-like, deferred)
- Field-level encryption for sensitive PII
Consequences
Positive
- Complete audit trail for sensitive tables
- Queryable history for debugging and compliance
- Envo support access is transparent and logged
- Partitioning enables efficient retention management
Negative
- Storage overhead (~10-20% of main data size)
- Trigger overhead on writes (~1-5ms per operation)
- Monthly partition management required