Skip to Content
Patient GraphAudit Logging

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 automatically

Manual 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

FilterTypeDescription
actorIdstringWho performed the action
customerIdstringWhose data was accessed
actionstringread, write, delete, export
resourcestringResource type
outcomestringallowed or denied
fromDateISO dateStart of time window
toDateISO dateEnd 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