# How Instatic's Audit Logging System Tracks Admin Actions

> Understand Instatic's audit logging system which tracks admin actions using an append-only log and read-only API. View activity streams in the admin dashboard.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-31

---

**Instatic implements an append-only audit log that records every administrative operation in an `audit_events` table, exposes read-only API endpoints gated by fine-grained capabilities, and renders human-readable activity streams in the admin dashboard.**

Instatic is an open-source content management platform that treats administrative accountability as a first-class concern. According to the CoreBunch/Instatic source code, the audit logging system captures every significant state change—user creation, role modification, plugin installation, and AI tool usage—through a centralized repository pattern that guarantees tamper-evident storage.

## Core Repository Architecture

The audit system centers on [`server/repositories/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/audit.ts), which defines the primary interface for creating and retrieving audit records. This file exports the `createAuditEvent` function for writing events and `listAuditEvents` for paginated retrieval.

An **audit event** consists of immutable metadata describing who performed an action, what was done, and when:

- **`actorUserId`** – The UUID of the authenticated user who triggered the operation, or `null` for system-initiated events
- **`action`** – A namespaced string identifier such as `user.create`, `role.update`, or `plugin.install`
- **`targetId`** – The unique identifier of the affected entity (user, site, role, etc.)
- **`ip_address`** – The requestor's IP address for security forensics
- **Timestamp** – Automatically generated by the database upon insertion

The repository ensures **append-only semantics**; once written via `createAuditEvent`, records cannot be modified or deleted through the application layer, creating a cryptographically verifiable trail of administrative activity.

## API Endpoints and Capability Enforcement

Instatic exposes audit data through dedicated REST endpoints protected by its capability system defined in [`src/core/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/capabilities.ts).

### CMS Audit Endpoint

The endpoint `GET /admin/api/cms/audit` returns paginated audit events for general administrative actions. The underlying implementation in [`src/core/persistence/cmsUsers.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/cmsUsers.ts) constructs the request using:

```typescript
apiRequest(`${basePath}/audit`)

```

Access requires the **`audit.read`** capability. Requests without this permission receive a `403 Forbidden` response, as verified in [`src/__tests__/server/auditLogEdges.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/auditLogEdges.test.ts).

### AI-Specific Audit Endpoint

For operations involving AI tooling, `GET /admin/api/ai/audit` (exposed in [`src/admin/ai/api.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/ai/api.ts)) provides filtered access to AI-related events. This endpoint requires the **`ai.audit.read`** capability, allowing administrators to segregate permissions between general CMS activity and AI-specific logs.

## Frontend Integration and Rendering

The admin UI consumes audit data through specialized utilities and React hooks.

### Audit Formatting Utilities

Located in [`src/admin/pages/users/utils/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/users/utils/audit.ts), the formatting layer translates raw database records into human-readable strings:

- **`auditTitle(event, usersById, rolesById)`** – Generates contextual descriptions like "John Doe created user jane.smith" or "System installed plugin analytics"
- **`auditDetails(event, rolesById)`** – Returns supplemental metadata arrays containing IP addresses, previous role states, or configuration diffs

### Dashboard Activity Widget

The [`useDashboardStats.ts`](https://github.com/CoreBunch/Instatic/blob/main/useDashboardStats.ts) hook in `src/admin/pages/dashboard/hooks/` executes a lightweight query against the audit table:

```typescript
SELECT ... FROM audit_events LIMIT 50

```

This powers the "Recent activity" list on the admin dashboard, providing at-a-glance visibility into the last 50 administrative actions without loading the full audit history.

## Security Boundaries and Testing

The audit system enforces strict permission boundaries through capability checks. The test suite in [`src/__tests__/server/auditLogEdges.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/auditLogEdges.test.ts) validates that:

1. Anonymous requests receive `403 Forbidden`
2. Authenticated users without `audit.read` cannot access CMS audit endpoints
3. The API correctly handles HTTP method restrictions (rejecting POST/PUT/DELETE on read-only audit resources)

Formatting logic is independently verified in [`src/__tests__/users/auditFormat.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/users/auditFormat.test.ts), ensuring that `auditTitle` and `auditDetails` correctly handle edge cases like deleted users (where `actorUserId` references no longer exist) or system-initiated events (where `actorUserId` is `null`).

## Summary

- **Immutable storage**: [`server/repositories/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/audit.ts) provides `createAuditEvent` for append-only writes to the `audit_events` table
- **Scoped access**: Two distinct capabilities (`audit.read` and `ai.audit.read`) gate access to `/admin/api/cms/audit` and `/admin/api/ai/audit`
- **Rich UI rendering**: [`src/admin/pages/users/utils/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/users/utils/audit.ts) transforms database rows into human-readable activity descriptions
- **Performance optimized**: The dashboard uses `LIMIT 50` queries via [`useDashboardStats.ts`](https://github.com/CoreBunch/Instatic/blob/main/useDashboardStats.ts) to balance visibility with query speed
- **Test coverage**: Dedicated test suites verify both permission enforcement and formatting logic

## Frequently Asked Questions

### What actions does Instatic log as audit events?

Instatic logs any administrative action that mutates system state, including user creation (`user.create`), role permission changes (`role.update`), plugin installation (`plugin.install`), site publishing, and AI tool invocations. Each action is recorded with the actor's user ID, target entity ID, and source IP address.

### How does Instatic prevent tampering with audit logs?

The system implements an **append-only** architecture at the repository layer. The `createAuditEvent` function in [`server/repositories/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/audit.ts) only supports INSERT operations; no update or delete methods are exposed through the API or repository interfaces. Database-level permissions can further restrict the application user from modifying the `audit_events` table.

### Can regular users view audit logs, or only administrators?

Only users possessing the `audit.read` capability (for CMS actions) or `ai.audit.read` capability (for AI actions) can retrieve audit data. These capabilities are typically granted only to administrator roles. Unauthorized requests receive HTTP 403 responses according to the permission logic in [`src/core/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/capabilities.ts) and validated in [`src/__tests__/server/auditLogEdges.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/auditLogEdges.test.ts).

### How does the admin UI handle audit events from deleted users?

The formatting utilities in [`src/admin/pages/users/utils/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/users/utils/audit.ts) gracefully handle missing actor references. When `actorUserId` is null (system events) or references a deleted user, the `auditTitle` function falls back to displaying "System" or "Unknown user" rather than failing, ensuring the activity stream remains readable even when user accounts are purged.