Compliance & GDPR

Audit logs — what's recorded and where to read it

Use Clineso audit history to understand who changed important patient, prescribing, payment, and practice records and when.

The audit_logs table is append-only. Once written, a row cannot be edited or deleted via any UI or RPC.

What gets audited

Every action that has clinical, financial, or compliance significance:

Clinical decisions

  • case_approved / case_declined / case_more_info
  • prescription_issued / prescription_cancelled
  • case_product_set

Fulfilment

  • order_labeled / order_manifested / order_dispatched
  • order_cancelled
  • inventory_consumed (per batch decrement from FEFO)
  • inventory_adjusted (manual)

Comms

  • comm_event_sent / comm_event_failed
  • comm_template_updated (with old + new wording)

Patient / staff

  • patient_created (auto on payment)
  • user_role_granted / user_role_revoked
  • consent_signed (with wording snapshot)
  • requirement_verified / requirement_rejected

Compliance

  • gdpr_export_generated
  • patient_deletion_requested / _approved / _refused
  • tenant_suspended / tenant_unsuspended / tenant_anonymised
  • dpa_processor_acknowledged

Anti-spam

  • funnel_submission_rejected (with reason + IP)

Schema

Each audit row:

  • id — uuid
  • tenant_id — scope
  • action — slug (e.g. case_approved)
  • actor_user_id — who did it (NULL for system actions)
  • subject_table — table of the subject (e.g. prescription_requests)
  • subject_id — primary key of the subject row
  • metadata — JSONB with action-specific detail
  • created_at — timestamp

How to read it

From the UI

/practice/audit — filterable audit explorer per tenant. Filter by action / actor / subject. Chips link directly to the subject row (e.g. a case_approved chip opens the case detail).

From SQL

select *
from public.audit_logs
where tenant_id = '<tenant-uuid>'
  and action = 'case_approved'
order by created_at desc
limit 100;

Across tenants (platform admin only)

Platform admins can query across tenants. Same query without the tenant filter, but RLS will only return rows their grants permit.

Why it's append-only

UK GDPR + MHRA expect every clinical decision to be attributable, dated, and immutable. The table:

  • Has no UPDATE or DELETE policy
  • Is enforced by RLS — only service_role and SECURITY DEFINER RPCs can insert
  • All inserts go through the write_audit() helper that validates actor_user_id against the current session

What's NOT audited

  • Reads — looking at a record doesn't write to audit_logs. (Read-audit is a separate, much larger problem — out of scope for v1.)
  • UI navigation — clicking around without taking an action doesn't audit.
  • Failed actions — RLS-blocked actions don't write audit (the row never gets created in the first place). Server errors are in observability logs separately.

Retention

Audit logs are retained for the platform's UK clinical-record retention period (currently 10 years from the relevant patient's last interaction). Anonymisation strips actor PII from audit rows but never deletes the row.

When to investigate

Audit logs are your first stop for:

  • "Who approved this case?"
  • "When was this template changed?"
  • "Did the patient actually consent to this version?"
  • "Which batches did this prescription consume?"

If you can't find an answer in audit_logs that should be there, escalate — it's either the wrong query or a regression we need to look at.