Audit Trail System
Complete record of who did what, when, and where—built in for HIPAA and GDPR compliance.
Status — partly shipped. The synchronous local audit-log writer is shipped (Layer 1.1, see local-logging.md). The read API (
GET /v1/audit-logs*, CSV export, single-event detail), the admin viewer UI, the break-glass session table, the patient-impersonation flow, and the asynchronous Telemetry forwarder are all planned — they land in Layer 1.13 (admin viewer), Layer 11 (telemetry forwarding), and Layer 12 (break-glass + impersonation + retention). The endpoints, SQL examples, and break-glass queries below describe the target shape, not what you can call today.
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
- Actor action: Admin updates patient record, specialist signs form, patient books appointment, an AI agent drafts a triage note
- Automatic logging: System records action, actor (principal + actor type), timestamp, IP address, what changed (old value → new value), plus AI provenance when the actor was a model
- Sensitive data masked: Passwords, API keys never logged, form values masked if needed
- Searchable history: View all actions per actor, 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 | 0190af3b-1c2e-7c00-8a4f-b2d9c4e5f100 |
actor_id | Principal who performed the action (FK to principals.id) | 0190af3b-1c2e-7c00-8a4f-b2d9c4e5f200 |
actor_type | Denormalized actor kind ('human' | 'agent' | 'service_account' | 'system') | human |
action | HTTP method mapped to CRUD | CREATE, UPDATE, DELETE |
entity_type | Resource type | appointment, patient, form |
entity_id | Resource ID | 0190af3b-1c2e-7c00-8a4f-b2d9c4e5f300 |
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 |
model_version | AI provenance — model + version when actor_type = 'agent'; NULL otherwise | claude-opus-4-7 |
inputs_hash | AI provenance — SHA-256 of the inputs the model saw (replayability without storing PHI); NULL when not an AI action | \xab12... |
confidence | AI provenance — model-reported confidence in [0, 1]; NULL when not an AI action | 0.873 |
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)actor_id+actor_typecapture who (or what) 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
-- Read access is gated on the `audit_log.view_org` permission, not on a
-- role-string compare. By default, only the admin system role template is
-- granted that permission, so the effective scope is "org admins read their
-- org's audit logs" — but the gate is the permission, which lets a future
-- compliance-officer role read audit logs without inheriting the rest of
-- admin. 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_has_permission('audit_log', 'view_org')
);
-- No INSERT / UPDATE / DELETE policy. With RLS enabled and no policy
-- for an operation, Postgres denies it by default — a "double-deny"
-- combined with the REVOKE below. Production writers run via AdminPool
-- (audit.Recorder) or SECURITY DEFINER (trigger-side audit_log_insert).
REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON audit_log FROM restartix_app;No INSERT / UPDATE / DELETE policies — audit log is append-only by virtue of having no write policy at all (default-deny under RLS) and no write privilege on the AppPool role.
Access Control
Read access is gated on the audit_log.view_org permission. By default this is granted only to the admin system role template; cloned per-org admin roles inherit it. Superadmins read across all orgs by virtue of the AdminPool RLS bypass.
| Role | Permissions |
|---|---|
admin | audit_log.view_org granted by default → reads its org's audit logs |
superadmin | Read all audit logs across all organizations (AdminPool bypass) |
specialist | No access (no audit_log.view_org grant) |
patient | No access |
customer_support | No access |
A custom per-org role can be granted audit_log.view_org to give compliance staff audit-read access without making them full admins.
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 (planned — none of these are wired today)
The shape below is the target API. The handlers are not yet registered in services/api/internal/core/server/routes.go; they land with the Layer 1.13 admin viewer.
GET /v1/audit-logs?start_date={date}&end_date={date}
→ Paginated list of audit events (gated by `audit_log.view_org`)
GET /v1/audit-logs/{id}
→ Single audit event detail
GET /v1/audit-logs/export?start_date={date}&end_date={date}
→ CSV export of audit eventsThe audit_log.view_org permission is seeded today (see reference/rbac-permissions.md) and gates the RLS SELECT policy — handlers will use the same permission as the route gate when they ship.
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 actor (any principal type)
SELECT * FROM audit_log
WHERE actor_id = $1
AND organization_id = $2
ORDER BY created_at DESC;
-- All actions by AI agents (filter on the denormalized actor_type)
SELECT * FROM audit_log
WHERE actor_type = 'agent'
AND organization_id = $1
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
- local-logging.md - Core API synchronous audit middleware (shipped, Layer 1.1) — single source of truth for compliance audit
- compliance.md - HIPAA/GDPR requirements and retention
Related Documentation
- reference/rbac-permissions.md - Permission catalog incl.
audit_log.view_org - reference/rls-policies.md - RLS policies, including
audit_logappend-only enforcement - reference/gdpr-compliance.md - Full GDPR architecture
- architecture/data-model.md -
audit_logtable definition