> For the complete documentation index, see [llms.txt](https://mercure-technologies.gitbook.io/expo-open-ota/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mercure-technologies.gitbook.io/expo-open-ota/security/audit-log.md).

# Audit log

{% hint style="warning" %}
The audit log is an Enterprise feature and requires [control-plane mode](/expo-open-ota/controle-plane-mode/overview.md). Events are only collected while an active Enterprise license is present (see [open-core-and-licensing](/expo-open-ota/open-core-and-licensing.md)).
{% endhint %}

The audit log records every state-changing action performed on the server: who did it, what was touched, on which app, when, from which address, and whether it succeeded. Entries are append-only, and they survive the deletion of everything they mention, so the history of a deleted user, app or API token stays readable. Admins consult the log from the dashboard, under **Audit log** in the Access & Security section.

## What an entry contains

Each entry carries an actor (a dashboard user, an API token, or the server itself), an action, the target the action applied to, the app it belongs to when it is app-scoped, an outcome (`success`, `denied` or `failure`), the client IP address and user agent, and action-specific details in a metadata object. Names are captured at the moment of the event: an entry about a token that was revoked last month still shows the token's name, and the actions of a deleted user still show their email.

Metadata never contains secrets. An entry about an API token carries its name and its masked hint, never key material, and the SSO configuration entries carry the security toggles, never the client secret.

## What is recorded

This section is the reference of every action the log can contain. New releases that add actions list them here.

### Sign-ins and accounts

| Action                                         | Recorded when                                                                                                                                              |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user.login`                                   | A dashboard sign-in with email and password. Failures are recorded too, with the attempted email and the reason, which makes brute-force attempts visible. |
| `user.sso_login`                               | A dashboard sign-in through SSO.                                                                                                                           |
| `user.password_changed`                        | An account changed its own password.                                                                                                                       |
| `user.created`                                 | An admin created an account.                                                                                                                               |
| `user.updated`                                 | An admin revoked an account's access (the new state is in the metadata).                                                                                   |
| `user.approved`                                | An admin approved a pending account.                                                                                                                       |
| `user.deleted`                                 | An admin deleted an account.                                                                                                                               |
| `user.sso_provisioned`                         | A first SSO sign-in created the account automatically.                                                                                                     |
| `user.sso_linked`                              | A first SSO sign-in attached an identity-provider subject to an existing account.                                                                          |
| `user.admin_granted`                           | An admin granted the admin flag to an account.                                                                                                             |
| `user.admin_revoked`                           | An admin removed the admin flag from an account.                                                                                                           |
| `role.created`, `role.updated`, `role.deleted` | A role was created, edited or deleted.                                                                                                                     |
| `user.grants_updated`                          | A member's per-app grants were replaced (the new grants are in the metadata).                                                                              |

### Enterprise administration

| Action               | Recorded when                                                                                                                                           |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `license.activated`  | The Enterprise license key was activated (license id and expiry in the metadata).                                                                       |
| `license.removed`    | The license key was removed. It is the last entry the collection gate lets through before turning off.                                                  |
| `sso_config.saved`   | The SSO configuration was saved. The security toggles (manual user validation, unverified-email trust, allowed domains and groups) are in the metadata. |
| `sso_config.deleted` | The SSO configuration was deleted.                                                                                                                      |

### App management

| Action                                      | Recorded when                                                        |
| ------------------------------------------- | -------------------------------------------------------------------- |
| `app.created`, `app.renamed`, `app.deleted` | An app was created, renamed (both names in the metadata) or deleted. |
| `channel.created`, `channel.deleted`        | A channel was created or deleted.                                    |
| `channel_branch.mapped`                     | A channel was pointed at a branch.                                   |
| `branch.created`, `branch.deleted`          | A branch was created or deleted from the dashboard.                  |

### Delivery

| Action                                                                        | Recorded when                                                                                                    |
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `update.published`                                                            | A [publish](/expo-open-ota/eoas/publish-an-update.md) completed and the update became servable.                  |
| `update.rollback`                                                             | A rollback was published.                                                                                        |
| `update.republished`                                                          | An existing update was republished.                                                                              |
| `channel_rollout.started`, `channel_rollout.updated`, `channel_rollout.ended` | A channel rollout started, changed percentage, or ended (the metadata says whether it was promoted or reverted). |
| `update_rollout.set`, `update_rollout.reverted`                               | A per-update rollout percentage was set, or the rollout was reverted.                                            |

### Credentials and key material

| Action                               | Recorded when                                                                                                                                                            |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `api_key.created`, `api_key.revoked` | An API token was created or revoked.                                                                                                                                     |
| `api_key.restrictions_updated`       | A token's [IP whitelist](/expo-open-ota/security/ip-whitelisting.md) or protected-branch access changed. The metadata carries the allow-list as it is actually enforced. |
| `branch_protection.updated`          | A branch was marked [protected](/expo-open-ota/security/branch-protection.md) or unprotected.                                                                            |
| `certificate.downloaded`             | The code-signing certificate was downloaded. It is the only read the log records, because it is key material.                                                            |

### Access control

| Action              | Recorded when                                                                                                      |
| ------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `permission.denied` | A member attempted an action their role does not allow. The metadata names the missing permission and the request. |

Refusals only ever appear as `permission.denied`: every other action is recorded once it actually happened, so an `app.deleted` entry always means the app was really deleted.

## Consult the log

The dashboard page shows the log newest first, fifty entries at a time, with the filtered total next to the filters. You can narrow it by actor (pick a user or an API token from the list, or click the actor of any entry to filter on it, which also works for actors that no longer exist), by action, by app, by outcome, and by date range. Clicking an entry opens its full detail, including the metadata.

The page is only visible to admins.

## Retention

Entries are kept in the database for `AUDIT_LOG_RETENTION_DAYS` days (550 by default, about a year and a half) and a daily purge removes what is older. If you need a longer history, enable the archive below: purged entries live on in your storage.

## Archive to your storage

The server can export the log to your object storage as it grows, both as a long-term archive and as the ingestion point for a SIEM: Splunk, Datadog, Panther and Elastic all read S3-compatible buckets natively, and with local storage any log shipper can tail the directory.

{% stepper %}
{% step %}

#### Create a dedicated destination

Create a bucket, container or directory that is only for audit logs. The server refuses to reuse the updates destination on purpose: audit data deserves its own access rules, and your update tooling has no business reading it.
{% endstep %}

{% step %}

#### Enable the archive

Set `ARCHIVE_AUDIT_LOGS=true` and the destination variable matching your `STORAGE_MODE`:

| Storage mode | Variable                                                  |
| ------------ | --------------------------------------------------------- |
| `s3`         | `S3_BUCKET_AUDIT_LOGS_NAME`                               |
| `gcs`        | `GCS_BUCKET_AUDIT_LOGS_NAME`                              |
| `azure`      | `AZURE_BLOB_AUDIT_LOGS_CONTAINER_NAME`                    |
| `local`      | `LOCAL_AUDIT_LOGS_BASE_PATH` (defaults to `./audit-logs`) |

If the archive is enabled but the destination is missing or identical to the updates one, the server refuses to start rather than run without its archive.
{% endstep %}

{% step %}

#### Tune the interval if needed

The exporter runs at boot, then every `AUDIT_LOGS_EXPORT_INTERVAL_SECONDS` (300 by default). Events therefore appear in the archive at most a few minutes after the action, which is the same delivery model as CloudTrail.
{% endstep %}
{% endstepper %}

### The file format

Files are written as `YYYY/MM/DD/<firstId>-<lastId>.ndjson` under the day of their first event, one JSON object per line, with the same field names as the dashboard API:

```json
{"id":42,"occurredAt":"2026-07-22T09:14:03Z","actorType":"api_key","actorId":"7","actorDisplay":"ci-production","action":"update.published","targetType":"update","targetId":"1753175643000","appId":"0b3c...","outcome":"success","ip":"203.0.113.7","metadata":{"branch":"main","runtime_version":"1","platform":"ios"}}
```

The exporter keeps a cursor in the database, so it is safe with several replicas, a failed upload is retried at the next run without losing anything, and restarting the server resumes where it left off. The retention purge never touches the archive: files stay until your own lifecycle rules decide otherwise.

Access control on the destination, retention of the files, and write-once storage if your compliance program requires it (S3 Object Lock, for example) are yours to configure: the server writes the files, your infrastructure owns them.

## See also

[Branch protection](/expo-open-ota/security/branch-protection.md) and [IP whitelisting](/expo-open-ota/security/ip-whitelisting.md) restrict what API tokens can do; the audit log records what they did.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://mercure-technologies.gitbook.io/expo-open-ota/security/audit-log.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
