Audit log
The tamper-evident record of administrative and (optionally) member activity in your organization.
The audit log is the Audit log section of Organization, in the rail's Data and oversight group, at /admin/audit. Viewing it requires canViewAudit; exporting the log as CSV requires canExportAudit. An admin who holds canViewAudit but not canManageOrgSettings can't open the organization's settings, so for them /admin/audit opens the Audit log as a page of its own, with no rail, and the admin sidebar lists it as Audit log. The log's retention and detail level are set, and its tamper check runs, under Organization → Data and privacy → Audit log.
The activity table
The Audit log renders your organization's security events as a live, filterable table. The line above it tells you the coverage window at a glance — "Showing security events for Acme Corp. This log begins Jan 18, 2026." — and the footer shows "Showing 1–20 of 42" with Back / Next paging in the bottom-right. Where the table is wider than the panel it scrolls sideways, and a fade at its right edge shows there is more.
Each row shows:
| Column | What it holds |
|---|---|
| Action | The namespaced verb, e.g. user.role_changed. See the action vocabulary below. |
| Category | A human grouping derived from the action prefix — User management, Groups, Roles & permissions, Sessions, Organization, Database connections, Agents, Billing, Authentication, API keys, Knowledge base, Invitations, Data export, Announcements, Extensions, Connectors, Data policy, Content safety, Oversight, AI engine, Provisioning, Support, and more. |
| User | The actor's email. SCIM-driven changes show scim:<token-label>. |
| IP address | The actor's source IP, shown raw, with the country flag of its approximate location in a circle to its left. |
| Time | When it happened, in your local timezone. |
Filter with the search box (matches action, actor email, target email, and IP address), the category dropdown (All categories until you pick one), and the date-range picker. Filters combine, and the result count updates as you narrow. A date can be typed into either date field the way your language writes it (in German 18.07.2026 or 18. Juli 2026), or as Jul 18 2026 or 2026-07-18 in any language; the calendar beside it names the months and days the way your language does.
The Audit log is drawn in the language you chose in Settings: the line above the table, the columns and categories, the dates and times, the export dialog and an event's details. What an event records stays as recorded: the action (such as user.role_changed), the emails and IP address, the place the address was traced to and the details. The CSV export's column headings and categories are in English whatever the language of the person who exports it.
Event details
Click any row to open the detail panel from the right. At the top is a map pinpointing the approximate location of the recorded IP address; below it, the full record — action, category, actor, target, IP address, resolved location, timestamp, audit ID, and the action's details payload (before/after values, counts, or whatever the action emitted).
KeyStrata stores only the raw IP address — never a derived location. The country flag, map position, and location strings are resolved at read time from MaxMind GeoLite2 data and are approximate (typically city-level at best). Nothing geographic is ever written to the audit record.
What is logged
Every state-changing admin action passes through a single _audit() helper that writes a row to the audit_log table. The row shape is:
| Column | What it holds |
|---|---|
audit_id | UUID primary key. |
org_id | The organization the action targeted. |
actor_user_id | The admin who performed the action. SCIM-driven changes and the model-retirement migration use the nil UUID 00000000-0000-0000-0000-000000000000. |
actor_email | The admin's email. For SCIM, scim:<token-label>; for the model-retirement migration, system:model-retirement. |
action | A namespaced verb. See the action vocabulary below. |
target_user_id | The user the action acted on, if any. |
target_email | The target user's email, if any. |
details | JSON blob of before/after, counts, or whatever the action emits. |
ip_address | The actor's source IP. |
created_at | UTC timestamp. |
Action vocabulary
Action verbs are namespaced (<area>.<verb>). The set logged today includes:
- User lifecycle —
user.created,user.updated,user.role_assigned,user.role_changed,user.enabled,user.disabled,user.deleted,user.memory_wiped,user.conversations_deleted,user.data_exported,user.display_name_changed,user.avatar_changed,user.rate_limit_changed,user.spend_cap_changed,user.spend_request_approvedanduser.spend_request_denied(an admin answered a member's request for more usage; the member is the target, and the details hold the request id and, for an approval, the new monthly limit in cents),user.bulk_<action>(plus a.failedvariant when a bulk item errors). - Per-user governance —
user.model_access_changed,user.python_access_changed,user.jira_write_access_changed,user.confluence_write_access_changed,user.subagent_approval_access_changed,user.agent_controls_changed,db.access_changed,ext.toggled,ext.tokens_revoked,ext.tokens_revoked_bulk. - Groups —
group.created,group.updated,group.deleted,group.member_added,group.member_removed,group.membership_set,group.bulk_assigned,group.role_bound,group.role_unbound(binding or unbinding a role to a group so its members inherit that role's permissions),group.spend_cap_changed. See Groups. - Roles —
role.created,role.updated,role.deleted. - Sessions —
session.revoked(a single session or all of one user's sessions),sessions.revoked(sessions killed when a role's permissions narrow),org.sessions_revoked_all(force sign-out of every member of an org). - Org settings —
org.created,org.updated,org.suspended,org.unsuspended,org.logo_changed,org.lock_permission.acknowledged(the database-access padlock was closed with Pin anyway over members who already hold a higher database permission than the level your organization grants them; records how many, and up to 50 of them by user ID with each one's permission and governed level). - Authentication —
auth.sign_in,auth.sign_in_failedandauth.sign_out(all recorded at every tier),auth.signup_intent(a self-serve signup was started). Rows written by earlier releases carryauth.login_failedfor failed attempts. - Billing —
billing.portal_opened,billing.checkout_started,billing.trial_set,billing.licences_set,billing.user_licence_set,billing.seats_committed_set,billing.subscription_provisioned, and other subscription lifecycle events mirrored from Stripe. See Billing. - Usage —
usage.spend_request_filed(a member asked for more usage; the member is the actor and the target, and the details hold the request id, their monthly limit and usage when they asked, and where that limit comes from: the member, a group or the organization default. Never the reason they gave, which stays on the request and is deleted with the member) andusage.model_allowance_hit(a turn was stopped by, or moved off, a model whose allowance for the period is used up or that is turned off; recorded at most once a day per model and outcome on each Strata server, with the model, why, what happened, the model it moved to and the allowance figures in tokens). Both file under Usage in the category filter. - AI engine —
org.foundry.configured,org.foundry.cleared(your per-tenant Azure AI Foundry connection),org.foundry.deployments_updated(the voice, transcription or embedding deployment names changed, with each old and new name),org.model_allowance.updated,org.model_allowance.bulk_updated,org.model_allowance_default.updated(per-model access for your org), and theauto_deploy.*events (autonomous Foundry deployment — see Autonomous Foundry deployment). - AI models —
model.retirement_migrated: on a model's retirement date Strata moved your organization's settings to its replacement. The actor issystem:model-retirement, or the Kronisys operator when Kronisys retires a model immediately.detailsnames the model, the replacement and how many of each setting changed. - Org connections —
org_db.created,org_db.updated,org_db.deleted,org_db.tested(an admin ran Test connection on a shared connection; records whether it passed),connection.test_failed. - Atlassian extensions —
org.confluence_config_updated(any save in the Confluence group of the Atlassian extension page), plus every write Strata makes:confluence.page_created,confluence.comment_added,jira.issue_created,jira.comment_added. Confluence reads are not audited — searching a space or opening a page leaves no row. - MCP connectors —
mcp.connector_added,mcp.connector_updated,mcp.connector_removed(a marketplace connector enabled or removed under Settings → Apps), andorg.private_connector_created,org.private_connector_deleted(a private connector your organization defines itself). - Policy & DLP —
policy_hook.updated,policy_hook.removed,policy_hook.tested,policy_hook.breaker_reset,policy_detectors.updated(your data-loss-prevention enforcement plane), plus the runtime blocks it records:dlp.email_external_blocked,dlp.onedrive_write_blocked,dlp.office_preview_blocked,dlp.office_preview_minted. Thepolicy_*configuration events are recorded regardless of audit tier (see below). - Content safety —
content_safety.blocked(the AI provider's content filter blocked a request, a custom data rule set to block matched it, or a Claude model declined it: sourcemodel_refusal) andcontent_safety.flagged(a custom data rule set to record matched, and the request went through). Written only while Alerts for blocked requests is on (Organization → Data and privacy), at every detail level. The details hold the source, the surface, the model, the provider's categories and severities (self-harm is recorded only assensitive) or the matching rule ids and labels, whether something had already happened in the turn, and whether an alert named the member. Never the text:prompt_retainedis alwaysfalse. The conversation id is kept only for a turn the member's settings retain, in a conversation that existed before it. - Approvals and sub-agents —
approvals.decided(every approval that waited for a person, with how it ended: approved, denied, expired or stopped, and the reason),subagents.approval_mode_changed(a member changed a chat's Approvals setting, with the value before, the value asked for, the value applied and the organization limit),subagents.action_auto_approved(a sub-agent's action approved without asking, with the tool, the risk tier, the reason code and counts such as the number of recipients),subagents.action_refused(an action sub-agents may never take). These rows never contain the email, message, query or page itself. - Invitations —
invite.created,invite.revoked,invite.resent,invite.preconfigured. - API keys —
api_key.created,api_key.updated,api_key.rotated,api_key.revoked,api_key.purged,api_key.webhook.created,api_key.webhook.deleted, plus the connection bots that back extension-scoped keys:api_bot.created,api_bot.connect_started,api_bot.deleted. See API keys. - SCIM —
scim.token_issued,scim.token_revoked. (SCIM user deactivation flows throughuser.disabledwith ascim:actor.) - Agents —
agent.deleted_by_admin(an admin deleted a member's scheduled agent). - Member-chat oversight —
member_chat.viewed,member_chat.files_viewed,member_chat.file_downloaded,member_chat.conversations_exported,admin.member_conversations_listed(an admin enumerated a named member's chats). - Org deletion —
org.deletion_requested,org.deletion_canceled, andorg.hard_deleted(written org-less when the org is finally purged — see Organization deletion). - Meta —
admin.users_exported(organization data export),audit.read(someone read the audit log — see below),audit.exported(a CSV export of the audit log itself),support_docs.refreshed,admin.legal_cache_flushed.
This list grows over time; treat the live table as authoritative.
Events recorded at every tier
A short set of security-critical events is recorded regardless of your audit detail level — the tier gate never drops them, so even a Standard-tier org keeps them. This set covers organization data exports (admin.users_exported, user.data_exported), reading and exporting the audit log itself (audit.read, audit.exported), enumerating a member's chats (admin.member_conversations_listed), forced sign-outs (sessions.revoked), and every change to the DLP enforcement plane (policy_hook.updated, policy_hook.removed, policy_hook.breaker_reset, policy_detectors.updated). The rationale is simple: these are the first actions a breach investigation looks for, and a compromised admin must not be able to turn enforcement off or drain member data without leaving a row.
Audit detail level
Beyond admin mutations, your organization can opt into capturing more activity at one of three tiers. The level is set in Organization → Data and privacy → Audit log → Detail level, and changing it requires the canManageCompliance permission (an owner-reserved capability). A new level applies to new events only.
| Tier | What it adds | Volume |
|---|---|---|
| Standard | Default. Admin mutations, plus user sign-ins (auth.sign_in), failed sign-in attempts (auth.sign_in_failed), sign-outs (auth.sign_out), and extension activity such as email sends (outlook.email_sent) and OneDrive uploads (onedrive.file_uploaded) — all recorded at every tier. | Baseline. |
| Compliance | Adds a few lower-signal events: policy-endpoint tests (policy_hook.tested), billing-portal opens (billing.portal_opened), autonomous-deployment configuration and access checks (auto_deploy.config, auto_deploy.check_access), and a row naming each member a bulk action failed on (user.bulk_<action>.failed). | Higher. |
| Forensic | Adds a per-message record (chat.message_sent) capturing the model, mode, and the count of tool calls used, plus a kb.search record per Knowledge Base lookup. When sub-agents ran, the record also carries a content-free summary: how many ran, in how many groups and how deep, their models, their tool calls by category, how each ended, how their approval requests ended (asked, approved, denied, expired, stopped, approved automatically, refused), how many approved or automatically approved actions then failed, counted as not done or as not confirmed (possibly done), and the tokens they used. No task, label or output is recorded. | Highest — roughly 30× the row volume. |
KeyAt Standard, user chat activity is not in the audit log — it lives in the
activity_logtable surfaced under Usage. Sign-ins, sign-outs, email sends and OneDrive uploads are captured at every tier. Per-message chat activity is written only at Forensic.
Tier-gated events are dropped at insert time if the org hasn't opted up, so a Standard-tier org never accumulates user-event rows it didn't choose to keep. Raising the tier applies going forward; it does not backfill.
NoteRaise the tier before the period you need it to cover — it applies going forward and never backfills. Treat the audit log as an internal control record for your own reviews and investigations, not as a compliance attestation: Kronisys holds no independent certification (SOC 2, ISO 27001, FedRAMP) and offers no BAA, and the Usage Policy and DPA prohibit submitting data regulated under HIPAA, PCI-DSS, FERPA or GLBA unless your organization has an enterprise agreement that expressly permits it.
Exporting the log
Click Export above the table (requires canExportAudit) to open the Export audit log dialog. You choose:
- Columns — any of Time, Action, Category, Actor, Target, IP address, Location, Details, and Audit ID.
- Timezone — timestamps in the CSV are rendered in the IANA timezone you pick: your browser's local timezone by default, or UTC, or any timezone your organization's members are in.
- Date range — bound the export window; it inherits any filters already active on the table.
The download is audit-<timestamp>.csv, up to 50,000 rows per export, streamed server-side from a fresh DB read at the moment you confirm. For more than 50,000 rows, run multiple narrower date-range exports and concatenate.
The API export endpoint (POST /api/admin/audit/export) without a columns field keeps the original fixed layout for existing integrations:
audit_id, actor_email, action, target_email, details, ip, created_at
NoteEvery CSV export writes a meta-audit entry of its own (
audit.exported), recorded on every tier — so a compromised admin can't drain the log via export without leaving a record.
Reading the log via API
Call POST /api/admin/audit with a JSON body (requires canViewAudit). The filters moved into the request body — a location-bearing search term does not belong in a URL that lands in access logs and browser history. Supported fields:
| Field | What it does |
|---|---|
action | Exact action match (e.g. user.role_changed). |
actionPrefix | Prefix match (e.g. user. for all user-lifecycle events). |
actorUserId / targetUserId | Filter by actor or target UUID. |
actorEmail | Exact actor email match. |
q | Free-text search across action, actor email, target email, and IP address. |
since / until | ISO-8601 time window. |
limit / offset | Pagination — limit is capped at 5,000 per call. |
cursorTs / cursorId | Keyset cursor (last row's created_at + audit_id) — the stable way to page a large window without offset drift. |
withMeta | Set true to change the response shape to { entries, total, earliest } for building pagers. |
Each returned entry carries a read-time geo field (country, city, ll) resolved from the stored IP — it is computed per request and never persisted.
KeyReading the audit log is itself an auditable act. Each distinct read — a given actor, scope, and set of filters — writes an
audit.readrow (recorded at every tier), so an admin cannot search the log for a colleague and leave no trace. Identical reads within a 10-minute window collapse into one row, so paging and the panel's live refresh never spam the log. Nothing the client sends can suppress the row.
NoteThe
GET /api/admin/auditandGET /api/admin/audit/exportendpoints from earlier releases now return a "reload" response — both the table read and the CSV export moved toPOSTso their filters travel in the body. Update any integration that still calls theGETforms.
Tamper evidence
Audit rows are sealed into a hash chain (prev_hash / row_hash). A sweep seals new rows every hour. In the console the check is the Tamper-evident log row under Organization → Data and privacy → Audit log. It runs when the page opens, and again when you come back to the section once its last answer is more than a minute old. There is no button to run it by hand: a Retry link appears only when a check could not run.
GET /api/admin/audit/verify (requires canViewAudit) seals any pending rows, re-walks your organization's chain, and recomputes every row's SHA-256. ok: false means a sealed row was edited or deleted after sealing — strong evidence of tampering at the storage layer. A break also names the row where the chain stopped verifying, in two ways: brokenAt is its audit id, and brokenAtTime is its recorded time (ISO 8601, or null when it could not be read). The console names that time the way the audit log lists events, so you can find the row there. checked counts the rows verified before the break.
On a clean pass the result also reports coverage — total rows, checked (sealed) rows, and unsealed — so you can tell full verification apart from a chain that is only partly sealed. When some rows are unsealed, predates counts the ones older than the chain (it only ever extends forward, so rows written before it started stay outside it) and pending counts the newest ones, which the next hourly seal picks up. The two add up to unsealed; both are 0 when nothing is unsealed, and both are left out when the split could not be read. Rows outside the chain are normal: the console still reports Intact and names them in one line. If total is missing, the log could not be counted and the console says some events may be outside the chain.
Every result carries checked_at. The check is limited to 10 runs a minute per organization; past that the route returns 429 with retryAfterSeconds and a matching Retry-After header.
Retention
The audit table is swept nightly by a cleanup job. Entries older than your organization's audit_retention_days setting are hard-deleted.
- Default retention: 365 days.
- Allowed range: 30 to 3650 days. The console refuses a value outside it; the API clamps one.
- Set under Organization → Data and privacy → Audit log → Keep audit entries for: 90 days, 1 year, 3 years, 7 years, or Custom. Changing it requires
canManageCompliance. Shortening it asks you to confirm first and counts the entries that would be deleted — for example This shortens the window from 365 to 90 days., with Delete after 90 days or Keep 365.
The sweeper runs once on server boot, then every 24 hours, per-organization so each tenant's policy is honored independently. There is no way to recover entries older than the retention window — set the value to match your compliance posture before any cleanup runs.
WarningMost financial firms keep 7 years (2555 days) for compliance. Pick your retention deliberately; deletion is irreversible.
What can go wrong
| Error | What it means |
|---|---|
Admin access required | Your role does not grant canAccessAdmin. |
Permission required: canViewAudit | Your admin role does not include audit read. |
Permission required: canExportAudit | You can view the table but cannot download the CSV. |
| No Keep audit entries for or Detail level row on Data and privacy | Both need canManageCompliance, which is owner-reserved; without it the rows are left out. A direct PATCH answers You don't have permission to change compliance settings (audit retention / detail). |
| Check limit reached. Try again in N seconds. | The tamper check is limited to 10 runs a minute per organization. The seconds count down on the row, and Retry comes back when the wait is over. |
| CSV export is shorter than expected | The CSV is capped at 50,000 rows. Narrow the date range and export in slices. |
| Flags or the map don't render | Geolocation is best-effort — private-network IPs and unresolvable addresses simply show no flag and no map. The record itself is unaffected. |
See Rate limits and quotas for rate-limit logging (separate from audit), and Organization deletion for how the audit log behaves around an org deletion.