MODULES / SECURITY
Audit Log
Free · MITv0.1.1Append-only, tamper-resistant record of who did what and when: every module event is captured automatically, and your own code can record entries with the request's IP and user agent.
terminalnpx ajolla add audit-logWhat you get
- Records every Ajolla event automatically, exactly once
- Append-only table: updates and deletes are blocked in the database
- Record entries from your own code with IP and user agent captured
- Searchable admin screen with filters and fast keyset pagination
- CSV export of up to 50,000 rows, protected against spreadsheet formula injection
- Org-scoped access: auditors only see their organization's entries
- Configurable retention with a cron-driven purge
- Secrets are stripped from event payloads before they are stored
Routes it adds
/admin/audit
/api/ajolla/audit-log/export/api/ajolla/audit-log/purge
Plus 1 Supabase migration with row-level security, and an AGENTS.md your coding agent reads after install.
Installs with
Kernel (added automatically).
Configuration
- AUDIT_RETENTION_DAYS
- Delete entries older than this many days when the purge cron runs (default 365; 0 keeps entries forever).
- AUDIT_IGNORE_EVENTS
- Comma-separated event types not to record, e.g. "analytics.tracked,search.*".
Use it from any stack
Ajolla Hosted runs the same code as a multi-tenant API. Create an app in your dashboard, enable Audit Log, and call /api/m/audit-log/v1 with your app key.
OpenAPI document — feed it to Ajolla to generate SDKs in 7 languages and an MCP server.
AGENTS.md
Read the instructions installed with this module
markdown# Ajolla Audit Log
An append-only record of who did what, and when. Every event any Ajolla module emits is recorded automatically; your own server code can record entries too.
## What was installed
- **Admin page** `/admin/audit` (permission `audit.read`): filters (action prefix, actor, actor id, target, organization, source, date range), newest-first table with expandable details and metadata, keyset pagination, "Export CSV".
- Organization auditors open it with `?orgId=<uuid>`; the permission is checked in that organization.
- **Route** `GET /api/ajolla/audit-log/export` — the same filters as a CSV download (max 50,000 rows, `audit.read` required). Cells that would run as spreadsheet formulas are prefixed with `'`.
- **Route** `GET /api/ajolla/audit-log/purge` — retention cron, protected by `CRON_SECRET`.
- **Table** `ajolla_audit_log_entries` — `id, occurred_at, actor_id, actor_label, action, target_type, target_id, org_id, ip, user_agent, metadata, source ('event' | 'api' | 'app'), event_id`.
- Append-only: a trigger blocks `update`, `delete` and `truncate` for every role, including the service role. Only `ajolla_audit_log_purge(before)` (service role only) may delete.
- RLS: reading requires `ajolla_has_permission('audit.read', org_id)`. Signed-in users may insert entries only as themselves (`actor_id = auth.uid()`, `source = 'app'`, no `event_id`, not backdated). Anonymous visitors get nothing.
- `actor_id` has no foreign key on purpose: the trail outlives deleted accounts, and `actor_label` keeps a readable snapshot.
- **Subscriber** `lib/ajolla/subscribers/audit-log.ts` (`"*"`) — records every event (action = event type) with credential-like payload keys (`/password|secret|token|key/i`) removed. Idempotent through the `event_id` unique key.
- **Code**
- `lib/ajolla/audit-log/core.ts` — `recordAudit`, `recordAuditBatch`, `listAuditEntries`, `getAuditEntry`, `iterateAuditEntries`, `toCsv`, `sanitizeMetadata`, `purgeOlderThan`.
- `lib/ajolla/audit-log/record.ts` — `recordAuditFromRequest(entry)` for server actions and route handlers.
- `lib/ajolla/admin/audit-log.ts` — admin page registration.
- **Events**: emits none; consumes every event (`"*"`).
- **Env**
- `AUDIT_RETENTION_DAYS` — default 365; `0` keeps entries forever.
- `AUDIT_IGNORE_EVENTS` — comma-separated event types not to record; `module.*` skips a whole module (e.g. `analytics.tracked,search.*`).
## Setup
1. Apply migrations: `supabase db push`.
2. Give someone `audit.read`. Until Roles & Permissions is installed, admins (`app_metadata.role = "admin"`) hold it.
3. Schedule the retention purge daily. In `vercel.json`, next to the kernel's events cron:
```json
{ "crons": [{ "path": "/api/ajolla/audit-log/purge", "schedule": "30 3 * * *" }] }
```
Vercel sends `Authorization: Bearer $CRON_SECRET`. Any other scheduler must send the same header.
## Recording your own entries
```ts
"use server";
import { recordAuditFromRequest } from "@/lib/ajolla/audit-log/record";
await recordAuditFromRequest({
action: "invoices.sent", // "<module>.<verb>", lowercase
targetType: "invoice",
targetId: invoice.id,
orgId: invoice.orgId, // optional: makes the entry visible to that org's auditors
metadata: { amount: invoice.total }, // JSON object, at most 16 KB
});
```
It records the signed-in user as the actor (their email as the label), the user agent, and the client IP from the first `X-Forwarded-For` hop. Only trust that header behind a proxy that sets it (Vercel, Netlify, Cloudflare); otherwise clients can spoof it.
For system jobs without a signed-in user, call `recordAudit({ db: createAjollaAdminClient(), appId: null }, { action, source: "app", … })` from server code.
## Customising
- Columns and filters: `app/admin/audit/page.tsx` and `components/ajolla/audit-log/*`.
- What counts as sensitive in event payloads: `SENSITIVE_KEY` in `core.ts`.
- Export cap: `MAX_ROWS` in `app/api/ajolla/audit-log/export/route.ts`.
- The purge refuses cutoffs less than a day old, so a misconfiguration can't wipe the log.