Audit Trails and Automatic Retention
Automate logging and lifecycle transitions so compliance is enforced "by default," not by convention.
Audit Trails
Security & Compliance Events to Log
| Event | Description |
|---|---|
view | Document accessed/viewed |
download | Document downloaded |
create | Document created |
update | Document modified |
delete | Document deleted |
status_change | Lifecycle state transition |
permission_change | Access permissions modified |
retention_change | Retention policy modified |
hold_applied | Legal hold placed |
hold_removed | Legal hold released |
Required Fields Per Event
| Field | Description |
|---|---|
event_id | Unique event identifier |
timestamp | When event occurred (UTC) |
user_id | Who performed action |
session_id | User session identifier |
action | Event type |
doc_id | Affected document |
old_value | Previous value (if applicable) |
new_value | New value (if applicable) |
source_ip | Client IP address |
device | Device identifier |
decision | Allow/deny result |
Storage Requirements
| Requirement | Description |
|---|---|
| Append-only | Cannot modify existing logs |
| Tamper-evident | Cryptographic verification |
| WORM/log-structured | Write-once storage |
| Blockchain-style | Optional for HIPAA/GDPR |
| Retained | Logs retained per regulatory requirements |
Schema Reference
Data Structure
field_name:
type: string
required: true
description: Field description
example: "example_value"
API Reference
Endpoint Overview
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/resource | List resources |
| POST | /api/v1/resource | Create resource |
| PUT | /api/v1/resource/:id | Update resource |
| DELETE | /api/v1/resource/:id | Delete resource |
Retention Policies
Central Retention Catalog
CREATE TABLE retention_policies (
retention_category TEXT PRIMARY KEY,
description TEXT NOT NULL,
duration_years INT NOT NULL,
trigger_event TEXT NOT NULL, -- "effective_date", "account_closure"
legal_basis TEXT NOT NULL -- "HIPAA", "SEC 17a-4"
);
Example Categories
| Category | Duration | Trigger | Legal Basis |
|---|---|---|---|
| HIPAA-6Y | 6 years | effective_date | HIPAA 45 CFR 164.316(b)(2)(i) |
| SEC-7Y | 7 years | effective_date | SEC Rule 17a-4 |
| FINRA-6Y | 6 years | effective_date | FINRA 4511 |
| HR-7Y | 7 years | termination_date | State employment law |
Document Linking
Each document links to a retention category:
retention_category: "HIPAA-6Y"
effective_date: "2025-01-01"
# System computes: retain_until = 2031-01-01
Automated Retention Jobs
Background Jobs
- Scan for documents past
retain_until - Check not on legal hold
- Action:
- Move to archive
- Anonymize
- Destroy (with approval if required)
Legal Holds
| Field | Description |
|---|---|
legal_hold | Boolean flag |
hold_reason | Why hold was placed |
hold_owner | Who placed the hold |
hold_date | When placed |
Legal hold prevents deletion regardless of retention expiry.
UI for Hold Management
- View all documents on hold
- Place/release holds
- Track hold history
- Generate hold reports
Automation Patterns
Event-Driven Triggers
| Event | Action |
|---|---|
status_change → approved | Set effective_date, compute review_due_date, recalc retain_until |
retention_category changed | Recompute retain_until |
review_due_date approaching | Generate review task |
Integration with Tasking
- Generate review tasks ahead of
review_due_date - Alert document owners
- Escalate overdue reviews
Retention Update SQL Trigger
CREATE OR REPLACE FUNCTION recompute_retain_until()
RETURNS TRIGGER AS $$
DECLARE
pol retention_policies;
years INT;
BEGIN
SELECT * INTO pol
FROM retention_policies
WHERE retention_category = NEW.retention_category;
IF NOT FOUND THEN
RAISE EXCEPTION 'Unknown retention_category: %', NEW.retention_category;
END IF;
years := GREATEST(pol.duration_years, pol.min_years);
IF NEW.effective_date IS NULL THEN
RAISE EXCEPTION 'effective_date required for retention computation';
END IF;
NEW.retain_until := (NEW.effective_date + (years || ' years')::INTERVAL)::DATE;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Destruction Queue
CREATE TABLE destruction_queue (
id BIGSERIAL PRIMARY KEY,
doc_id UUID NOT NULL,
queued_at TIMESTAMPTZ NOT NULL DEFAULT now(),
processed_at TIMESTAMPTZ,
status TEXT NOT NULL DEFAULT 'queued',
reason TEXT NOT NULL
);
-- Daily job: enqueue eligible docs
INSERT INTO destruction_queue (doc_id, reason)
SELECT dm.doc_id, 'retention_expired'
FROM document_metadata dm
WHERE dm.legal_hold = FALSE
AND dm.retain_until <= CURRENT_DATE
AND NOT EXISTS (
SELECT 1 FROM destruction_queue dq
WHERE dq.doc_id = dm.doc_id
AND dq.status IN ('queued','processing','completed')
);