Audit Logging
Every access to patient data in the Patient Graph is logged for HIPAA compliance. The audit system records who accessed what data, when, and whether access was granted or denied.
How It Works
Audit entries are automatically created by the RBAC middleware on every API request. Both successful and denied access attempts are logged.
Audit Entry Structure
interface AuditEntry {
actorId: string; // Clerk user ID of the requester
customerId: string; // Patient whose data was accessed
role: RbacRole; // Role used for the access check
action: RbacAction; // read, write, delete, or export
resource: RbacResource; // Resource type (profile, lab_results, etc.)
outcome: string; // 'allowed' or 'denied'
metadata: Record<string, unknown>;
}Automatic Logging
The logAccess() middleware writes entries to the rbac_logs table after each RBAC check:
// In route middleware
app.use('/profiles/*', requireAccess('profile', 'read'));
// → RBAC check runs → audit entry created automaticallyManual Audit Logging
For operations that need custom audit context, use persistAuditEntry():
import { persistAuditEntry } from '@loop/shared/rbac';
await persistAuditEntry(db, {
actorId: user.id,
customerId: targetId,
role: 'admin',
action: 'export',
resource: 'lab_results',
outcome: 'allowed',
metadata: { exportFormat: 'csv', recordCount: 150 },
});Wrapped Operations
The withAudit() helper combines an operation with automatic audit logging:
import { withAudit } from '@loop/shared/rbac';
const result = await withAudit(db, {
actorId: user.id,
customerId: patientId,
role: userRole,
action: 'read',
resource: 'lab_results',
}, async () => {
return await repos.labs.findByCustomerId(patientId);
});Querying Audit Logs
Via API
Only admin and staff roles can query audit logs:
# All access by a specific staff member
curl "https://patient-graph.loop.health/audit-logs?actorId=user_staff_123&limit=50" \
-H "Authorization: Bearer $ADMIN_JWT"
# All access to a specific patient's data
curl "https://patient-graph.loop.health/audit-logs?customerId=prof_abc123" \
-H "Authorization: Bearer $ADMIN_JWT"
# All denied access attempts
curl "https://patient-graph.loop.health/audit-logs?outcome=denied" \
-H "Authorization: Bearer $ADMIN_JWT"
# Access during a specific time window
curl "https://patient-graph.loop.health/audit-logs?fromDate=2024-06-01&toDate=2024-06-15" \
-H "Authorization: Bearer $ADMIN_JWT"Filters
| Filter | Type | Description |
|---|---|---|
actorId | string | Who performed the action |
customerId | string | Whose data was accessed |
action | string | read, write, delete, export |
resource | string | Resource type |
outcome | string | allowed or denied |
fromDate | ISO date | Start of time window |
toDate | ISO date | End of time window |
Retention
Audit logs are retained for 7 years per HIPAA requirements. A Trigger.dev job runs nightly to enforce retention policies:
- Active logs: Retained indefinitely within the 7-year window
- Expired logs: Soft-deleted after 7 years, hard-deleted after 30 additional days
- Legal holds: Certain logs can be flagged for legal hold, preventing deletion
Compliance
The audit logging system supports HIPAA compliance requirements:
- Access tracking — Every PHI access is recorded with actor, resource, and timestamp
- Denied access — Failed access attempts are logged for security monitoring
- Tamper resistance — Audit logs are append-only with no update/delete API
- Retention — 7-year retention with automated cleanup
- Export — Admin users can export audit logs for compliance reporting