> ## Documentation Index
> Fetch the complete documentation index at: https://api-documentation.kare-app.fr/llms.txt
> Use this file to discover all available pages before exploring further.

# Audit & History

> One audit trail, two uses — RGPD compliance and the product timeline. Write path, read path, data model.

## Why one trail

KARE records a single technical audit trail (`audit_logs`) that serves **two needs from one source of truth**:

* **Compliance / RGPD** — who did what, when, on which entity.
* **Product history** — the timeline on an entity's detail view (e.g. an anomaly: *"Status: Pending → Confirmed by Jean"*).

The same rows feed both: the product timeline is a filtered, projected read of the trail.

***

## Write path

```mermaid theme={null}
flowchart LR
  REQ[HTTP request] --> CTX[audit-context plugin resolves actor]
  CTX --> TX[mutation inside auditedTx]
  TX --> AR[auditRecord - semantic event]
  TX --> DR[diffAndRecord - field diff, kind=auto]
  AR --> SINK[(per-tx sink)]
  DR --> SINK
  SINK -->|commit| LOGS[(audit_logs)]
  SINK -->|rollback| DROP[discarded - nothing recorded]
```

Every audited write goes through `prisma.auditedTx(fn)`. Inside it, two helpers append entries to a per-transaction buffer (the *sink*), which is flushed to `audit_logs` **atomically on commit**. A rollback records nothing — there is no audit row for a failed mutation.

| Helper                                                | Produces            | Use for                                                                                      |
| ----------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------- |
| `auditRecord({ action, entityType, entityId })`       | `kind = 'semantic'` | Named business events (`anomaly.status_resolved`, `template.archived`)                       |
| `diffAndRecord(before, after, { fields, action, … })` | `kind = 'auto'`     | Field-level changes; stores only the changed keys in `before`/`after` (e.g. `status: 1 → 2`) |

For events **outside** a transaction (login, access), `emit()` writes a single best-effort row.

The actor is resolved per request (RS/BO/AD auth) and **denormalised** into every row, so the trail survives a user's deletion or anonymisation.

***

## Read path

```mermaid theme={null}
flowchart LR
  UI[Front - entity detail] -->|"GET /audit/v1/:entityType/:entityId"| ROUTE[audit route RS]
  ROUTE --> SCOPE[requireContext.rs - org scope + AUDIT_READ]
  SCOPE --> QUERY[paginate audit_logs by entity and org, newest-first]
  QUERY --> PROJ[projectAuditRow - timeline DTO]
  PROJ --> UI
```

`GET /audit/v1/:entityType/:entityId` returns an entity's timeline: **org-scoped**, **offset-paginated**, newest-first. It's backed by the `audit_logs_entity_timeline_idx (entityType, entityId, createdAt desc)` index, so the scan is index-ordered (no sort step). The response is the standard `{ data, totalCount, pagination }` envelope; each item carries the actor, the `action` key (for i18n on the front) and the field diff.

***

## Data model — `audit_logs`

| Column                                                           | Meaning                                                                                    |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `id`, `createdAt`                                                | UUIDv7 id, write time (the timeline order)                                                 |
| `actorType`                                                      | `user` · `na` · `system` · `provider` · `anonymous`                                        |
| `actorUserId`, `actorEmail`, `actorAudience`, `displayActorName` | denormalised actor (no FK — survives erasure)                                              |
| `organizationId`, `siteId`                                       | tenant scope                                                                               |
| `kind`                                                           | `semantic` (business event) or `auto` (field diff)                                         |
| `category`                                                       | `data` · `auth` · `access` · `system`                                                      |
| `action`                                                         | event key, free-form string (e.g. `anomaly.status_resolved`)                               |
| `entityType`, `entityId`                                         | the audited entity (the timeline key)                                                      |
| `changedFields[]`, `before`, `after`                             | the diff (only changed keys; secrets redacted at write time)                               |
| `payload`, `metadata`                                            | render extras (comment, attachment ref, i18n params) / request context (ip, ua, requestId) |
| `sensitive`                                                      | flag for rows needing extra care                                                           |

`action` is intentionally a free-form string, never a Postgres enum: the catalogue (`@lib/audit/action-keys`) evolves without a migration.

***

## Actor — not necessarily a `User`

The actor is a **discriminated union**, so every action is attributed even when no account is involved:

| `actorType` | Who                               | Stored                                |
| ----------- | --------------------------------- | ------------------------------------- |
| `user`      | a signed-in user                  | userId, email, audience, displayName  |
| `na`        | identified without a full account | userId, displayName                   |
| `system`    | cron / workers                    | label (`"system"`)                    |
| `provider`  | external party via PIN / link     | label + token hash (never the secret) |
| `anonymous` | unidentified                      | ip                                    |

`displayActorName` is always set — the human-readable label the timeline shows, even for a system or anonymous action. `actorEmail` is **not** exposed on the product timeline (`displayActorName` is enough).

***

## RGPD

* **Redaction at write time** — values under secret-looking keys are replaced before storage (`redactValue`).
* **Erasure** — `eraseUserFromAudit(userId)` anonymises the actor side (`anonymizeAuditActor`) and clears subject-side PII (`eraseAuditSubject`: the `before`/`after`/`payload` of rows about that user), keeping who/when/action.
* **Minimal PII** — prefer storing ids + a denormalised `displayActorName` over raw PII, so erasure stays a single update.

### Follow-ups (not yet wired)

* Branch `eraseUserFromAudit` into the user-deletion flow.
* Retention / purge policy (legal decision on duration) + DB-enforced immutability (`REVOKE UPDATE/DELETE`).
* PII embedded in *other* entities' diffs (e.g. an assignee name) is out of scope of the targeted erasure.
