Skip to content
All guides

Administration · Updated August 31, 2026

Audit Log

What the audit trail records, what it deliberately leaves out, who can read it, and how to find the row you're looking for.


An append-only record

The Audit Log screen needs view_audit_log, which Admin, Finance / Accounts and Auditor hold by default. Exporting it needs export_reports as well. See Roles & Permissions.

Rows are written once and never changed. The table carries a database trigger that refuses any update outside a maintenance transaction only an operator can open, so there is no screen, permission or API call in the product that edits or deletes an entry. The trail also outlives the company it describes: deleting a workspace removes the workspace and keeps its audit rows, because the one thing a record of a deletion must survive is the person who wanted it gone.

Everything lands in the same table, so the trail reads across modules. An item edit, a role change, a rejected waste note and a support session sit in one timeline, in the order they happened.

What gets recorded

Every request that changes something (POST, PUT, PATCH or DELETE) and reaches its handler is logged, whether it succeeded or failed. A failure is recorded with a description that starts with FAILED and the status code, because a rejected approval or a delete that threw halfway is exactly what an investigation is looking for.

Each row carries:

  • Time, in your browser's local time on screen.
  • Entity, the first segment of the path: inventory, procurement, users, compliance, auth and so on. The first record id in the path is kept alongside it, so "every change to this one purchase order" is a filter away.
  • Action, the method and path with record ids stripped out, such as PATCH /inventory/items, so rows group.
  • User, the actor's email, captured at write time so the row still names them after the account is gone.
  • Description and IP address.
  • Changes, where the writer supplied them: the previous and new values, shown side by side in red and green when you open a row. Not every write records a field-level diff; a row without one says "No change details recorded for this entry."

Two things are not in the trail, on purpose. Reads by members of your company are not logged (a busy bar would drown the table in them). And a request refused by a permission check before it reached its handler is not recorded either, because the check runs before the logger does.

Sign-in events

Authentication happens before a company is selected, so it's recorded separately under the entity type auth, with the source IP and user agent captured from the request. The actions are:

ActionWhen
auth.login.succeeded, auth.login.failedA password sign-in, either way
auth.logoutA sign-out
auth.password.changedA password changed from the account page
auth.password.reset_requested, auth.password.reset_completedThe forgot-password flow, and the same link an invitation uses
auth.two_factor.enabled, auth.two_factor.disabledTwo-factor enrolment

A failed login is recorded against the account it targeted, never against the typed email, which is unverified input. And an auth event lands in your company's log only when that account belongs to exactly one company. Someone who works for two Bohari customers has their sign-ins recorded at platform scope, visible to Bohari staff but to neither company, because the event can't honestly be attributed to one of them. The header your browser sends naming a company never gets a vote in that decision.

Bohari Support rows

When platform staff act inside your workspace, every request they make is logged, reads included, and the row is tagged. On screen the user column shows a Bohari Support badge instead of an email, with the stated reason for the support session in the tooltip and in the row's detail. The operator's email and IP address are withheld, since both identify a person outside your company. A platform admin acting inside a company they're actually a member of is logged as an ordinary member.

What never lands in the trail

Rows are permanent, stored in plain text and readable by every holder of view_audit_log, so the values are scrubbed on the way in. Any key whose name looks like a password, token, API key, two-factor secret or backup code, signature, KRA PIN, national ID, passport number, bank or card number or M-Pesa number is replaced with [REDACTED], and the whole subtree under such a key goes with it. Booleans are left alone (knowing two-factor was switched on is the point of the row) and absent values stay absent rather than pretending a secret was there. Sign-in events carry no payload at all, so there is no field through which a password could arrive.

Finding a row

Along the top you get a Search box, an Entity dropdown, a From and To date, and Clear filters. The search matches action, entity, user email, description and record id across the rows already loaded; the entity list is built from those same rows. The dates go to the server.

The screen loads the newest 250 rows for the current filters, and the API serves at most 500 in one call. A busy company posts that many in an afternoon, so if the row you want is older than the bottom of the list, narrow the dates rather than paging further.

Click any row for the detail view: the summary fields, the support-session notice if it applies, the description, and the change diff.

Export CSV downloads the filtered rows as seven columns (timestamp, entity type, entity id, action, user, IP, description). Cells starting with a character a spreadsheet would run as a formula are prefixed with an apostrophe, which Excel strips on display.

FAQ

Can I delete an entry that was recorded by mistake? No, and neither can Bohari support from inside the product. The table refuses updates and deletes at the database. If a row is wrong, the correction is another row: the reversal, the re-approval, the follow-up change.

Why are one person's failed logins missing? Most likely they belong to more than one company. Auth events are attributed to a company only when the account has exactly one; otherwise they're recorded at platform scope, where no tenant can read them. Ask Bohari support if you need that history.

Who is "Bohari Support"? Platform staff acting in your workspace under a stated reason. Their reads are logged too, which ordinary members' reads are not. Open the row to see the session reason.

Why can't I see who looked at a supplier's price list? Reads by your own people aren't recorded. The trail is a record of what changed and of operator access, not of every page view.

The list stops before the date I'm after. You've hit the 250-row window. Set From and To around the day you care about and the newest 250 rows inside that range load instead.