# Instatic Audit Log Append-Only Architecture: Immutable Activity Tracking for CMS Operations

> Discover Instatic's audit log append-only architecture for immutable activity tracking. Explore how this tamper-resistant system creates a verifiable historical trail for CMS operations.

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

---

**Instatic implements a tamper-resistant audit system by writing every administrative action to an immutable `audit_events` table that strictly accepts inserts while prohibiting updates or deletes, creating a complete and verifiable historical trail.**

The CoreBunch/Instatic codebase provides a robust **append-only audit log architecture** designed to record every meaningful administrative action across the platform. This architectural pattern guarantees that once an event is persisted, it cannot be altered or removed, establishing a trustworthy record of authentication events, content mutations, role changes, and plugin lifecycle operations.

## Storage Layer and Schema Design

At the foundation of the audit system lies a single database table named `audit_events`. According to the migration definitions and schema documentation, this table operates under strict immutability constraints—rows are never updated or deleted, and every new action results in a fresh insertion.

The table stores event metadata in a flat JSON structure called `metadata_json`. To maintain query efficiency and UI rendering simplicity, the **AuditMetadata** type constrains this data to `Record<string, string | number | boolean | null | string[]>`, explicitly disallowing nested objects.

All writes to this table are funneled through a centralized repository layer defined in [`server/repositories/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/audit.ts). This module exports the closed-union **AuditActionSchema**, which type-checks all permissible action strings, and provides two primary helpers: `createAuditEvent` for writes and `listAuditEvents` for reads.

## Enforcing the Append-Only Guarantee

The codebase enforces immutability through multiple layers to prevent accidental or malicious tampering.

First, the schema documentation explicitly states that "events are never updated or deleted." Second, the `createAuditEvent` function serves as the **sole writer** to the `audit_events` table; a comprehensive review of the repository confirms no `UPDATE` or `DELETE` statements target this table anywhere else in the application. Third, the test suite in [`src/__tests__/server/auditLogEdges.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/auditLogEdges.test.ts) verifies that only insertion operations occur, providing automated regression protection against mutating queries.

## HTTP API and Access Control

Read access to the audit trail is exposed through a dedicated, read-only endpoint implemented in [`server/handlers/cms/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/audit.ts). The handler responds to `GET /admin/api/cms/audit` and guards access via the `audit.read` capability.

```typescript
export async function handleAuditRoutes(req: Request, db: DbClient) {
  const url = new URL(req.url)
  if (url.pathname !== `${CMS_API_PREFIX}/audit`) return null

  const actor = await requireCapability(req, db, 'audit.read')
  if (actor instanceof Response) return actor
  if (req.method !== 'GET') return methodNotAllowed()

  return jsonResponse({ events: await listAuditEvents(db) })
}

```

This implementation ensures that consumption of audit data remains separate from the write pipeline, reinforcing the append-only nature by exposing no modification interface.

## UI Integration and Event Rendering

The audit log surfaces in the admin dashboard through two primary components. The **ActivityWidget** ([`src/admin/pages/dashboard/widgets/ActivityWidget.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/dashboard/widgets/ActivityWidget.tsx)) displays recent events on the main dashboard, while the dedicated Users → Audit tab utilizes formatting utilities from [`src/admin/pages/users/utils/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/users/utils/audit.ts).

The UI layer transforms raw database records into human-readable descriptions using pattern matching on the action type:

```typescript
export function formatAuditTitle(event: AuditEvent): string {
  switch (event.action) {
    case 'login.success': return `Login succeeded for ${event.actorLabel}`
    case 'data.row.publish': return `Published ${event.metadata.slug}`
    default: return `Event ${event.action}`
  }
}

```

By consuming the flat metadata structure defined in the repository layer, the UI avoids complex parsing logic while maintaining clear audit trail visibility.

## Writing Audit Events in Practice

When an administrative operation mutates persisted state—such as publishing a row or installing a plugin—the surrounding handler invokes `createAuditEvent` immediately after the state change succeeds. This proximity to the source of truth ensures audit failures surface naturally rather than being treated as best-effort side effects.

The following TypeScript snippet demonstrates recording a publish event:

```typescript
import { createAuditEvent } from '../../server/repositories/audit'

await createAuditEvent(db, {
  action: 'data.row.publish',
  actorUserId: user.id,
  targetId: row.id,
  targetType: 'row',
  metadata: {
    tableId: row.tableId,
    tableSlug: 'posts',
    slug: row.slug,
    fromStatus: 'draft',
    toStatus: 'published',
  },
  ipAddress: clientIp(req),
  userAgent: req.headers.get('user-agent'),
})

```

Retrieving the latest 100 events requires a simple repository call:

```typescript
import { listAuditEvents } from '../../server/repositories/audit'

const recent = await listAuditEvents(db)   // default limit = 100
// recent is an array of AuditEvent objects ready for UI rendering

```

## Summary

- **Immutable Storage**: The `audit_events` table in CoreBunch/Instatic accepts only `INSERT` operations, guaranteeing a tamper-resistant historical record.
- **Centralized Repository**: All writes flow through [`server/repositories/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/audit.ts), which enforces type safety via `AuditActionSchema` and prevents mutations through a single `createAuditEvent` interface.
- **Read-Only API**: The `GET /admin/api/cms/audit` endpoint in [`server/handlers/cms/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/audit.ts) provides secure, capability-gated access without exposing modification vectors.
- **Flat Metadata**: Events carry structured metadata as simple key-value pairs, optimizing query performance and UI rendering in components like [`ActivityWidget.tsx`](https://github.com/CoreBunch/Instatic/blob/main/ActivityWidget.tsx).
- **Synchronous Recording**: Audit events are written immediately after state mutations, ensuring reliability and consistency with the source of truth.

## Frequently Asked Questions

### What defines the append-only nature of Instatic's audit log?

The append-only architecture relies on a single source of truth—the `createAuditEvent` function in [`server/repositories/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/audit.ts)—which exclusively executes `INSERT` statements against the `audit_events` table. The codebase contains no `UPDATE` or `DELETE` operations for this table, and the schema documentation explicitly prohibits modifications, ensuring every recorded event remains immutable throughout its lifecycle.

### How does the system prevent nested metadata from complicating queries?

The **AuditMetadata** type restricts metadata to flat scalar values and arrays of strings, explicitly disallowing nested objects. This design decision, implemented in the repository layer, keeps database queries efficient and eliminates the need for complex JSON traversal when rendering audit trails in the admin interface.

### Which capabilities control access to audit log data?

Access requires the `audit.read` capability, checked by the `handleAuditRoutes` function in [`server/handlers/cms/audit.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/audit.ts). This permission gate ensures that only authorized administrators can retrieve the immutable event history through the `GET /admin/api/cms/audit` endpoint.

### When are audit events triggered during state changes?

Audit events are created **immediately after** successful state mutations, such as publishing a row or updating a user role. This synchronous approach, implemented throughout the CMS handlers, ensures that audit trails fail alongside their associated operations rather than being treated as asynchronous, best-effort side effects.