Back to ER Diagram
Audit Logging

Audit Logging System

Comprehensive documentation of the generic audit trail system for tracking all entity changes, user actions, and maintaining compliance records across the ProKure platform.

PostgreSQL
5 Tables
SOX/GDPR Compliant
Schema v2.4

Overview

ProKure implements a comprehensive audit logging system that captures all changes to business entities. This system is essential for compliance (SOX, GDPR), security investigations, and operational debugging. Every CREATE, UPDATE, and DELETE operation is logged with full before/after state preservation.

CREATE

New record inserted

UPDATE

Record modified

DELETE

Record removed

VIEW

Sensitive data accessed

Security & Compliance

Audit logs are immutable once written. They cannot be modified or deleted by regular users. Only database administrators with elevated privileges can manage audit retention policies. All access to audit logs is itself audited.

Audit Architecture

User Action
Application Layer
Capture Context
Database Trigger
config.audit_trails
Old Values (JSONB)
New Values (JSONB)

Dual-Layer Capture

Audit logs are captured at both the application layer (for user context, IP address, session info) and the database layer (via triggers for data integrity). This ensures no changes escape logging, even direct database access.

Audit Tables

config.audit_trails (Primary Audit Log)

  • entity_type - Table/entity being modified (e.g., 'VENDOR', 'PURCHASE_ORDER')
  • entity_id - UUID of the specific record
  • action - CREATE, UPDATE, DELETE, VIEW
  • old_values - JSONB snapshot before change (null for CREATE)
  • new_values - JSONB snapshot after change (null for DELETE)
  • user_id - User who performed the action
  • ip_address - Client IP address
  • user_agent - Browser/client information
  • created_at - Timestamp of the action

admin.user_sessions

  • Links audit entries to user sessions
  • Provides session-level context for security analysis
  • Tracks device, browser, and location information

admin.login_attempts

  • All login attempts (successful and failed)
  • IP address and geolocation tracking
  • Failure reason categorization
  • Used for security monitoring and lockout policies

config.scheduled_jobs & config.job_execution_logs

  • Audit trail for automated/scheduled operations
  • Job start/end times and execution status
  • Error details and retry information
  • Links batch operations to individual audit entries

Audit Entry Structure

Field Type Description
id UUID Primary key, auto-generated
entity_type VARCHAR(50) Entity/table name (VENDOR, PO, PR, ITEM, etc.)
entity_id UUID Primary key of the affected record
action VARCHAR(20) CREATE UPDATE DELETE VIEW
old_values JSONB Record state before change (null for CREATE)
new_values JSONB Record state after change (null for DELETE)
changed_fields TEXT[] Array of field names that changed (for UPDATE)
user_id UUID Reference to admin.users
ip_address INET Client IP address
user_agent TEXT Browser/client user agent string
session_id UUID Reference to user session
request_id UUID Correlation ID for request tracing
created_at TIMESTAMPTZ Timestamp of the action (immutable)

Audit Logging Flow

1

Capture User Context

Before any database operation, the application layer captures user context including user ID, session ID, IP address, and request correlation ID.

// Application sets session variables for audit context
SET LOCAL app.current_user_id = 'user-uuid-here';
SET LOCAL app.current_ip = '192.168.1.100';
SET LOCAL app.request_id = 'correlation-uuid';
SET LOCAL app.session_id = 'session-uuid';
2

Database Trigger Fires

PostgreSQL AFTER triggers capture the old and new row values for INSERT, UPDATE, and DELETE operations.

CREATE OR REPLACE FUNCTION audit_trigger_function()
RETURNS TRIGGER AS $$
DECLARE
    v_old_data JSONB;
    v_new_data JSONB;
    v_changed_fields TEXT[];
BEGIN
    IF (TG_OP = 'UPDATE') THEN
        v_old_data := to_jsonb(OLD);
        v_new_data := to_jsonb(NEW);
        -- Calculate changed fields
        SELECT array_agg(key) INTO v_changed_fields
        FROM jsonb_each(v_old_data) old_kv
        FULL JOIN jsonb_each(v_new_data) new_kv USING (key)
        WHERE old_kv.value IS DISTINCT FROM new_kv.value;
    ELSIF (TG_OP = 'DELETE') THEN
        v_old_data := to_jsonb(OLD);
    ELSIF (TG_OP = 'INSERT') THEN
        v_new_data := to_jsonb(NEW);
    END IF;

    INSERT INTO config.audit_trails (...)
    VALUES (...);

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
3

Audit Record Created

The audit record is inserted with full context. For updates, only changed fields are highlighted while preserving the complete before/after state.

Audit Record Example

  • entity_type: 'VENDOR'
  • entity_id: 'vendor-uuid-123'
  • action: 'UPDATE'
  • old_values: {"status": "PENDING", "credit_limit": 50000}
  • new_values: {"status": "APPROVED", "credit_limit": 100000}
  • changed_fields: ['status', 'credit_limit']
4

Sensitive Data Handling

Certain fields are automatically redacted before storing in audit logs to comply with security policies.

Redacted Fields

password_hash, bank_account_number, gstin, pan, api_keys, tokens - these fields are stored as '[REDACTED]' in audit logs. Original values are never persisted.

Audited Entity Types

User & Access

  • All admin.users
  • All admin.roles
  • All admin.user_roles
  • All admin.permissions
  • View admin.user_permissions

Organization

  • All organization.companies
  • All organization.departments
  • All organization.plants
  • All organization.cost_centers

Vendor

  • All vendor.vendors
  • All vendor.vendor_bank_details
  • All vendor.vendor_approvals
  • All vendor.vendor_blacklist

Procurement

  • All procurement.purchase_requisitions
  • All procurement.purchase_orders
  • All procurement.rfqs
  • All inventory.grns

Querying Audit Logs

Get History of a Specific Record

SELECT
    action,
    old_values,
    new_values,
    changed_fields,
    u.email AS changed_by,
    created_at
FROM config.audit_trails at
LEFT JOIN admin.users u ON at.user_id = u.id
WHERE
    entity_type = 'VENDOR'
    AND entity_id = 'vendor-uuid-123'
ORDER BY created_at DESC;

Find All Changes by a User

SELECT
    entity_type,
    entity_id,
    action,
    changed_fields,
    created_at
FROM config.audit_trails
WHERE
    user_id = 'user-uuid-456'
    AND created_at >= NOW() - INTERVAL '7 days'
ORDER BY created_at DESC;

Track Specific Field Changes

-- Find all status changes for Purchase Orders
SELECT
    entity_id,
    old_values->>'status' AS from_status,
    new_values->>'status' AS to_status,
    u.email AS changed_by,
    created_at
FROM config.audit_trails at
LEFT JOIN admin.users u ON at.user_id = u.id
WHERE
    entity_type = 'PURCHASE_ORDER'
    AND action = 'UPDATE'
    AND 'status' = ANY(changed_fields)
ORDER BY created_at DESC;

Suspicious Activity Detection

-- High-frequency changes from single IP
SELECT
    ip_address,
    COUNT(*) AS change_count,
    array_agg(DISTINCT entity_type) AS affected_entities
FROM config.audit_trails
WHERE
    created_at >= NOW() - INTERVAL '1 hour'
GROUP BY ip_address
HAVING COUNT(*) > 100
ORDER BY change_count DESC;

Retention & Compliance

7 Years
Financial Records
3 Years
General Audit Logs
1 Year
Login Attempts
90 Days
Session Logs

SOX Compliance

Financial transaction audit trails are retained for 7 years. All changes to vendors, purchase orders, invoices, and payments are immutably logged with user attribution.

GDPR Compliance

Personal data in audit logs can be anonymized upon request. VIEW actions for personal data are logged. Data subject access requests can be fulfilled via audit queries.

Archival Strategy

Audit logs older than 1 year are automatically archived to cold storage (partitioned tables). Queries can span both hot and cold partitions transparently. Full-text search is available for archived logs via read replicas.

Best Practices

Always Set User Context

Before any database operation, ensure the application sets the user context via session variables. Anonymous changes indicate a bug in the application layer.

Performance Considerations

Audit triggers add minimal overhead (~2-5ms per operation). For bulk imports, consider temporarily disabling audit triggers and logging the batch operation as a single entry with reference to the import file.

Audit Log Security

Access to audit logs should be restricted to compliance officers and security administrators. Use row-level security to limit visibility based on department or role. Never expose raw audit logs via public APIs.

Enable Easy Investigation

Always include request_id in audit logs to correlate with application logs. This enables end-to-end tracing from user click to database change.