Skip to content

Audit trail

A permanent, append-only record of who changed users, roles, network restrictions, API keys and invitations, and when.

Each realm with an auth section keeps an audit trail: one record per security-sensitive action or event, with who did it, to what, when and from where. It answers questions such as “who gave Bob the admin role?” or “which key created this user?”. Records can be read, but not changed or deleted: they expire after the realm’s retention period.

What is recorded

ActionWhenDetails
user.createAn administrator (or backd-server bootstrap) creates a userpassword (whether one was set), roles (seeded from realm.yaml)
user.signupA user signs upinvited, roles
user.passwordAn administrator sets a user’s password
password.expireAn administrator expires a user’s password, or every password of the realm (target realm)scope (user or realm), reason; for the realm also accounts, sessions, keep_sessions
password.reset_linkAn administrator made a reset link for a userexpires_at, redirect_to; never the token
password.expire_notifyThe job that tells the users to choose a new password endedjob_id, queued, skipped, deferred
session.revoke_allAn administrator ended every session of the realmsessions
user.password_changeA user changes their own password
user.verify_emailAn administrator marks an email as verified, or not; or a user verifies it with the link in their emailverified
user.password_resetA user sets a new password with a reset linkverified_address
user.email_changedA user’s address changes: confirmed by the new address, or set by an administratorby (self or admin)
user.email_change_revertedThe old address undid a change
user.purged_unverifiedA worker deletes accounts that never verified their addresscount, older_than
push.testAn administrator sent a test push to a usersend, kind
device.removedAn administrator removes a user’s push deviceplatform, device
user.claimedA link or code verified an address that was never verified, and took the account away from whoever made it: password, providers, second factor and sessions are removedmethods, mfa
function.invoke_manualAn administrator runs a function by hand (admin API)as (the user’s id, never their email, or null)
files.versions.regenerateAn administrator runs backd files regenerate (admin API)version, missing_only, job_id
storage.reconcileAn administrator runs backd storage reconcile (admin API)delete, orphans (how many were found), deleted
export, export.doneAn administrator starts and ends an export (admin API); never any contentdatabase, collections, files, partial; at the end collections, documents, files, bytes, complete
export.authAn export includes authentication data; never the data or the keycollections, key_fingerprint
import, import.doneAn administrator starts and ends an import (admin API); never any contentmode, dry_run, skip_invalid, databases, source_realm; at the end the counts by outcome (created, replaced, skipped, unchanged, conflict, invalid), files, complete
storage.checkAn administrator runs a storage check (admin API); never the keys or anything storedok, provider, bucket
data.checkAn administrator starts a schema check (admin API); the target is data:<database>/<collection>, data:<database> or data:*; never the reportcollections, limit, job_id
schedule.pause, schedule.resumeAn administrator pauses or resumes a function’s schedule (admin API); only a change is recorded; the target is schedule:<database>/<function>none
triggers.gap_resolveAn administrator closes a gap of the trigger dispatcher (admin API, backd triggers resolve); the target is trigger-gap:<id>none
triggers.rescanAn operator runs backd-server triggers rescan for a collection; the actor is cli; the target is <database>/<collection>trigger, since, scanned, queued, run
job.cancel, job.rerunAn administrator cancels a job, or queues a finished one again (admin API); the target is job:<id>function; for a re-run also new_job
user.disable, user.enableAn administrator disables or re-enables a user
user.deleteAn administrator erases a user (the tombstone is made; the data work is in the job)job_id
user.erasedThe erase job finished, or failed for goodjob_id and counts per database/collection/operation; needs_attention and error when it failed. Never content
user.data_export_requested, user.data_export_requested_silentlyA user asked for a copy of their data, or an administrator made one for them and told them (requested) or did not (_silently, with the reason); the target is user:<id>export, ticket (administrator), reason (silent)
user.data_exported, user.data_export_failedThe export is ready, or failed for goodexport, size, by; error
user.data_export_downloaded, user.data_export_expiredA link to the package was given or its bytes sent, with who took it; the package was deleted at the end of its retentionexport, by
user.delete_accountA user deletes their own account (it is deactivated, not erased)
user.networksA user’s network restrictions change, through the API or from realm.yamladmin_networks, login_networks
identity.apple_revokedApple revoked a user’s tokens after their account was erased or deleted, or Apple was unlinked
identity.linkedA provider account is linked to a user, automatically or by the user; also when a provider sign-up creates the userprovider
identity.unlinkedA user (or an administrator) removes a sign-in methodprovider, by (self or admin)
identity.signin_refusedA provider sign-in for an account that can’t sign in (the app is only told signin_refused)provider, reason (disabled, network, erased, …)
role.add, role.removeA role is assigned or taken away, through the API or from realm.yaml seedsrole
data.transfer, data.assign_orgA document is moved to another organization, or a job gives a collection’s documents without one an organization (admin data route)database, collection, id, from, to / org, job_id; never content
org.create, org.updateAn organization is created (by a user or an administrator) or renamedby, creator / fields
org.member_add, org.member_remove, org.member_leave, org.rolesA member is added without an invitation (by is admin or sso), removed, leaves, or has roles changeduser, roles, by / added, removed
org.team_create, org.team_update, org.team_delete, org.team_member_add, org.team_member_removeA team is made, changed or deleted, or someone joins or leaves itteam, name, roles / user
org.invitation_create, org.invitation_link, org.invitation_accept, org.invitation_revokeAn organization invitation is made, given a new link, accepted or revokedinvitation, binding, roles, teams, sent / user
org.require_mfaAn organization turns its second-factor requirement on or offvalue
org.delete, org.deleted, org.reactivateAn organization is deleted (access ends), its cleanup ends, or it is brought backby, job_id / counts (documents removed per collection; never content) / slug
org.sso.set, org.sso.deleted, org.sso.disabledAn organization’s sign-in connection is set, removed, or switched off by backd because the manager who set it no longer holds the roles it gives on joiningprovider, enabled, secret_changed / provider, by / provider, reason (never the secret)
org.sso_requiredAn organization turns on or off the requirement to sign in through its connection, or backd turns it off because the last working connection wentvalue; turning it on without_sso, revoked; backd turning it off reason
org.sso.loginA person signed in through an organization’s connectionuser, provider, joined (this sign-in made them a member)
org.claims, org.owner_assignClaims are replaced; a manager is assignedkeys / user
secret.key.create, secret.key.rotate, secret.key.deleteA field key is created, imported, rotated or deletedfingerprint (the new one), generated or previous and rewrapped; never the key
apikey.create, apikey.revokeAn API key is created or revokedrole, networks, expires_at
invitation.create, invitation.revokeAn invitation is created or revokedbound_to_email (whether it is), expires_at
invitation.sentAn invitation is emailed to its addressexpires_at
passkey.registeredA user added a passkeypasskey, name, synced
passkey.removedA passkey was removed, by the user or an administratorpasskey, name, by (self or admin), sessions_ended
admin.loginA user holding an admin role logs insession_id, strategy, methods
encrypt.start, encrypt.rekey, encrypt.migrateAn encryption migration was started, a field rekeyed, or a migration finished (by the job)database, collection, fields, dry_run, job_id, generation; the counts and the number of retired keys; never a value
data.revealA full administrator read documents that hold encrypted fields in plain through the admin data route (one record per read)database, collection, fields, documents; never a value
data.create, data.update, data.delete, data.restore, data.purgeA document is written through the admin data route (a batch audits each operation)database, collection, id; the target is doc:<database>/<collection>/<id>; never content
session.revokeAn administrator ends a user’s session (admin API)session
admin.refusedAn admin API request is refused: by a network restriction, or because the administrator’s roles don’t open the area or hold the rights of the user or role involvedreason, method, path
realm.bootstrapbackd-server bootstrap creates the realm’s first administratorrole

Failed logins aren’t recorded here: they are throttled and counted, and every request is in the access log.

Records

{
  "id": "d3cb1q0spovnshcrql9g",
  "at": "2026-09-27T12:00:00.000Z",
  "action": "role.add",
  "actor": "user:d3c9ljp8hc2g00b6s1m0",
  "target": "user:d3cb0m8spovnshcrqk90",
  "details": { "role": "editor" },
  "request_id": "d3cb1q0spovnshcrql8g",
  "client_ip": "192.0.2.10"
}
  • actor is who acted: user:<id> (a session), key:<name> (an API key), anonymous (sign-ups and refused requests), config:realm.yaml (seeds applied at startup) or cli:bootstrap.
  • target is what the action applied to: user:<id>, key:<name>, invitation:<id>, or null.
  • request_id matches the request’s X-Request-ID and its line in the access log. client_ip follows TRUSTED_PROXIES.

Records name users by id, never by email, and never contain passwords or password hashes, session tokens, API keys, invitation tokens, or request bodies. A test checks this for every action.

Reading the trail

With the CLI, as an administrator:

backd audit --realm blog --since 7d
# TIME                      ACTION         ACTOR                    TARGET                   CLIENT      DETAILS
# 2026-09-27T12:00:00.000Z  role.add       user:d3c9ljp8hc2g00b6s1m0  user:d3cb0m8spovnshcrqk90  192.0.2.10  role=editor
# …

backd audit --realm blog --user bob@example.com       # records about one user
backd audit --realm blog --action apikey.create --json

--action, --actor and --target filter exactly; --user looks up an existing user’s id (for a deleted user, use --target user:<id>); --since takes an RFC 3339 time or a duration back from now; --limit defaults to 50; --json prints one JSON record per line.

Over HTTP, GET /v1/{realm}/_admin/audit returns a page of records, newest first, with the query parameters action, actor, target, since, until, limit and skip (see the admin API). The JavaScript client has client.admin.audit.list(…).

Each record is also written to the log, as a line with "msg":"audit" and the same fields, so the trail reaches your log pipeline too. If a record can’t be stored, the action it describes has already happened; backd logs the record as an error ("msg":"audit record not stored") instead.

Retention

Records are kept for one year by default. Set another period per realm in realm.yaml:

audit:
  retention: 180d   # at least 1d

Each record’s expiry is fixed when it’s written, so a new retention applies to records written from then on. MongoDB removes expired records (a TTL index on expires_at).

Records hold client IP addresses and user ids, which are personal data in many jurisdictions: choose a retention your privacy policy covers, and include the trail in answers to access requests.

Append-only

  • No endpoint or command changes or deletes records.
  • In the production reference, backd’s own database user may only read and insert into the audit collection, so not even a compromised backd process can rewrite history.
  • Records are stored in the realm’s system database and included in its backups.
  • The trail doesn’t cover direct access to MongoDB: people with database credentials can do anything. Keep those credentials to the provisioning step, as the production reference does.
Edit this page · Last updated 13 Nov 2026