Features Apograph CMS on GitHub

Activity plugin

The audit log, and why it is a subscriber rather than a second write.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

The audit log. It records what happened by subscribing to the transactional outbox rather than by being called — which is what makes an audited action impossible to commit without its audit row, and impossible to audit without committing.

Install

npm install @apograph/activity-server @apograph/activity-admin

Register

// apps/server/src/plugins.ts
import { ActivityPlugin } from '@apograph/activity-server';

ActivityPlugin();
// apps/admin/src/plugins.ts
import { ActivityPlugin } from '@apograph/activity-admin';

ActivityPlugin();

No configuration, no environment variables. It owns activity_events and ships its migrations.

A generic sink

kind and meta are open — text and jsonb. Each emitting plugin owns its own event kinds: identity emits authentication and token events, users emits the member lifecycle, content emits the publish lifecycle, media emits asset and folder changes. That is what keeps this package decoupled from any one domain.

Since 0.4.0 the sign-in row also records how somebody signed in and through which provider, and single sign-on contributes three kinds of its own — user.sso_linked, user.sso_provisioned and user.sso_role_mapped. They are deliberately not folded into the sign-in row: “somebody signed in” cannot answer “where did this account come from?”, which is the first question anyone reviewing an SSO deployment asks.

A new producer needs a mapper here

An event kind with no entry in FACET_MAPPERS is not an error anywhere. The dispatcher finds no subscriber, stamps the row dispatched_at, and the action is simply never audited — silently.

The check is one query: compare select distinct kind from outbox_events against AUDITED_EVENT_KINDS. This has been missed three times upstream — API tokens, entry publishes, and the whole media library.

Reading it

The admin’s Activity Log drives GET /api/activity, filtered by workspace, actor, kind and date range. See the endpoint reference for the query parameters, and insights and activity for what the page does with them.

Dead letters

An outbox event no subscriber accepted after fifteen attempts is parked, and the subscriber it was most often bound for is this plugin’s — so a parked row is, more often than not, an audit row that was never written. GET /api/activity/dead-letters lists them (gated on activity:read, the same key as the log, because “what is missing from the trail” is the same question as “what is in it”), and the Activity page shows a notice whenever the total is non-zero.

It reports and does not repair. Replaying a parked row means clearing its attempts, which is an operator’s action against a fixed cause rather than a button. See domain events for the query and the retry ladder behind it.