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 auth: enabled 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 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
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
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
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
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.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
role.add, role.removeA role is assigned or taken away, through the API or from realm.yaml seedsrole
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
admin.loginA user holding an admin role logs insession_id
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 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 08 Oct 2026