Audit events get designed twice. The first time, somebody adds logging to a handler and writes whatever describes it. The second time, a year later, a customer is streaming those events into their own tooling, has written detection rules against the names, and every rename you make silently breaks a rule they will not notice has stopped firing.
Which makes the vocabulary an interface. It deserves the same treatment as a set of endpoints: designed up front, documented, versioned, and extended rather than edited.
A shape that scales past the first fifty
Name events object first, verb second, in the past tense: member.invited, connection.deleted, api_key.created. Object-first sorts usefully, groups naturally in a filter list, and forces the question of what the event is actually about. Verb-first reads better in English and produces a list nobody can scan.
Keep the verbs to a small closed set — created, updated, deleted, enabled, disabled, granted, revoked, succeeded, failed — and let the object carry the specificity. When you need a new verb, that is worth a moment's thought, because it usually means the object was named too broadly.
- object.verb, past tense, lower case, dot separated
- A closed set of verbs; specificity lives in the object
- Namespace by product area when the list outgrows one screen
- Publish the list; an audit vocabulary nobody can enumerate cannot be filtered
Every rename is a detection rule that stops firing at a customer, silently, with no error anywhere.
Granularity is the real decision
Too coarse and the event is useless: settings.updated tells a reviewer nothing about whether somebody turned off enforced single sign-on. Too fine and you have three hundred names, most of which fire never, and a customer configuring alerts gives up.
The rule that holds up is to split where the security consequences differ. A change to a display name and a change to an authentication policy are both settings, and they belong to different events because one of them is worth waking somebody for. Anything below that line can carry its detail in structured fields instead of in the name.
Evolving it without breaking anybody
Additions are safe if consumers ignore unknown names, so say in your documentation that they must. Removals and renames are not, and the workable path is the same one you would use for an endpoint: emit both names for a deprecation period, announce the date, and keep the old one until the traffic on it is gone.
Version the payload separately from the name. A schema version field on every event lets you add fields freely and change a field's meaning explicitly, which is the difference between a consumer that adapts and one that misreads a value for six months. And do not put anything in an event that you would not want retained for years — audit records are append-only by definition, so a secret written into one cannot be taken out.
- Add freely; consumers must tolerate unknown action names
- Dual-emit through a deprecation window before removing a name
- Schema version on the payload, independent of the action name
- No credentials, tokens or personal detail beyond what the event needs
Everything here, already built
Sign-in, enterprise SSO, directory provisioning, roles and an audit trail behind one API. Start with the quickstart and have a working sign-in this afternoon.