Audit & Compliance
The audit log is the source of truth for compliance. Every mutation is recorded in an immutable, hash-linked chain.
How Audit Works
Every CREATE, UPDATE, DELETE, or RESTORE operation is written to platform.audit_log via a stored procedure:
CALL platform.audit_insert( schema_path := 'acme/supply-chain/procurement/purchase-order/v1', entity_id := 'PO-001', operation := 'UPDATE', actor := 'ravi.kumar', old_value := '{"status":"draft",...}', new_value := '{"status":"approved",...}', reason := 'Approved per stakeholder request');The stored procedure:
- Assigns incremental
event_id - Computes hash:
SHA256(event_id || timestamp || actor || old_value || new_value || previous_hash) - Writes the row
- Returns the new hash to the caller
This creates an hash-linked chain where each event includes the hash of the previous event.
Audit Log Schema
CREATE TABLE platform.audit_log ( event_id BIGINT PRIMARY KEY, event_hash VARCHAR(64) NOT NULL, -- SHA256 hex prev_hash VARCHAR(64), -- Chain linkage timestamp TIMESTAMPTZ NOT NULL, schema_path TEXT NOT NULL, -- acme/supply-chain/procurement/purchase-order/v1 entity_id TEXT NOT NULL, -- PO-001 operation TEXT NOT NULL, -- CREATE | UPDATE | DELETE | RESTORE actor TEXT NOT NULL, -- ravi.kumar old_value JSONB, -- State before change new_value JSONB, -- State after change reason TEXT, -- Optional; required if schema demands it fail_mode TEXT, -- Auth fail mode (REDIS_UNAVAILABLE_DENIED, etc.) fail_open_allowed BOOLEAN, -- Was failing open permitted?
PRIMARY KEY (event_id), INDEX (timestamp), INDEX (schema_path, entity_id));All columns are immutable (no UPDATEs allowed).
Query Audit Log
Via REST API
curl -G https://api.velocity.acme.com/api/platform/audit \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode 'schema=acme/supply-chain/procurement/purchase-order/v1' \ --data-urlencode 'entity_id=PO-001' \ --data-urlencode 'limit=50'Response:
{ "data": [ { "event_id": 1, "timestamp": "2026-05-19T14:32:00Z", "actor": "ravi.kumar", "operation": "CREATE", "old_value": null, "new_value": {"id":"PO-001","status":"draft",...}, "reason": null }, { "event_id": 2, "timestamp": "2026-05-19T14:35:00Z", "actor": "anita.sharma", "operation": "UPDATE", "old_value": {"status":"draft"}, "new_value": {"status":"approved"}, "reason": "Approved per stakeholder request" } ]}Via CLI
velocity audit list \ --schema acme/supply-chain/procurement/purchase-order/v1 \ --entity-id PO-001Via SQL
SELECT * FROM platform.audit_logWHERE schema_path = 'acme/supply-chain/procurement/purchase-order/v1' AND entity_id = 'PO-001'ORDER BY event_id;Verify Audit Chain Integrity
The audit chain is hash-linked. Verify that no tampering has occurred:
velocity audit verify \ --schema acme/supply-chain/procurement/purchase-order/v1 \ --entity-id PO-001Output:
✓ Audit chain valid (42 events, 0 tampering detected) Last hash: abc123def456... Chain integrity: verifiedIf tampering is detected:
✗ Audit chain invalid (hash mismatch at event 15) Event 14 hash: abc123... (✓) Event 15 hash: def456... expected abc789... Events 15-42 are suspect Action required: Investigate source, contact security teamHow Verification Works
The CLI fetches all audit events and recomputes the hash chain:
prev_hash = ""for event in events: computed_hash = SHA256( str(event.event_id) + str(event.timestamp) + event.actor + json.dumps(event.old_value) + json.dumps(event.new_value) + prev_hash )
if computed_hash != event.event_hash: print(f"Tampering detected at event {event.event_id}") exit(1)
prev_hash = computed_hash
print("Chain valid")Any change to any event’s data (timestamp, actor, values) breaks the chain.
Required Reason Field
Some schemas require a reason for mutations:
apiVersion: velocity.sh/v1kind: SchemaDefinitionspec: audit: requireReason: true reasonPattern: ".{10,}" # Min 10 charsIf you try to UPDATE without providing a reason:
curl -X PATCH https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001 \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"status":"approved"}'Response:
{"code": "AUDIT_REASON_REQUIRED", "message": "Reason is required for mutations on this schema"}Include the reason:
curl -X PATCH https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001 \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "status": "approved", "reason": "Approved per stakeholder request after review" }'Audit of Auth Decisions
Every request logs its authentication and authorization outcome:
SELECT actor, operation, outcome, fail_mode, timestampFROM platform.audit_logWHERE fail_mode IS NOT NULLORDER BY timestamp DESC LIMIT 10;Output:
actor operation outcome fail_mode────────────────────────────────────────────────────────ravi.kumar CREATE success NONEanita.sharma READ denied_rbac NONEbot@acme UPDATE denied_auth_invalid NONE─────────────────────────────────────────────────────system READ success_revocation_denied REDIS_UNAVAILABLE_DENIEDThis helps with:
- Debugging auth failures
- Detecting suspicious patterns (brute force attempts)
- Compliance audits (“Who tried to access this data?”)
Sensitive Field Redaction in Audit
Sensitive fields (marked with sensitivity: pii) are redacted in audit entries:
spec: fields: - name: credit_card type: string sensitivity: pii masking: strategy: partial visibleChars: 4When you view audit log, the credit_card field shows as ****1234 (not plaintext).
{ "new_value": { "id": "PO-001", "credit_card": "****1234", # Not the full card "status": "approved" }}Long-Term Retention & Export
Audit log is partitioned by month for efficient archival:
CREATE TABLE platform.audit_log_2026_05 PARTITION OF platform.audit_log FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');Export to S3 for long-term storage (7 years):
# Automated by lifecycle jobpg_dump -t platform.audit_log_2026_04 velocity | gzip | \ aws s3 cp - s3://velocity-audit-archive/2026-04/audit_log.sql.gzOr query via warm-reader if needed for compliance.
Prometheus Metrics
velocity_audit_events_total{operation="create", schema="..."} 15042velocity_audit_events_total{operation="update", schema="..."} 3201velocity_audit_chain_tampering_detected_total 0velocity_audit_verification_duration_seconds 0.234Alert on tampering:
alert: AuditTamperingDetectedexpr: increase(velocity_audit_chain_tampering_detected_total[5m]) > 0for: 1mseverity: criticalannotations: summary: "Audit chain tampering detected in {{ $labels.schema }}" action: "Quarantine schema, contact security team immediately"Use Cases
Compliance Audit Trail
Export audit log for external auditors:
velocity audit list \ --schema acme/supply-chain/procurement/purchase-order/v1 \ --output json > po_audit_2026_q1.json
# Or export to CSV for Excelvelocity audit list ... --output csv > po_audit_2026_q1.csvInclude in your compliance report.
Dispute Resolution
Customer claims a value was different:
# Query audit for that entityvelocity audit list \ --schema acme/supply-chain/procurement/purchase-order/v1 \ --entity-id PO-001
# Check the timestamps and old_value/new_value# Provide to dispute resolution teamThe hash chain proves you didn’t modify history after the fact.
Incident Investigation
An unauthorized update occurred. Find who and when:
# Query by actor and time rangeSELECT * FROM platform.audit_logWHERE actor = 'unknown-user' AND timestamp > now() - interval '7 days' AND operation IN ('UPDATE', 'DELETE');Or use the CLI:
velocity audit list \ --actor unknown-user \ --since 7d \ --operation UPDATE,DELETECompliance Standards
Audit log design follows:
- SOC 2 Type II: Immutable audit trail, hash integrity
- GDPR: Reason field, actor identification, PII redaction
- HIPAA: Sensitivity tagging, field-level redaction, tamper detection
- PCI-DSS: Cardholder data redaction, access logging
- SOX: Separation of duties (different roles in approval chain)
Best Practices
- Never disable audit: It’s always on, by design.
- Require reasons for sensitive changes: Set
audit.requireReason: trueon high-risk schemas. - Verify chain regularly: Run
velocity audit verifynightly as a health check. - Export monthly: Archive audit logs to S3 for long-term retention.
- Alert on tampering: Set up Prometheus alert on
velocity_audit_chain_tampering_detected_total. - Review suspicious patterns: Monitor for
fail_mode: REDIS_UNAVAILABLE_DENIED(dependency failure). - Redact sensitive fields: Mark PII fields with
sensitivity: piiand configure masking.
Limitations
- Audit log is append-only; you cannot edit or delete audit entries (by design).
- Reason field is optional unless explicitly required by schema policy.
- Warm-tier audit queries (> 90 days) are slower (warm-reader).