HelpTopics

Security

Audit logs

Audit logs capture significant lifecycle events — what changed, who changed it, and when. They are critical for regulated environments and for investigating issues after the fact. Each row is immutable once written; the system enforces this both at the model layer and at the Django admin (the change form is read-only).

What gets logged

Two coverage mechanisms run in parallel. (1) Signal-based CRUD on a curated allowlist of high-value models — Appointment, Invoice, Payment, Expense, EmployeeLoan, PayrollRun, EmploymentContract, Role, and PatientProfile — emits create/update/delete rows automatically with before/after diffs limited to the fields that actually changed. (2) Explicit helper calls in services capture named security and state-transition events with rich descriptions: login (with and without 2FA) and logout; loan lifecycle (approve, reject, cancel, disburse with JE number, write-off); loan installment operations (adjust amount with residual handling, waive with JE number); payroll lifecycle (posted with JE number + line count + loan repayment summary, paid with payment method + reference); manual loan-deduction override on a payroll line (set, changed, or cleared); expense edit-with-JE-reversal (with both the reversed and the re-posted JE numbers); accounting period close + reopen; cross-clinic consent grant + revoke. Each entry stores actor, action, entity, before/after, IP, user-agent, and timestamp.

Viewing logs

Platform admins can read every audit row at GET /api/v1/platform/audit-logs/, filterable by date range, actor, action, entity_type, or entity_id. Tenant-level admins see only their own tenant's rows (filtered by service_provider). The detail view shows the full before/after payload — useful for tracing a problem back to its origin or proving non-repudiation in an incident.

Retention

Audit logs are retained for at least 12 months and as long as needed to investigate any open security incident. They are excluded from soft-delete so a deleted business record still has its history visible to administrators. Demo tenants are an exception — their audit rows are hard-deleted along with everything else when the tenant expires.

Known limitations

Two structural caveats remain. (1) Bulk operations that bypass Django signals (e.g., ``QuerySet.update``, ``bulk_create`` without ``post_save`` fan-out) leave no signal-driven row — explicit helper calls cover the most important cases, but bulk-edit endpoints in less-critical areas may go unlogged. (2) Read-access on sensitive records (viewing a progress note, reading a patient's MRN, opening an HR document) is not currently audited — would need a separate request-level mechanism if you need read tracking for PDPL / GDPR demonstrability. Want either of those? Worth a separate conversation; both are scoped pieces of work, not framework changes.