DocsPlatformAudit Log

Audit Log

Immutable audit trail captured at the PostgreSQL level. Every INSERT, UPDATE, and DELETE on every entity table is recorded automatically. No code required, no way to bypass.


How it works

The Core installs an AFTER INSERT OR UPDATE OR DELETE trigger on every entity table when it is created. The trigger calls rootcx_system.audit_trigger_fn(), which writes a structured entry to rootcx_system.audit_log within the same transaction.

If the data changes, the audit entry is written. If the transaction rolls back, the audit entry rolls back with it. No phantom records.

The trigger function runs with SECURITY DEFINER, meaning it executes with the privileges of the function owner regardless of who triggers it. This ensures audit entries cannot be blocked by row-level security or permission issues.


What gets captured

Every mutation on every entity table:

Operation old_record new_record
INSERT NULL Full row as JSONB
UPDATE Full row before change Full row after change
DELETE Full row before deletion NULL

The record_id is extracted from the row's id column (the primary key).

Attribution (who did this, on whose authority)

Every mutation also captures the actor chain:

Scenario actor_uid delegator_uid trigger_ref
User creates a record via API user UUID NULL api
Agent creates a record (tool call) agent UUID invoking user UUID agent_tool
Agent acts via cron/hook agent UUID trigger owner UUID agent_tool

This answers the 3 questions during an incident: who performed the action (actor), on whose authority (delegator), and how (trigger).


Log schema

CREATE TABLE rootcx_system.audit_log (
    id            BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    table_schema  TEXT NOT NULL,
    table_name    TEXT NOT NULL,
    record_id     TEXT,
    operation     TEXT NOT NULL,
    old_record    JSONB,
    new_record    JSONB,
    actor_uid     UUID,
    delegator_uid UUID,
    trigger_ref   TEXT,
    changed_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);
Field Type Description
id BIGINT Auto-incrementing primary key. Ordered by time.
table_schema TEXT PostgreSQL schema name (matches the app ID).
table_name TEXT Entity name that was mutated.
record_id TEXT Primary key (id) of the affected row.
operation TEXT INSERT, UPDATE, or DELETE.
old_record JSONB Full row before the mutation. NULL on INSERT.
new_record JSONB Full row after the mutation. NULL on DELETE.
actor_uid UUID Who performed the action. For direct API calls: the user. For agent actions: the agent.
delegator_uid UUID On whose authority. NULL for direct user actions. For agent actions: the human who delegated.
trigger_ref TEXT How the action was triggered: api (HTTP request), agent_tool (agent tool call), or a trigger identifier.
changed_at TIMESTAMPTZ Timestamp of the mutation.

Indexes

Index Columns Purpose
idx_audit_id_desc id DESC Fast cursor-based pagination
idx_audit_ts changed_at DESC Time-based queries
idx_audit_table (table_schema, table_name) Filter by app and entity

Query endpoint

GET /api/v1/audit

Requires authentication. Returns an array of audit entries.

Filter parameters

Parameter Alias Type Description
table_schema app_id TEXT Filter by app (schema name).
table_name entity TEXT Filter by entity (table name).
record_id TEXT Filter by specific record ID.
operation TEXT Filter by operation type: INSERT, UPDATE, or DELETE.
changed_at TIMESTAMPTZ Filter by timestamp.

Operator suffixes

Append suffixes to filter parameters for advanced filtering:

Suffix SQL operator Example
(none) or __eq = ?operation=UPDATE
__neq != ?operation__neq=DELETE
__contains ILIKE '%..%' ?table_name__contains=contact
__gte >= ?changed_at__gte=2024-12-01
__lte <= ?changed_at__lte=2024-12-31

Pagination

Parameter Default Description
limit 100 Max entries to return. Range: 1-1000.
before Cursor: return entries with id < before (for pagination).
order_by id Column to sort by (must be a known column).
order desc Sort direction: asc or desc.

Example

GET /api/v1/audit?app_id=crm&entity=contacts&operation=UPDATE&limit=50
[
  {
    "id": 1001,
    "table_schema": "crm",
    "table_name": "contacts",
    "record_id": "a1b2c3d4-...",
    "operation": "UPDATE",
    "old_record": { "id": "a1b2c3d4-...", "status": "lead" },
    "new_record": { "id": "a1b2c3d4-...", "status": "customer" },
    "changed_at": "2024-12-01T10:30:00+00:00"
  }
]

Trigger installation

Triggers are installed automatically when a table is created during manifest deployment. The Core calls rootcx_system.enable_tracking('{schema}.{table}'::regclass) which creates an AFTER INSERT OR UPDATE OR DELETE FOR EACH ROW trigger.

You can also manage tracking manually:

-- Enable tracking on a table
SELECT rootcx_system.enable_tracking('crm.contacts'::regclass);

-- Disable tracking on a table
SELECT rootcx_system.disable_tracking('crm.contacts'::regclass);

Immutability

Audit log entries cannot be deleted, modified, or truncated via the API. There is no DELETE or UPDATE endpoint for the audit log. The only way to remove entries is direct database access (which itself would be captured by any PostgreSQL audit extension at the infrastructure level).


Retention

Audit logs are not automatically purged. The table grows indefinitely. For high-volume production deployments, consider partitioning rootcx_system.audit_log by month or implementing a retention policy at the infrastructure level.


Technical details

Property Value
Table rootcx_system.audit_log
Primary key id (BIGINT, auto-incrementing identity)
Trigger type AFTER INSERT OR UPDATE OR DELETE FOR EACH ROW
Trigger function rootcx_system.audit_trigger_fn() (PL/pgSQL, SECURITY DEFINER)
Transactional Yes. Audit entry rolls back if the data mutation rolls back.
Snapshot format Full row as JSONB (to_jsonb(OLD), to_jsonb(NEW))
Response format snake_case (raw PostgreSQL column names)

API endpoints summary

Method Path Auth Description
GET /api/v1/audit Yes Query audit log with filters and pagination