Audit Trail System
Complete record of who did what, when, and where—built in for HIPAA and GDPR compliance.
What this enables
Compliance proof: Prove to regulators that form X was signed by person Y on date Z—audit logs don't lie, every change timestamped.
Security investigation: Patient data accessed without authorization? Search audit logs: who viewed what form, when, from which IP address.
Accidental deletion recovery: Patient record deleted by mistake? Audit log shows what data was deleted, when, by who—can recover.
Regulatory eDiscovery: During compliance audits, run reports on access patterns—"Show me all edits to patient X's forms in Q1 2025".
Role enforcement: Verify that only admins can change pricing, only specialists can complete clinical forms, only patients can sign their own consents.
How it works
- User action: Admin updates patient record, specialist signs form, patient books appointment
- Automatic logging: System records action, user, timestamp, IP address, what changed (old value → new value)
- Sensitive data masked: Passwords, API keys never logged, form values masked if needed
- Searchable history: View all actions per user, per patient, per date range
- HIPAA safe: Logs stored locally (not just forwarded), never lost even if cloud service is down
Technical Reference
Overview
Restartix The Core API implements a comprehensive audit trail system designed to meet HIPAA and GDPR compliance requirements. The architecture uses a dual-write pattern: all audit events are written synchronously to the local audit_log table (HIPAA safety net), then forwarded asynchronously to the Telemetry service for enrichment and analytics.
Why Dual-Write?
Local-first guarantee: If the Telemetry service is restarting during a deployment (even for 5 seconds), mutations during that window would be unaudited if we only wrote to Telemetry. The local audit_log table ensures zero audit event loss (HIPAA requirement).
Telemetry enrichment: Telemetry adds geo enrichment, actor hashing (SHA-256 for patient privacy), security threat detection, and ClickHouse aggregations for compliance dashboards. These capabilities don't belong in the platform's request path.
Architecture
Every mutation request → Audit Middleware
1. Synchronous INSERT into local audit_log (~1ms) ← HIPAA guarantee, never lost
2. Async forward to Telemetry (private network, best-effort) ← enrichment + analyticsData Flow
Client Request (POST/PUT/PATCH/DELETE)
↓
Audit Middleware captures request metadata
↓
Handler processes business logic
↓
Response captured (status code)
↓
1. SYNC: Write to the Core API's audit_log table
├─ Blocking operation (~1ms)
├─ Transaction guarantees durability
└─ ERROR level log if this fails (critical)
↓
2. ASYNC: Forward to Telemetry service
├─ Non-blocking channel enqueue
├─ Batched delivery (50 events or 2 seconds)
├─ Best-effort (drops if queue full)
└─ Already safe in local audit_logWhat Gets Logged
Every mutation request (POST, PUT, PATCH, DELETE) creates an audit entry. GET requests are not audited (read operations).
Captured Fields
| Field | Description | Example |
|---|---|---|
organization_id | Tenant context | 1 |
user_id | Actor who performed the action | 42 |
action | HTTP method mapped to CRUD | CREATE, UPDATE, DELETE |
entity_type | Resource type | appointment, patient, form |
entity_id | Resource ID | 12345 |
changes | Before/after diff (sensitive fields masked) | {"name": {"old": "John", "new": "Jane"}} |
ip_address | Client IP (via CF-Connecting-IP or X-Forwarded-For) | 203.0.113.42 |
user_agent | Client user agent | Mozilla/5.0... |
request_path | HTTP request path | POST /v1/appointments |
status_code | HTTP response code | 201, 400, 500 |
action_context | Special context (normal, break_glass, impersonation, gdpr_operation) | normal |
created_at | Event timestamp (UTC) | 2026-02-13T10:00:00Z |
Sensitive Data Masking
Never logged:
- Passwords
- API keys
- Session tokens
- Authorization headers
- Cookie values
Pattern-based masking replaces sensitive fields with [REDACTED] before logging.
Compliance Requirements
HIPAA (164.312(b)) - Audit Controls
HIPAA requires recording and examining activity in systems containing PHI.
Requirements:
- Log all data access and mutations
- Tamper-evident audit trail
- 6-year minimum retention
- Audit logs must not be modifiable after creation
Our implementation:
- All mutations logged (via middleware)
audit_logtable has no UPDATE or DELETE RLS policies (append-only)- 6-year retention (hot PostgreSQL for 12 months, warm S3 archives for 5 years)
- Immutable after creation (no UPDATE statements in codebase)
GDPR (Article 30) - Records of Processing Activities
GDPR requires maintaining records of all processing activities, including access logs.
Requirements:
- Purpose of processing
- Categories of data subjects
- Categories of personal data
- Who accessed what data
- Data retention periods
Our implementation:
actionfield captures purpose (CREATE/UPDATE/DELETE)entity_typecategorizes data subjects (patient, appointment, form)user_idcaptures who accessedchangesJSONB captures what was processed- Retention documented in compliance.md
Special Contexts
Break-Glass Access
Emergency access sessions are tagged with action_context = 'break_glass' and include a break_glass_id linking to the break_glass_log table.
-- Query all actions during an emergency access session
SELECT * FROM audit_log
WHERE break_glass_id = $1
ORDER BY created_at;Patient Impersonation
Admin impersonation sessions are tagged with action_context = 'impersonation' and include an impersonation_id.
-- Query all actions during an impersonation session
SELECT * FROM audit_log
WHERE impersonation_id = $1
ORDER BY created_at;GDPR Operations
GDPR data subject requests (export, erasure, rectification) are tagged with action_context = 'gdpr_operation'.
Retention Strategy
See compliance.md for full retention policy.
Three-tier retention:
- Hot (0-12 months): PostgreSQL, queryable via API
- Warm (12 months - 6 years): S3 JSONL archives, downloadable by admins
- Delete (after 6 years): Purged per HIPAA requirements
Special retention:
- Break-glass logs: Never deleted (permanent records)
- GDPR operation logs: 7 years (legal requirement)
Security
Row-Level Security
-- Admins can read audit logs for their organization
-- Superadmins bypass RLS via AdminPool (owner role)
CREATE POLICY audit_select ON audit_log FOR SELECT USING (
organization_id = current_app_org_id() AND current_app_role() = 'admin'
);
-- System can write (no user restrictions)
CREATE POLICY audit_insert ON audit_log FOR INSERT WITH CHECK (TRUE);No UPDATE or DELETE policies — audit log is append-only.
Access Control
| Role | Permissions |
|---|---|
admin | Read audit logs for their organization only |
superadmin | Read all audit logs across all organizations |
specialist | No access (audit logs are admin-only) |
patient | No access |
customer_support | No access |
Sensitive Field Masking
Before writing to audit_log, sensitive fields are masked:
var sensitivePatterns = []string{
"password", "secret", "token", "api_key", "apikey",
"authorization", "cookie", "session",
}Any field matching these patterns is replaced with [REDACTED].
Performance Considerations
Why Synchronous Local Write?
Synchronous writes add ~1ms latency to mutation requests. This is acceptable because:
- HIPAA requires guaranteed audit logging (async drops events on overflow)
- Modern PostgreSQL INSERT is fast (~1ms for single row)
- Write is isolated from RLS (uses connection pool, not RLS-scoped connection)
- Single INSERT statement (no joins or complex queries)
Connection Pool Impact
Audit writes use the connection pool directly (not the RLS-configured connection) because audit_log has no RLS policies. This avoids holding the per-request RLS connection for the audit write.
Telemetry Forwarding Performance
Asynchronous forwarding to Telemetry:
- Non-blocking channel enqueue (submicrosecond)
- Batched delivery (50 events or 2 seconds, whichever comes first)
- Bounded queue (500 events) — drops events if full (already safe in local
audit_log) - No retries (Telemetry can backfill from the Core API's table if needed)
Querying Audit Logs
API Endpoints
GET /v1/audit-logs?organization_id={id}&start_date={date}&end_date={date}
→ Paginated list of audit events (admin-only)
GET /v1/audit-logs/{id}
→ Single audit event detail (admin-only)
GET /v1/audit-logs/export?organization_id={id}&start_date={date}&end_date={date}
→ CSV export of audit events (admin-only)SQL Queries
-- All mutations on a specific patient
SELECT * FROM audit_log
WHERE entity_type = 'patient'
AND entity_id = $1
AND organization_id = $2
ORDER BY created_at DESC;
-- All actions by a specific user
SELECT * FROM audit_log
WHERE user_id = $1
AND organization_id = $2
ORDER BY created_at DESC;
-- All failed requests (4xx, 5xx)
SELECT * FROM audit_log
WHERE status_code >= 400
AND organization_id = $1
ORDER BY created_at DESC;
-- All break-glass sessions
SELECT bg.*, COUNT(al.id) AS action_count
FROM break_glass_log bg
LEFT JOIN audit_log al ON al.break_glass_id = bg.id
WHERE bg.organization_id = $1
GROUP BY bg.id
ORDER BY bg.created_at DESC;Implementation Files
- schema.sql - Database schema, indexes, RLS policies
- local-logging.md - Core API synchronous audit middleware
- telemetry-forwarding.md - Asynchronous Telemetry forwarding
- compliance.md - HIPAA/GDPR requirements and retention