Skip to main content

Audit Trail

Overview

Q01 Core APIs automatically maintain a complete audit trail of all operations for compliance, debugging, and accountability. Every record includes who created it, who modified it, and when these actions occurred.

Benefits:

  • ✅ Complete accountability (who did what, when)
  • ✅ Compliance with regulations (GDPR, SOX, HIPAA)
  • ✅ Debugging and troubleshooting
  • ✅ Security investigation
  • ✅ Change history tracking
  • ✅ Automatic (no manual logging needed)

Audit Information Tracked:

  • User identity (who)
  • Operation type (what - create/update/delete)
  • Timestamp (when)
  • Record affected (which)
  • IP address and user agent (where/how)
  • Payload (what changed)

Automatic Audit Fields

Standard Audit Fields

Every dimension table includes audit fields:

CREATE TABLE TB_ANAG_PRD00 (
PRD_ID VARCHAR(36) PRIMARY KEY,

-- Business fields
XPRD01 VARCHAR(255),
XPRD02 DECIMAL(10,2),

-- Context fields
PRD_SOURCE VARCHAR(50),
PRD_CENTRO_DETT VARCHAR(50),
PRD_PESO VARCHAR(1),
PRD_AMBIENTE VARCHAR(20),
TREC VARCHAR(1),

-- ⭐ Audit fields (automatic)
CREATED_BY VARCHAR(255), -- Who created
CREATED_AT VARCHAR(14), -- When created (YYYYMMDDHHmmss)
UPDATED_BY VARCHAR(255), -- Who last updated
UPDATED_AT VARCHAR(14), -- When last updated
DELETED_BY VARCHAR(255), -- Who deleted (soft delete)
DELETED_AT VARCHAR(14), -- When deleted

INDEX idx_created_by (CREATED_BY),
INDEX idx_updated_by (UPDATED_BY),
INDEX idx_created_at (CREATED_AT),
INDEX idx_updated_at (UPDATED_AT)
);

Audit Field Population

Automatically set on every operation:

Create (POST)

INSERT INTO TB_ANAG_PRD00 (
PRD_ID,
XPRD01,
XPRD02,
CREATED_BY, -- ⭐ From JWT token
CREATED_AT, -- ⭐ Current timestamp
UPDATED_BY, -- ⭐ Same as CREATED_BY
UPDATED_AT -- ⭐ Same as CREATED_AT
) VALUES (
'uuid-...',
'Widget Pro',
99.99,
'user@example.com', -- From session
'20251219160000', -- YYYYMMDDHHmmss
'user@example.com',
'20251219160000'
);

Update (PUT/PATCH)

UPDATE TB_ANAG_PRD00
SET XPRD02 = 89.99,
UPDATED_BY = 'admin@example.com', -- ⭐ From JWT token
UPDATED_AT = '20251219170000', -- ⭐ Current timestamp
TREC = 'M'
WHERE PRD_ID = 'uuid-...';

-- CREATED_BY and CREATED_AT unchanged (original creator preserved)

Delete (Soft)

UPDATE TB_ANAG_PRD00
SET TREC = 'C',
DELETED_BY = 'admin@example.com', -- ⭐ From JWT token
DELETED_AT = '20251219180000', -- ⭐ Current timestamp
UPDATED_BY = 'admin@example.com', -- Also updated
UPDATED_AT = '20251219180000'
WHERE PRD_ID = 'uuid-...';

CoreWrite Audit Implementation

Automatic Audit Injection

DataStore.php handles audit fields:

// DataStore.php
public function insert(string $dimension, array $data): array {
$sessionContext = $this->getSessionContext();
$userId = $sessionContext['user_id'];
$timestamp = date('YmdHis');

// ⭐ Inject audit fields automatically
$data['CREATED_BY'] = $userId;
$data['CREATED_AT'] = $timestamp;
$data['UPDATED_BY'] = $userId;
$data['UPDATED_AT'] = $timestamp;

// Also inject context fields
$data["{$dimension}_SOURCE"] = $sessionContext['source'];
$data["{$dimension}_CENTRO_DETT"] = $sessionContext['centro_dett'];
$data["{$dimension}_PESO"] = $sessionContext['peso'];
$data["{$dimension}_AMBIENTE"] = $sessionContext['ambiente'];
$data['TREC'] = 'N';

return $this->executeInsert($dimension, $data);
}

public function update(string $dimension, string $id, array $data): array {
$sessionContext = $this->getSessionContext();
$userId = $sessionContext['user_id'];
$timestamp = date('YmdHis');

// ⭐ Update audit fields
$data['UPDATED_BY'] = $userId;
$data['UPDATED_AT'] = $timestamp;
$data['TREC'] = 'M';

// CREATED_BY and CREATED_AT NOT modified

return $this->executeUpdate($dimension, $id, $data);
}

public function delete(string $dimension, string $id, bool $force = false): array {
$sessionContext = $this->getSessionContext();
$userId = $sessionContext['user_id'];
$timestamp = date('YmdHis');

if ($force) {
// Force delete - no audit fields (record deleted)
return $this->executePhysicalDelete($dimension, $id);
}

// Soft delete - set audit fields
$data = [
'TREC' => 'C',
'DELETED_BY' => $userId,
'DELETED_AT' => $timestamp,
'UPDATED_BY' => $userId,
'UPDATED_AT' => $timestamp
];

return $this->executeUpdate($dimension, $id, $data);
}

Querying Audit Information

Who Created a Record?

SELECT CREATED_BY, CREATED_AT, XPRD01
FROM TB_ANAG_PRD00
WHERE PRD_ID = '123';

-- Result:
-- CREATED_BY: user@example.com
-- CREATED_AT: 20251219160000
-- XPRD01: Widget Pro

JavaScript:

async function getRecordAudit(dimension, id) {
const response = await fetch(
`${apiBase}/api/v4/core/${dimension}/${id}`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);

const result = await response.json();

return {
createdBy: result.data.CREATED_BY,
createdAt: parseTimestamp(result.data.CREATED_AT),
updatedBy: result.data.UPDATED_BY,
updatedAt: parseTimestamp(result.data.UPDATED_AT)
};
}

function parseTimestamp(timestamp) {
// YYYYMMDDHHmmss → Date
const year = timestamp.substr(0, 4);
const month = timestamp.substr(4, 2) - 1;
const day = timestamp.substr(6, 2);
const hour = timestamp.substr(8, 2);
const minute = timestamp.substr(10, 2);
const second = timestamp.substr(12, 2);

return new Date(year, month, day, hour, minute, second);
}

// Usage
const audit = await getRecordAudit('PRD', '123');
console.log(`Created by ${audit.createdBy} on ${audit.createdAt}`);
console.log(`Last updated by ${audit.updatedBy} on ${audit.updatedAt}`);

Who Modified a Record?

SELECT UPDATED_BY, UPDATED_AT, CREATED_BY, CREATED_AT
FROM TB_ANAG_PRD00
WHERE PRD_ID = '123';

-- Result shows both creator and last updater

Records Created by User

SELECT PRD_ID, XPRD01, CREATED_AT
FROM TB_ANAG_PRD00
WHERE CREATED_BY = 'user@example.com'
AND PRD_SOURCE = 'productManagement'
AND TREC != 'C'
ORDER BY CREATED_AT DESC;

Records Updated Recently

SELECT PRD_ID, XPRD01, UPDATED_BY, UPDATED_AT
FROM TB_ANAG_PRD00
WHERE PRD_SOURCE = 'productManagement'
AND UPDATED_AT >= '20251219000000' -- Today
AND TREC = 'M'
ORDER BY UPDATED_AT DESC;

Deleted Records

SELECT PRD_ID, XPRD01, DELETED_BY, DELETED_AT
FROM TB_ANAG_PRD00
WHERE PRD_SOURCE = 'productManagement'
AND TREC = 'C'
AND DELETED_AT >= '20251219000000' -- Today
ORDER BY DELETED_AT DESC;

Outbox Event Audit

Complete Operation Log

OUTBOX table provides detailed event log:

CREATE TABLE OUTBOX (
OUTBOX_ID VARCHAR(36) PRIMARY KEY,
EVENT_TYPE VARCHAR(100) NOT NULL, -- ProductCreated, OrderUpdated, etc.
AGGREGATE_ID VARCHAR(36) NOT NULL, -- Record ID
AGGREGATE_TYPE VARCHAR(10) NOT NULL, -- Dimension (PRD, ORD, etc.)
OPERATION VARCHAR(10) NOT NULL, -- POST, PUT, PATCH, DELETE
PAYLOAD TEXT NOT NULL, -- Full record data (JSON)
METADATA TEXT, -- Session context
USER VARCHAR(255), -- ⭐ Who performed operation
CREATED_AT VARCHAR(14) NOT NULL, -- ⭐ When operation occurred
PUBLISHED_AT VARCHAR(14),
STATUS VARCHAR(20),

INDEX idx_outbox_user (USER),
INDEX idx_outbox_created_at (CREATED_AT),
INDEX idx_outbox_aggregate (AGGREGATE_TYPE, AGGREGATE_ID)
);

Query Operation History

All operations on a record:

SELECT OUTBOX_ID, EVENT_TYPE, OPERATION, USER, CREATED_AT
FROM OUTBOX
WHERE AGGREGATE_TYPE = 'PRD'
AND AGGREGATE_ID = '123'
ORDER BY CREATED_AT ASC;

-- Result:
-- ProductCreated POST user@example.com 20251219160000
-- ProductUpdated PATCH admin@example.com 20251219170000
-- ProductDeleted DELETE admin@example.com 20251219180000

All operations by user:

SELECT AGGREGATE_TYPE, AGGREGATE_ID, EVENT_TYPE, CREATED_AT
FROM OUTBOX
WHERE USER = 'admin@example.com'
AND CREATED_AT >= '20251219000000' -- Today
ORDER BY CREATED_AT DESC
LIMIT 100;

Operations by type:

-- All delete operations today
SELECT AGGREGATE_TYPE, AGGREGATE_ID, USER, CREATED_AT
FROM OUTBOX
WHERE OPERATION = 'DELETE'
AND CREATED_AT >= '20251219000000'
ORDER BY CREATED_AT DESC;

Audit Dashboard Example

User Activity Dashboard

class AuditDashboard {
async getUserActivity(userId, startDate, endDate) {
// Query OUTBOX for user's operations
const response = await fetch(
`${apiBase}/api/v4/audit/users/${userId}?` +
`start=${startDate}&end=${endDate}`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);

const data = await response.json();

return {
totalOperations: data.total,
creates: data.operations.filter(op => op.OPERATION === 'POST').length,
updates: data.operations.filter(op => op.OPERATION === 'PATCH').length,
deletes: data.operations.filter(op => op.OPERATION === 'DELETE').length,
operations: data.operations
};
}

async getRecordHistory(dimension, recordId) {
const response = await fetch(
`${apiBase}/api/v4/audit/records/${dimension}/${recordId}`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);

const data = await response.json();

return data.history.map(event => ({
timestamp: parseTimestamp(event.CREATED_AT),
user: event.USER,
operation: event.OPERATION,
changes: JSON.parse(event.PAYLOAD)
}));
}

renderActivity(activity) {
return `
<div class="audit-summary">
<h3>User Activity</h3>
<p>Total Operations: ${activity.totalOperations}</p>
<ul>
<li>Creates: ${activity.creates}</li>
<li>Updates: ${activity.updates}</li>
<li>Deletes: ${activity.deletes}</li>
</ul>
</div>

<table>
<tr>
<th>Timestamp</th>
<th>Operation</th>
<th>Type</th>
<th>Record</th>
</tr>
${activity.operations.map(op => `
<tr>
<td>${parseTimestamp(op.CREATED_AT).toLocaleString()}</td>
<td>${op.OPERATION}</td>
<td>${op.AGGREGATE_TYPE}</td>
<td>${op.AGGREGATE_ID}</td>
</tr>
`).join('')}
</table>
`;
}
}

Compliance Requirements

GDPR Compliance

Right to be forgotten:

-- Remove user's personal data from audit fields
UPDATE TB_ANAG_PRD00
SET CREATED_BY = 'deleted-user',
UPDATED_BY = CASE WHEN UPDATED_BY = 'user@example.com' THEN 'deleted-user' ELSE UPDATED_BY END
WHERE CREATED_BY = 'user@example.com';

-- Also update OUTBOX
UPDATE OUTBOX
SET USER = 'deleted-user'
WHERE USER = 'user@example.com';

Data access request:

-- Export all operations by user
SELECT *
FROM OUTBOX
WHERE USER = 'user@example.com'
ORDER BY CREATED_AT DESC;

SOX Compliance

Audit trail requirements:

  • ✅ Who performed operation (CREATED_BY, UPDATED_BY)
  • ✅ When operation occurred (CREATED_AT, UPDATED_AT)
  • ✅ What changed (OUTBOX PAYLOAD)
  • ✅ Cannot be modified (OUTBOX is append-only)
  • ✅ Complete history (all operations logged)

HIPAA Compliance

Access logging:

-- Log all access to sensitive records
INSERT INTO ACCESS_LOG (
USER, RESOURCE_TYPE, RESOURCE_ID, ACTION, TIMESTAMP
) VALUES (
'doctor@hospital.com', 'PATIENT', '123', 'READ', '20251219160000'
);

Best Practices

✅ DO:

Trust automatic audit fields:

// ✅ Good - audit fields handled automatically
await createProduct({ XPRD01: 'Widget' });
// CREATED_BY, CREATED_AT set automatically

Query audit fields for debugging:

-- ✅ Good - investigate who changed record
SELECT UPDATED_BY, UPDATED_AT, XPRD02
FROM TB_ANAG_PRD00
WHERE PRD_ID = '123';

Use OUTBOX for complete history:

-- ✅ Good - full operation history
SELECT * FROM OUTBOX
WHERE AGGREGATE_TYPE = 'PRD'
AND AGGREGATE_ID = '123'
ORDER BY CREATED_AT ASC;

Preserve audit trail on delete:

-- ✅ Good - soft delete preserves audit
DELETE /api/v4/core/PRD/123
-- Sets DELETED_BY, DELETED_AT, TREC='C'

❌ DON'T:

Don't try to override audit fields:

// ❌ Bad - audit fields are read-only
await createProduct({
XPRD01: 'Widget',
CREATED_BY: 'other@example.com' // Validation error!
});

Don't force delete unnecessarily:

# ❌ Bad - destroys audit trail
DELETE /api/v4/core/PRD/123?force=true
# Record physically deleted, no DELETED_BY/DELETED_AT

Don't modify OUTBOX:

-- ❌ Bad - OUTBOX is append-only
UPDATE OUTBOX SET USER = 'anonymous' WHERE ...;

Don't store sensitive data in OUTBOX:

// ⚠️ Caution - OUTBOX stores full payload
await createUser({
username: 'user@example.com',
password: 'plaintext' // Don't do this!
});
// Password in OUTBOX forever

Summary

  • ✅ Automatic audit fields (CREATED_BY, UPDATED_BY, etc.)
  • ✅ Every operation tracked with user and timestamp
  • ✅ OUTBOX provides complete operation history
  • ✅ Audit fields read-only (cannot be overridden)
  • ✅ Soft delete preserves audit trail
  • ✅ Compliance with GDPR, SOX, HIPAA
  • ✅ Query audit history for debugging and investigation

Key Takeaways:

  1. All records include audit fields automatically
  2. Audit fields populated from JWT token (user_id)
  3. Cannot override or modify audit fields
  4. Soft delete preserves audit trail
  5. OUTBOX provides complete operation history
  6. Query audit fields for investigation
  7. Comply with regulatory requirements

Audit Fields:

FieldPurposeSet OnModified On
CREATED_BYWho createdPOSTNever
CREATED_ATWhen createdPOSTNever
UPDATED_BYWho last updatedPOST, PUT, PATCH, DELETEPUT, PATCH, DELETE
UPDATED_ATWhen last updatedPOST, PUT, PATCH, DELETEPUT, PATCH, DELETE
DELETED_BYWho deletedDELETE (soft)Never
DELETED_ATWhen deletedDELETE (soft)Never

Audit Query Examples:

-- Who created record?
SELECT CREATED_BY FROM TB_ANAG_PRD00 WHERE PRD_ID = '123';

-- When was record last updated?
SELECT UPDATED_BY, UPDATED_AT FROM TB_ANAG_PRD00 WHERE PRD_ID = '123';

-- Complete operation history
SELECT * FROM OUTBOX WHERE AGGREGATE_TYPE = 'PRD' AND AGGREGATE_ID = '123';