Events and audit
Every audit record is an event, recorded in the transaction that made the change and fanned out to subscribers, webhooks, and the audit chain.
Your application and your security team both need to know when access changes. The application may sync a new member to a billing system or invalidate a cache when a role changes; the security team wants every administrator elevation in its SIEM and an alert on repeated failed sign-ins. And auditors want a record of all of it that nobody can quietly edit.
Better IAM serves all three from one source: the audit log. Every audit record is also an event you can react to.
Each of these produces one audit record inside the transaction that made the change: provisioning operations,
authentication events (auth:session:create, auth:mfa:enable, and so on), denials, root overrides, invitation
redemptions, access-request decisions, and deployment operations.
The record is appended to the tenant's tamper-evident and fanned out in that same transaction. So nothing is emitted for a change that rolled back, and nothing committed is lost.
You can consume events three ways:
- In-process subscribers run your code in the application for matching events. Use them to update caches, metrics, or other systems in the same codebase.
- send signed HTTPS requests to an endpoint. Use them for SIEMs, alerting, and services outside your application.
- The audit log answers questions after the fact, such as "who changed this role last week?".
There is one deliberate exception to "nothing is emitted for a change that rolled back". auth:signin:fail
records a wrong password, factor, or recovery code presented for a real account. The refused sign-in rolled back,
so the event is appended in a transaction of its own, with metadata.reason, metadata.ip, and
metadata.userAgent. That makes it a good subscription for brute-force alerting.
The event
Every event has the same shape, whichever way you receive it:
Prop
Type
Audit records omit passwords, keys, and token bodies, so nothing that consumes events ever receives them. The
audit chain page explains sequence, previousHash, and hash, and the
lifecycle events page lists the metadata of the access lifecycle events.
In-process subscribers
When the reaction lives in your own codebase, subscribe to events directly instead of running a webhook endpoint.
iam.events.subscribe(patterns, handler) registers a handler for events whose action matches any of the patterns
and returns a function that unsubscribes it:
const stop = iam.events.subscribe(['iam:identities:*', 'access-request:*'], async (event) => {
await metrics.count(event.action, { tenant: event.tenantId, outcome: event.outcome });
});
// Later
stop();- Patterns use the policy glob syntax (
*and?) and may be a single string or a list. - The
events.onEventoption receives every event, for a single catch-all handler configured with the instance. - Plugin
afterAudithooks share the same queue, so plugins react to events the same way.
Dispatch
Handlers do not run inside the request that caused the event. If they did, a slow or failing handler could delay or break the operation that already committed. Instead the event is queued, and a dispatcher delivers it.
Call iam.events.dispatch() (the same function as iam.dispatchAuditHooks()) from your worker schedule, next to
iam.auth.dispatchOutbox(), which delivers emails and webhooks. It runs the queued events through every handler
and returns { dispatched }.
setInterval(async () => {
await iam.events.dispatch();
await iam.auth.dispatchOutbox();
}, 60_000);- Dispatch is at least once. A handler that throws leaves its row queued for the next run, so handlers must be
idempotent by
event.id. - Subscribers live in memory, so dispatch in the process that registers them. The CLI
outboxcommand serves only plugins andevents.onEvent. doctorreports audit hooks that have waited more than 15 minutes, a sign that nothing is dispatching.
Framework integrations wrap this. In NestJS, @OnIamEvent('identity:*') subscribes a provider method, and
dispatchIntervalMs runs dispatch inside the app on a timer; for multi-instance deployments, dispatch from a
single worker. See NestJS and background jobs.
Query the audit log
For questions after the fact, such as a member's activation history or last week's denials, read the log.
audit.list (iam:audit:read) returns a tenant's events newest first, filtered as you need:
const denials = await iam.api.audit.list(credential, {
tenantId,
action: 'iam:bindings:*', // exact name or glob
outcome: 'deny',
from: Date.now() - 7 * 86400_000,
limit: 100,
});limit is 1 to 1000 (100 by default) with an offset. action accepts an exact name or a glob pattern;
actorId, resourceId, and outcome match exactly; from and to bound the timestamp.
Where to go next
Better IAM is created by Sean Filimon
Last updated