adonia-activity-log reads the mandatory adonia_action_events store already written by core actions (and CRUD when enabled). It adds a read-only resource, exact audit filters, semantic diffs, retention tooling, and Plugin ABI metadata without creating a parallel audit store.
Install
pnpm add adonia-activity-logThe configured Plugin ABI manifest registers the resource helper on the panel. Its migration and client-entry metadata are also available to Adonia tooling:
import { Panel } from '@adonia/core'
import { activityLogPlugin } from 'adonia-activity-log'
import User from '#models/user'
export const admin = Panel.make('admin')
.path('/admin')
.plugins([
activityLogPlugin({
panel: 'admin',
authorize: async (ctx) => ctx.auth.user instanceof User && ctx.auth.user.role === 'admin',
tenantScope: false,
}),
])panel is required and scopes reads to that panel id. authorize(ctx, event?) gates both list and detail. The generated resource denies create, edit, delete, restore, and force-delete. Optional slug defaults to activity-log, and navigationGroup defaults to System.
Pass tenantScope exactly as for a core resource: a column name, a compound scope callback, or false for a deliberately shared log. Omission preserves core’s fail-closed behavior on a tenant request; the factory does not guess ownership. The stock core migration has no tenant column, so a string scope is appropriate only when the application adds that column.
For manual registration, createActivityLogResource(options) remains available. activityLogManifest exposes the unconfigured package metadata, including protocol version, migration directory, client entry, and resource helper.
Install the lazy browser half in the application registry:
import { activityLogUiPlugin } from 'adonia-activity-log/client'
import { adonia } from '@adonia/ui'
export default adonia({
registry(registry) {
registry.use(activityLogUiPlugin)
},
})The client plugin registers the status cell, exact-value audit filter, and semantic changes renderer. Diff and filter code is dynamically imported so it does not enter the base UI bundle.
Browsing and diffs
The detail schema exposes panel, resource, action, status, actor type and id, target id, batch id, both timestamps, scrubbed payload and changes, and safe exception state. Index filters support exact resource, action, actor id, and target id matches, plus status and occurrence range.
When actorType or resource is a slug in the active panel’s resource registry, actor and target identities receive server-resolved detail URLs. Unregistered identities remain plain text; the browser never guesses panel mounts or resource routes.
Changes shaped as { before, after } render as a field-level old-value → new-value table containing only changed keys. Legacy or malformed shapes fall back to escaped, pretty-printed JSON. Sensitive-field scrubbing still happens before persistence; applications must avoid secrets in custom action labels and exception messages.
Retention
Prune from automation with an absolute ISO cutoff or a relative age:
node ace adonia:activity:prune --older-than=90d
node ace adonia:activity:prune --older-than=2026-01-01T00:00:00Z --panel=adminRegister the exported Ace command in adonisrc.ts:
import { defineConfig } from '@adonisjs/core/app'
export default defineConfig({
commands: [() => import('adonia-activity-log/commands/activity_prune')],
})The equivalent programmatic API is:
import { purgeActivityEvents } from 'adonia-activity-log'
const deleted = await purgeActivityEvents('90d', { panel: 'admin' })Retention permanently deletes rows whose created_at is before the cutoff. Omitting panel prunes all panels, so use a scoped value when policies differ.
See adonia-activity-log API for every symbol.