Skip to content

Audit overview

PHIPA (Ontario’s Personal Health Information Protection Act) requires that custodians can produce, on request, a record of every access to identifying health information — who, what, when, why. Clinloop satisfies that with two layers:

  1. audit_events — every read or mutation of PHI, every auth event, every authorization denial, and every rate-limit trip.
  2. audit_notifications — every push, SMS, or email send (or blocked send), with hashed recipient identifiers.

Both tables are append-only at the database level via row-level security.

Three accountability layers

LayerMechanismCovers
Decorator@PhiAccess({ resourceType, eventType })Successful 2xx mutations and reads
InterceptorExceptionAuditInterceptorForbiddenException (authz.denied), ThrottlerException (request.rate_limited)
ManualAuditService.record(...)Auth flow, public endpoints, webhook rejections, anything not bound to a controller success

The decorator + interceptor handle the common case automatically. Anywhere a UnauthorizedException short-circuits before a controller returns 2xx (signature mismatch on a webhook, expired OTP), call AuditService.record(...) manually so the rejection is captured.

What goes in metadata

  • ✅ Hashed identifiers — email_hashed: SHA-256(lowercased)
  • ✅ Opaque IDs — attempted_resource_id
  • ✅ Categorical fields — previous_status, reason, tier, row_count, score_bucket
  • ❌ Raw email, phone, SIN, DOB, full name, address — never
  • ❌ Free-text patient notes, medication, diagnosis — never

The audit suite includes a sweep test (12.2) that scans every metadata blob for likely PHI patterns at the end of the run.

Where to look next