Core
IoT Platform v2.x
2

Register a Custom Audit Hook #

This guide explains how to record custom user actions in the audit trail from your own plugin or module.

The following concepts are expected to be known:

Overview #

auditTrailRegistry is a singleton exported from the backend package. Its register() method declares a custom audit hook and must be called before app.start() (typically in a plugin or module constructor). At startup, AuditTrailPlugin.init() reads all declared hooks from the registry and attaches two Kuzzle event listeners for each controller/action pair: one for the success case and one for the error case. Both write a document to the tenant's audit-trail collection.

Declared action and status keys are stored in Redis (keys audit-trail:registry:actions and audit-trail:registry:statuses). This means the registry is shared across all nodes in a cluster. The registry is rebuilt from scratch on every boot: on startup the plugin flushes the two Redis sets and then re-adds every key registered in the current process, ensuring no stale keys from a previous deployment persist. The flush+seed sequence is protected by a cluster-wide mutex so concurrent node restarts cannot race.

Step 1 - Call auditTrailRegistry.register() in the constructor #

Call auditTrailRegistry.register() in the constructor of your plugin or module, before app.start() is called. This is the only supported call site.

From a Plugin (internal):

import { auditTrailRegistry } from '../auditTrail/AuditTrailRegistry';

export class MyPlugin extends Plugin {
  constructor() {
    super({ kuzzleVersion: '>=2.53.0 <3' });

    auditTrailRegistry.register({
      controller: 'my-plugin/things',
      action: 'create',
      i18n: { action: 'createThing' },
      getObject: (request) => ({ id: request.input.args._id, type: 'myThing' }),
    });
  }

  async init(config: JSONObject, context: PluginContext) {
    this.config = config;
    this.context = context;
  }
}

Adjust the relative path to AuditTrailRegistry based on your plugin's location within the package.

From a Module:

import { Module } from '@kuzzleio/iot-platform-backend';

export class MyModule extends Module {
  register(): void {
    this.auditTrailRegistry.register({
      controller: 'my-plugin/things',
      action: 'create',
      i18n: { action: 'createThing' },
      getObject: (request) => ({ id: request.input.args._id, type: 'myThing' }),
    });
  }
}

auditTrailRegistry.register() must be called in a constructor, before app.start(). Calling it inside a plugin or module init() is incorrect: because all plugin init() methods run concurrently, AuditTrailPlugin.init() may have already locked the registry by the time your init() runs, causing the hook to be silently ignored with a warning log.

Step 2 - Hook configuration reference #

auditTrailRegistry.register({
  controller: 'my-plugin/things',
  action: 'create',
  i18n: {
    action: 'createThing',
    status: 'thingCreated', // (optional) defaults to kzOk
    error: 'thingCreateFailed', // (optional) defaults to kzError
  },
  getObject: (request) => ({
    id: request.input.args._id,
    type: 'myThing',
  }),
});

AuditHookConfig properties #

PropertyTypeRequiredDescription
controllerstringYesThe Kuzzle controller name, e.g. my-plugin/things
actionstringYesThe controller action name, e.g. create
i18n.actionstringYesKey identifying this action in the frontend translation files. Must not start with kz, that prefix is reserved for built-in actions and is enforced immediately at registration time
i18n.statusstringNoKey for the success status. Defaults to kzOk
i18n.errorstringNoKey for the error status. Defaults to kzError
getObject(request: Request) => AuditObjectNoCallback that extracts the audited object ({ id, type }) from the request. Omit if no object is relevant

The kz prefix is reserved for built-in platform actions. auditTrailRegistry.register() throws immediately if any of i18n.action, i18n.status, or i18n.error starts with kz. The error is raised at call time (in the constructor), not deferred to startup.

Step 3 - Register multiple hooks at once #

You can call register() as many times as needed. A common pattern is to loop over a map of actions. From a module, use this.auditTrailRegistry; from a plugin, use the directly imported auditTrailRegistry singleton:

const actionMap: Record<string, string> = {
  create: 'createThing',
  update: 'updateThing',
  delete: 'deleteThing',
};

for (const [action, actionKey] of Object.entries(actionMap)) {
  this.auditTrailRegistry.register({
    controller: 'my-plugin/things',
    action,
    i18n: { action: actionKey },
    getObject: (request) => ({
      id: request.input.args._id,
      type: 'myThing',
    }),
  });
}

Step 4 - Add frontend translations #

The frontend translates action and status keys via vue-i18n. For each key you registered, you must add a matching entry to the locale files. Without a translation the frontend will display the raw key name as a fallback.

Locale files are located at:

  • apps/web/src/locales/en.json
  • apps/web/src/locales/fr.json

Add your keys under auditTrail.action and, if you defined custom status keys, under auditTrail.status:

{
  "auditTrail": {
    "action": {
      "createThing": "Create thing",
      "updateThing": "Update thing",
      "deleteThing": "Delete thing"
    },
    "status": {
      "thingCreated": "Thing created",
      "thingCreateFailed": "Thing creation failed"
    }
  }
}

Do the same in fr.json:

{
  "auditTrail": {
    "action": {
      "createThing": "Créer un objet",
      "updateThing": "Mettre à jour un objet",
      "deleteThing": "Supprimer un objet"
    },
    "status": {
      "thingCreated": "Objet créé",
      "thingCreateFailed": "Échec de la création de l'objet"
    }
  }
}

If a translation key is missing, the frontend displays the bare key name (e.g. createThing) as a fallback. This is expected behaviour for integrator-defined keys that have not yet been translated.