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 |