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.

Approval requests
Audit trail — every request with state and outcome.
At a glance — Records are append-only at the database level. There is no Delete button. There is no Edit button. There is no admin escape. The records survive even module uninstall (they migrate to mfi.audit.archive).

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.

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.

Warning — Do NOT enable database vacuum on mfi.dual.control.audit or mfi.audit.archive. The retention cron handles legitimate archival; vacuum on these tables is a red flag in any audit.

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:

  1. Filter: Action = bypassed, date this week
  2. For each row: open the underlying loan/savings/GL action
  3. Confirm the action makes sense (member, amount, channel)
  4. Confirm the bypass attachment is genuine (AT status screenshot, not a Photoshop)
  5. Tick the Audit row's Reviewed checkbox — adds an internal-audit signature
  6. 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

Setting: SASRA prudential inspection lands at Mwananchi MFI for Q1 2026. Inspector requests 90 days of dual-control evidence.

CharacterRole
Florence AchiengCFO
Peter OtienoInternal Auditor
Aisha HassanSASRA Inspector

Timeline

  1. Day 1, 09:00: Aisha requests "all loan disbursements above KES 200,000 in Q1, with dual-control evidence" (—)
  2. Day 1, 09:30: Peter logs in as Auditor (read-only group) (Audit menu accessible, no Edit options visible)
  3. Day 1, 09:35: Filter: Rule = LOAN_DISBURSE, date 01/01–31/03, amount > 200,000 (247 rows returned)
  4. Day 1, 09:40: Action > Export PDF (signed) — Peter enters inspector name (PDF generated, 312 pages, tenant cert signature embedded)
  5. Day 1, 10:00: Inspector samples 20 rows — clicks through to see payload + diff (All show maker + checker + OTP timestamp)
  6. Day 1, 11:30: Inspector requests "verify chain integrity for the whole year" (Peter runs Verify chain)
  7. Day 1, 11:35: Chain verifies OK across 14,892 rows (OpenTimestamps anchors match)
  8. 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

ActionMoney moved?Signatures capturedTriggers SOC 2 incident?
approvedYesMaker + Checker (OTP)No
rejectedNoMaker + CheckerNo
expiredNoMaker onlyNo
reassignedNoMaker + Original checker + New checkerNo
bypassedYesMaker + Checker + 2x bypass authorisersYes (always reviewed)
rule_changeNoMaker + Checker (OTP)No

Troubleshooting

SymptomLikely causeFix
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 weekEither 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 outUser 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.

See also

Was this page helpful?