logicspike/docs

Settings

Audit Log — Future Implementation Specification

Overview

The Audit Log provides a complete, tamper-resistant record of all significant actions taken within a workspace. It answers the question: "Who did what, and when?"

This is essential for security compliance, debugging, and accountability in multi-admin workspaces.


Scope

Events to Capture (per workspace)

Category Action Who Triggers
Workspace Workspace renamed Owner / Admin
Workspace Workspace deleted Owner
Ownership Ownership transferred Owner
Members Member invited Owner / Admin
Members Invitation revoked Owner / Admin
Members Member removed Owner / Admin
Members Member role changed Owner / Admin
Members Member joined (accepted invite) Member
API Keys API key created Owner / Admin
API Keys API key revoked Owner / Admin
Billing Plan upgraded / downgraded Owner
Security 2FA enabled for workspace Owner
Settings Workspace logo changed Owner / Admin

Database Schema

Add a new audit_logs table:

CREATE TABLE audit_logs (
    id          TEXT PRIMARY KEY,
    tenant_id   TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    actor_id    TEXT NOT NULL REFERENCES users(id),     -- who did it
    action      TEXT NOT NULL,   -- e.g., "workspace.rename", "member.invite"
    target_type TEXT,            -- e.g., "user", "api_key", "workspace"
    target_id   TEXT,            -- the ID of the affected entity
    metadata    JSONB,           -- before/after values, extra context
    ip_address  TEXT,
    user_agent  TEXT,
    created_at  TIMESTAMP DEFAULT NOW() NOT NULL
);

Drizzle Schema (packages/core-database/src/schema.ts addition)

export const auditLogs = pgTable("audit_logs", {
    id: text("id").primaryKey(),
    tenantId: text("tenant_id").references(() => tenants.id, { onDelete: "cascade" }).notNull(),
    actorId: text("actor_id").references(() => users.id).notNull(),
    action: text("action").notNull(),       // "workspace.rename" | "member.invite" | etc.
    targetType: text("target_type"),        // "user" | "api_key" | "tenant" | null
    targetId: text("target_id"),
    metadata: jsonb("metadata"),            // { before: {...}, after: {...} }
    ipAddress: text("ip_address"),
    userAgent: text("user_agent"),
    createdAt: timestamp("created_at").defaultNow().notNull(),
})

Backend API

GET /manager/tenants/audit-log

  • Auth: role_owner or role_admin
  • Query params: ?limit=50&cursor=<last_id>&action=<filter>
  • Response: paginated list of audit events with actor info joined

Middleware: logAudit(ctx, action, target?, metadata?)

A reusable utility that other route handlers call at the end of their mutations:

await logAudit(db, {
    tenantId,
    actorId: userId,
    action: "workspace.rename",
    targetType: "tenant",
    targetId: tenantId,
    metadata: { before: { name: oldName }, after: { name: newName } },
})

Frontend

Route: /dashboard/settings/audit-log

  • Full-page table with infinite scroll / pagination
  • Filter by: Action category, Date range, Actor
  • Each row: Avatar + Actor name, Action label (human-readable), Target identifier, Timestamp (relative + absolute on hover)
  • Export to CSV button (calls GET /audit-log?format=csv)

Implementation Order

  1. Add auditLogs to schema + migrate
  2. Create logAudit() helper utility in packages/core-types or manager/src/lib
  3. Instrument key routes: PATCH /tenants, DELETE /tenants, POST /tenants/transfer, API key routes, team member routes
  4. Add GET /tenants/audit-log paginated endpoint
  5. Build the frontend page at /dashboard/settings/audit-log
  6. Add link card to workspace settings hub

Security Notes

  • Only role_owner and role_admin can view the audit log
  • Audit log records must be append-only — no update or delete endpoints
  • On workspace deletion, audit records are cascade-deleted (acceptable — workspace is gone)
  • For compliance-sensitive workspaces (future enterprise plan): archive to cold storage before deletion
Settings