Chapter 8.2 — Audit Logs
1. Purpose
Audit logs are the platform's tamper-resistant record of "who did what, when, from where". This chapter defines the audit log model — events captured, structure, retention, query patterns, security controls — that lets auditors, security teams, and partner admins reconstruct any state in the platform's history.
2. Why it matters
- Audit attestation — external auditors test the audit trail; weak trails cost audit scope.
- Security forensics — when something goes wrong (or is suspected to), the trail is how it gets investigated.
- Compliance — SOC 2 CC4, ISO 27001 A.12.4, PCI DSS Req. 10 all require detailed activity logging.
- Disputes — "I didn't do that" needs an authoritative answer.
3. Scope of logging
travoBooks logs every business-meaningful event, not raw HTTP requests. This is a deliberate choice — HTTP-level logs go to general observability (Chapter 8.x), but audit logs capture intent.
3.1 Events logged
Categories: - Authentication: login success / failure, MFA challenge, password change, logout, session revoke, token issued / revoked. - Authorization: permission grant / revoke, role assignment / removal. - Customer master: create, update, credit-limit change, sanctions screening result, deactivate. - Supplier master: create, update, bank-detail change (extra-sensitive), commission-rule change. - Booking: each state transition (DRAFT → HELD → ISSUED → ...), price quote, supplier call success/failure. - Ticketing: issuance, void, refund, exchange. - Memos: receipt, dispute, settlement. - Financial: every JE posted (header + summary; lines accessible via JE id), invoice generated, payment received, payout sent. - Reconciliation: every recon run, match decision, exception resolution. - Tax: rule change, return generation, payment. - Period: soft-close, close, reopen, lock. - System config: partner config changes, COA changes, role definition changes. - Imports / Exports: file upload, file commit, report generation, report download. - API: PAT created / revoked, webhook subscription created / updated.
3.2 Events NOT in audit log
These go to general observability logs, not audit logs: - Search queries (operational; high volume) - Read-only views (high volume; "who looked at X" is a Phase 2 PII-access log) - Cache hits / misses - Internal worker heartbeats
A separate PII access log (Phase 2) covers "who viewed sensitive customer data" — distinct from audit logs which cover writes / decisions.
4. Schema
audit_logs (
audit_log_id BIGINT PK AUTO_INCREMENT,
partner_id BIGINT NOT NULL,
event_type VARCHAR(100) NOT NULL, -- 'booking.issued', 'customer.credit_limit_changed'
event_severity ENUM('INFO','NOTICE','WARN','ALERT'),
actor_type ENUM('user','system','api_token','external'),
actor_id BIGINT NULL,
actor_display_name VARCHAR(255),
target_entity_type VARCHAR(50) NULL,
target_entity_id BIGINT NULL,
request_id CHAR(26) NULL,
ip_address VARBINARY(16) NULL,
user_agent VARCHAR(500) NULL,
geolocation VARCHAR(100) NULL,
event_payload JSON NOT NULL,
diff JSON NULL, -- before/after for updates
reason VARCHAR(500) NULL, -- user-supplied reason for sensitive ops
parent_audit_log_id BIGINT NULL, -- nested actions
created_at DATETIME(6) NOT NULL,
hash_chain_prev CHAR(64), -- previous row's hash
hash_chain_self CHAR(64), -- this row's hash
KEY idx_partner_time (partner_id, created_at),
KEY idx_event_type (event_type),
KEY idx_actor (actor_type, actor_id),
KEY idx_target (target_entity_type, target_entity_id),
KEY idx_request (request_id)
) ENGINE=InnoDB;
5. Tamper-evidence: hash chaining
Each audit row's hash_chain_self = SHA256(prev_hash || canonical(row_without_chain_fields)). The chain anchors at a periodically-signed root hash:
- Every 1 hour, the latest hash is signed with the platform's audit-attestation private key.
- The signature is published to an append-only attestation log.
- An attacker who alters a past row breaks every subsequent hash, detected by chain verification.
This does not make rows immutable; it makes tampering detectable. Combined with DB row-level access control (audit logs are write-once via stored-procedure interface), the practical guarantee is strong.
6. Common event payloads
booking.issued
{
"booking_id": 1009287,
"booking_ref": "TVB-2026-000123",
"customer_id": 4521,
"supplier_id": 17,
"amount": "65400.00",
"currency": "BDT",
"primary_ticket_number": "176-2400000123",
"service_date_start": "2026-05-28"
}
customer.credit_limit_changed
{
"customer_id": 4521,
"before": {"credit_limit": "500000.00"},
"after": {"credit_limit": "750000.00"},
"reason": "Q2 increase approved; ref MEMO-2026-Q2-CL-018"
}
supplier.bank_details_changed
{
"supplier_id": 17,
"before": {"account_number_masked": "****1234", "bank_swift": "ABCDXXXX"},
"after": {"account_number_masked": "****5678", "bank_swift": "EFGHXXXX"},
"reason": "Supplier-confirmed change",
"vendor_fraud_hold_until": "2026-05-26T19:15:00+06:00"
}
period.closed
{
"period_year": 2026,
"period_month": 5,
"trial_balance_balanced": true,
"snapshot_path": "s3://travobooks-snapshots/partner-42/2026-05/tb.json",
"initiator_user_id": 18,
"approver_user_id": 7
}
7. Reason capture
For sensitive operations, the platform requires a free-text reason field:
- Credit-limit change above threshold
- Supplier bank-detail change
- Refund without standard quote
- Period reopen
- Permission grant for sensitive role
- Manual journal entry
This is stored both in the audit log and in the underlying entity history. The UI prompts the user; the API requires it in the request payload.
8. Query patterns
Who changed customer credit limit recently?
SELECT * FROM audit_logs
WHERE event_type = 'customer.credit_limit_changed'
AND target_entity_type = 'customer'
AND target_entity_id = 4521
AND created_at >= NOW() - INTERVAL 90 DAY
ORDER BY created_at DESC;
Reconstruct user's session activity
SELECT * FROM audit_logs
WHERE actor_type = 'user' AND actor_id = 18
AND created_at BETWEEN '2026-05-25' AND '2026-05-26'
ORDER BY created_at;
All entity changes from a single request
SELECT * FROM audit_logs
WHERE request_id = '01HZ7P8X3R5...'
ORDER BY created_at;
Period audit pack
SELECT * FROM audit_logs
WHERE partner_id = 42
AND created_at BETWEEN '2026-05-01' AND '2026-05-31 23:59:59.999999'
ORDER BY created_at;
9. UI surfacing
| View | Purpose |
|---|---|
| Entity history | On every business entity (customer, booking, JE), a "History" tab shows recent audit events for that entity |
| User activity | Per-user dashboard — recent actions, sessions, IPs |
| System events | Partner-admin view — recent admin/configuration changes |
| Search audit log | Filter by event-type, actor, target, time range |
Access controlled by audit.read.partner permission (limited audience).
10. Retention
| Retention class | Period |
|---|---|
| Financial-impact events | 10 years (or local statute) |
| Other business events | 7 years |
| Authentication events | 7 years |
After in-DB retention period: archived to S3-Glacier with full chain attestation; restorable on demand for legal hold.
11. Privacy
Audit logs may contain PII (e.g., a customer creation event includes the customer's data). Treatment: - Subject to the same data-protection controls as primary data (Chapter 12.5). - Right-to-erasure honoured by anonymising the event_payload while preserving the audit-log row itself (regulatory exemption — audit trails are commonly excluded from erasure). - Access logged (meta-audit) — viewing the audit log is itself audited.
12. Performance
Audit-log writes are on the hot path of every business operation; the platform optimises: - Write path: append-only, no triggers, single-table insert with prepared statement. - Indexes tuned for the query patterns above. - High-volume events (e.g., per-segment status updates) batched into a single audit row with an array payload. - Older partitions moved to compressed cold tablespace (Phase 4 tiered storage).
13. Anomaly detection (Phase 2)
ML-based anomaly detection runs over the audit stream: - Bursts of failed logins → trigger account-level rate limit. - Off-hours bank-detail changes → escalation. - New IP for sensitive operations → step-up auth. - Permission changes for ex-employees → alert.
14. Common pitfalls
- ⚠️ Logging high-volume read events into the audit log. Use the access log instead; otherwise the audit log becomes unsearchable.
- ⚠️ Putting raw passwords or tokens in payloads. Strip before logging.
- ⚠️ Updating audit log rows after creation. Don't. The chain detects it.
- ⚠️ Deleting old audit rows for storage. Use the archival path; never DELETE directly.
- ⚠️ Skipping reason capture. Without reasons, audit becomes "who" without "why" — only half the story.
- 🔒 Audit-log access without itself being audited. Meta-audit must be on.