Skip to content

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

  1. User action: Admin updates patient record, specialist signs form, patient books appointment
  2. Automatic logging: System records action, user, timestamp, IP address, what changed (old value → new value)
  3. Sensitive data masked: Passwords, API keys never logged, form values masked if needed
  4. Searchable history: View all actions per user, per patient, per date range
  5. 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 + analytics

Data 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_log

What Gets Logged

Every mutation request (POST, PUT, PATCH, DELETE) creates an audit entry. GET requests are not audited (read operations).

Captured Fields

FieldDescriptionExample
organization_idTenant context1
user_idActor who performed the action42
actionHTTP method mapped to CRUDCREATE, UPDATE, DELETE
entity_typeResource typeappointment, patient, form
entity_idResource ID12345
changesBefore/after diff (sensitive fields masked){"name": {"old": "John", "new": "Jane"}}
ip_addressClient IP (via CF-Connecting-IP or X-Forwarded-For)203.0.113.42
user_agentClient user agentMozilla/5.0...
request_pathHTTP request pathPOST /v1/appointments
status_codeHTTP response code201, 400, 500
action_contextSpecial context (normal, break_glass, impersonation, gdpr_operation)normal
created_atEvent 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_log table 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:

  • action field captures purpose (CREATE/UPDATE/DELETE)
  • entity_type categorizes data subjects (patient, appointment, form)
  • user_id captures who accessed
  • changes JSONB 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.

sql
-- 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.

sql
-- 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:

  1. Hot (0-12 months): PostgreSQL, queryable via API
  2. Warm (12 months - 6 years): S3 JSONL archives, downloadable by admins
  3. 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

sql
-- 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

RolePermissions
adminRead audit logs for their organization only
superadminRead all audit logs across all organizations
specialistNo access (audit logs are admin-only)
patientNo access
customer_supportNo access

Sensitive Field Masking

Before writing to audit_log, sensitive fields are masked:

go
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

sql
-- 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