Audit log — every approval forever
The Audit page is the legal artefact your external auditor and the regulator will subpoena. Every action that touched the maker-checker engine is here, with both signatures, both OTP timestamps, the payload diff and any attachments.

What is recorded per event
- Request ID (DCR-YYYY-NNNNN)
- Rule code (e.g. LOAN_DISBURSE)
- Maker UID, name, branch, IP address, browser fingerprint
- Checker UID, name, branch, IP address, browser fingerprint
- Maker submission timestamp
- Checker action timestamp
- Action type (approved / rejected / expired / re-assigned / bypassed)
- OTP dispatch + verification timestamps and channel
- Full payload (JSON snapshot of what was written)
- Before-image diff (for write rules — what was the prior value)
- Attachments (board resolutions, evidence files)
- Reason text (for rejects and bypasses)
Viewing the log
MFI > Two-person rule > Audit. Default columns: Time, Rule, Maker, Checker, Action, Amount, Reference. Filters: This week, This month, This quarter (default for auditors), By rule, By checker, By branch, Bypasses only, Rejections only.
Click any row to open the detail form. The form has four tabs: Summary, Payload (JSON viewer), Diff (before/after), Attachments. All read-only.
Searching the log
Full-text search is enabled on the Reason field and the rule code. Common queries auditors run:
- All write-offs above KES 500,000 in Q1 2026 — filter by rule LOAN_WRITEOFF_BIG, date range
- All bypasses in 2026 — filter Action = bypassed
- All approvals by a specific checker — group-by Checker name
- All rejections with reason containing "M-Pesa" — full-text on Reason
- Average approval time per branch — pivot Submitted vs Approved, branch group
Export for external audit
Action menu > Export to CSV / Excel / PDF (signed). PDF export is digitally signed with the tenant's certificate and stamped with the auditor's name (entered at export time). PDFs are auto-archived in the Documents app under MFI > Audit Exports.
The CSV export includes every field shown on screen plus the JSON payload. ~30MB for a year of a medium-sized MFI.
Retention policy
Live records: forever in the active database. After 7 years, records move to mfi.audit.archive (compressed, slower query, still accessible). SASRA requires 7 years minimum; ODPC requires destruction of PII after 10 years unless legally required to retain.
Tamper-evidence (hash chain)
Each audit row stores a SHA-256 hash of (this row's contents || previous row's hash). The chain forms a blockchain-style ledger — modifying any historical row invalidates the chain from that point forward. The current hash head is published daily to a public OpenTimestamps anchor.
Verification: MFI > Two-person rule > Audit > Verify chain. Re-computes every hash; reports OK or the row where the chain broke. Daily cron runs this and emails the CRO if it ever fails.
Bypass review cadence
Internal audit should pull every bypass weekly and verify legitimacy:
- Filter: Action = bypassed, date this week
- For each row: open the underlying loan/savings/GL action
- Confirm the action makes sense (member, amount, channel)
- Confirm the bypass attachment is genuine (AT status screenshot, not a Photoshop)
- Tick the Audit row's Reviewed checkbox — adds an internal-audit signature
- If anything looks off — Audit row > Action > Raise incident → creates an SOC 2 incident
Worked scenarios
Scenario — SASRA inspector pulls 90 days of audit log
| Character | Role |
|---|---|
| Florence Achieng | CFO |
| Peter Otieno | Internal Auditor |
| Aisha Hassan | SASRA Inspector |
Timeline
- Day 1, 09:00: Aisha requests "all loan disbursements above KES 200,000 in Q1, with dual-control evidence" (—)
- Day 1, 09:30: Peter logs in as Auditor (read-only group) (Audit menu accessible, no Edit options visible)
- Day 1, 09:35: Filter: Rule = LOAN_DISBURSE, date 01/01–31/03, amount > 200,000 (247 rows returned)
- Day 1, 09:40: Action > Export PDF (signed) — Peter enters inspector name (PDF generated, 312 pages, tenant cert signature embedded)
- Day 1, 10:00: Inspector samples 20 rows — clicks through to see payload + diff (All show maker + checker + OTP timestamp)
- Day 1, 11:30: Inspector requests "verify chain integrity for the whole year" (Peter runs Verify chain)
- Day 1, 11:35: Chain verifies OK across 14,892 rows (OpenTimestamps anchors match)
- Day 2, 09:00: Inspector accepts; finding: "Dual-control evidence comprehensive and tamper-evident" (—)
Outcome — MFI passes the dual-control portion of the inspection with commendation. No remediation required.
Reference
Audit action types
| Action | Money moved? | Signatures captured | Triggers SOC 2 incident? |
|---|---|---|---|
| approved | Yes | Maker + Checker (OTP) | No |
| rejected | No | Maker + Checker | No |
| expired | No | Maker only | No |
| reassigned | No | Maker + Original checker + New checker | No |
| bypassed | Yes | Maker + Checker + 2x bypass authorisers | Yes (always reviewed) |
| rule_change | No | Maker + Checker (OTP) | No |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Verify chain reports "broken at row 8432" | Either a malicious tamper, a botched DB restore, or someone ran DELETE manually. | Stop all writes immediately. Lift the prior backup. Compare with OpenTimestamps anchor for the broken date — restore from before the break. Raise a P0 SOC 2 incident. |
| Audit log empty for the last week | Either no dual-control events (improbable) or the audit cron is broken. | Settings > Scheduled Actions > MFI: Audit chain commit — Active = True. Run manually once. |
| Cannot Export PDF — button greyed out | User is not in Auditor or CFO group. | CRO adds the user to the appropriate group. Note: Auditor group is read-only — they can view and export but cannot mutate. |
| Audit log loading slowly (60s+ for a query) | Years of records without partitioning. | DBA: enable PostgreSQL partitioning on mfi.dual.control.audit by created_at month. Already wired into the model — just create partitions for new months via mfi.audit.partition cron. |

