Audit Trail #
The Audit Trail module records every significant user action performed on the platform. Each entry captures who did what, when, and whether it succeeded.
The module is implemented as AuditTrailPlugin and is registered as a top-level plugin alongside the PlatformConfigPlugin.
See How To - Register an Audit Hook for a practical usage guide.
Hook registry #
External hooks are declared via auditTrailRegistry.register(). The registry must be populated before app.start(), typically in a plugin or module constructor:
- From a plugin (internal): import
auditTrailRegistryusing a relative path toAuditTrailRegistry.tsand callauditTrailRegistry.register()directly in the constructor. - From a module: call
this.auditTrailRegistry.register()using the protected getter inherited from theModulebase class (no import required).
The method validates the hook config immediately (enforcing the kz prefix guard) and appends the config to the registry's internal list.
During init(), AuditTrailPlugin reads auditTrailRegistry.getConfigs() and attaches all declared hooks. Because constructors run synchronously before app.start(), the registry is always complete and stable by the time any init() runs, regardless of the order or concurrency of plugin initialization.
Registry lock #
As its very first statement, AuditTrailPlugin.init() locks the registry by calling auditTrailRegistry.lock(). After the lock is set, any call to auditTrailRegistry.register() is warned and ignored, the hook will not be active in the current process.
The lock requires an opaque token (RegistryLockToken) that can only be obtained once, via acquireLockToken(). AuditTrailPlugin claims this token in its constructor. Neither acquireLockToken() nor the token type are exported from the public package barrel, making it impossible for integrator code to lock the registry out of turn.
Redis key registry #
Action keys and status keys are persisted in two Redis sets (audit-trail:registry:actions, audit-trail:registry:statuses). They are populated during init() when each hook config is processed.
These sets are exposed through getActionKeys() and getStatusKeys(), and are consumed by the audit-trail controller to:
- serve the
getActionsandgetStatusesAPI endpoints (used by the frontend to populate filter dropdowns) - validate the
statusfield inpostTrailrequests
The registry is rebuilt from scratch on every boot: at startup the plugin flushes both Redis sets and re-adds every key registered in the current process, ensuring no stale keys from a previous deployment persist.
Single write path #
All audit documents, whether written by built-in hooks (assets, devices) or by integrator-defined hooks, go through a single private method: writeAuditDocument. This method builds the standardised AuditTrail document and calls sdk.document.create on the tenant index.
Startup sequence #
When the plugin initialises it runs the following steps in order:
- Registry lock:
auditTrailRegistry.lock()is called immediately, preventing any further hook registrations. - Redis flush: clears
audit-trail:registry:actionsandaudit-trail:registry:statusesfrom any previous boot. This entire sequence (steps 2–5) is protected by a cluster-wideMutex('audit-trail/initRegistry')to prevent two nodes from racing during simultaneous restarts. - Seed built-in statuses: adds
kzOkandkzErrorto the Redis status set. - Attach built-in hooks: attaches hooks for the
device-manager/assetsanddevice-manager/devicescontrollers and seeds their keys into Redis. - Attach external hooks: reads
auditTrailRegistry.getConfigs()(populated byauditTrailRegistry.register()calls in constructors) and attaches each declared hook, seeding its keys into Redis. - Controller registration: the
audit-trailcontroller is registered with its API actions.
For each hook config, two Kuzzle event listeners are registered on this.hooks:
{controller}:after{Action}: writes a success audit entry{controller}:error{Action}: writes an error audit entry
These are registered directly on the plugin's this.hooks map (not via app.hook.register()), so Kuzzle's plugin manager picks them up through its standard _initHooks path for this plugin.
Schema versioning and legacy compatibility #
Every audit document written by the current module includes schemaVersion: 1. Documents written before this version was introduced (legacy documents) do not have this field.
The frontend uses the absence of schemaVersion to apply a key-remapping table (LEGACY_ACTION_KEY_MAP, LEGACY_STATUS_KEY_MAP) so that old documents continue to display correctly.
Built-in hooks #
The module ships with built-in hooks for the device-manager/assets and device-manager/devices controllers. Their action keys are prefixed with kz (e.g. kzCreateAsset) to distinguish them from integrator-defined keys. Integrators must not use the kz prefix (auditTrailRegistry.register() throws immediately if a reserved prefix is detected).
Roles #
The module registers two Kuzzle roles:
audit-trail-admin: read and write access to theaudit-trailcollectionaudit-trail-reader: read-only access to theaudit-trailcollection
API #
The module exposes the audit-trail controller. See Controllers - audit-trail for the full API reference.