Portal Community

Audit Log Table

-- AIExt_AppAuditLog
CREATE TABLE AIExt_AppAuditLog (
    Id            BIGINT          PRIMARY KEY,
    TenantId      NVARCHAR(100)   NOT NULL,
    AppId         NVARCHAR(100)   NOT NULL,
    EntityType    NVARCHAR(100)   NOT NULL,   -- "App", "Page", "Widget", "Layout"
    EntityId      NVARCHAR(200)   NOT NULL,   -- e.g., "lead-detail" for a page
    Action        NVARCHAR(100)   NOT NULL,   -- "Save", "Delete", "Import", "Export"
    ChangedBy     NVARCHAR(100)   NOT NULL,   -- userId from JWT
    ChangedAt     DATETIME2       NOT NULL,
    ClientIp      NVARCHAR(50),              -- IP from request headers
    DiffJson      NVARCHAR(MAX),             -- JSON diff (before/after) for saves
    Notes         NVARCHAR(500)             -- Optional: import source version, etc.
);

What Is Automatically Audited

OperationEntityAction logged
SaveAppAsyncApp"Save" with JSON diff of the app definition
DeleteAppAsyncApp"Delete"
SavePageAsyncPage"Save" with JSON diff of the page config
DeletePageAsyncPage"Delete"
SaveLayoutAsyncLayout"Save" for each page's widget set
ImportAppAsyncApp"Import" with bundle version label in Notes
ExportAppAsyncApp"Export" with requested by userId

Audit in the Service Layer — Automatic for All Writes

// The audit writer is called inside every write method:
private async Task WriteAuditLogAsync(
    string tenantId, string appId, string entityType,
    string entityId, string action, string changedBy,
    string? diffJson = null, string? notes = null)
{
    var entry = new AppAuditLogEntity
    {
        TenantId  = tenantId,
        AppId     = appId,
        EntityType = entityType,
        EntityId  = entityId,
        Action    = action,
        ChangedBy = changedBy,
        ChangedAt = DateTime.UtcNow,
        ClientIp  = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(),
        DiffJson  = diffJson,
        Notes     = notes
    };

    _db.AppAuditLogs.Add(entry);
    await _db.SaveChangesAsync();
}

Reading the Audit Log via the Service API

// IAppStudioService
Task> GetAuditLogAsync(string tenantId, string appId, AuditQuery query);

// AuditQuery supports filtering:
var query = new AuditQuery
{
    From     = DateTime.UtcNow.AddDays(-30),
    To       = DateTime.UtcNow,
    Action   = "Save",           // optional filter by action
    ChangedBy = "user-abc"       // optional filter by user
};

var log = await _service.GetAuditLogAsync(tenantId, appId, query);

Viewing the Audit Log in the Designer

App designers can view the audit log for an app directly in the App Studio Designer:

1
Open the app in App Studio Designer
2
Click the Audit Log icon in the left toolbar (clock icon)
3
Browse the chronological list of changes — who changed what, when
4
Click any "Save" entry to view the JSON diff (before/after comparison)
5
Filter by date range, user, or action type using the filter controls
Audit is non-bypassable Because all data access goes through IAppStudioService, audit logging is guaranteed for every write. It is structurally impossible for a controller to write to an app definition without creating an audit entry — the service method always calls the audit writer before returning.