> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Audit Log

> After this ships, every state change in the platform can be traced to who did it, what it changed from and to, and which request caused it.

## Context

ADR-017 chose CQRS-lite over event sourcing, and the audit log is what makes that trade honest:
without a complete change record, rejecting event sourcing would mean giving up the ability to
answer "why is this row like this".

It has a second job. Ley 21.719 audits **operational evidence**, and the audit log is the evidence:
who accessed a national ID, when a consent changed, which operator impersonated which tenant. That
is why access reads are logged and not only writes.

It is delivered early because it has no dependencies and everything depends on it. A command
handler written before the audit log exists will be written without it.

## Scope *(normative)*

* `core.audit_log`, append-only, monthly RANGE partitioned.
* Typed actor: `user | member | api_key | system`, with id.
* Full old→new diff of the changed entity.
* `correlation_id` propagation from the request that caused it.
* The helper every command handler calls, inside the command transaction.
* Read endpoints for the tenant's own trail and for Softcrum Ops.
* Retention per plan, with the legal minimum applying independently.

## Non-scope *(normative)*

* Application logging and traces — that is `packages/observability`, a different concern with a
  different retention and a different destination.
* Alerting on audit patterns — a later security feature.
* Immutability enforced by cryptography. Append-only plus restricted grants is the F1 posture; a
  hash chain is a possible later hardening.

## Behaviour *(normative)*

1. Every command writes exactly one audit row **inside its own transaction**. If the command rolls
   back, the audit row rolls back with it — an audit entry for something that did not happen is
   worse than none.
2. The actor is **always typed and always present**. `system` is a valid actor; unknown is not.
3. The diff is `old` → `new` over the changed fields, complete (DEC-C2). Sensitive values are
   recorded **masked**: the log proves the change happened without becoming a second copy of the
   national ID.
4. `correlation_id` is mandatory and inherited from the originating request, so an audit row joins
   to the events, jobs and sends it caused.
5. The table is **append-only**. No update path, no delete path, and the application role holds no
   grant for either.
6. Reads that expose sensitive data write their own audit row — reading a full national ID is an
   auditable act (DEC-A4).
7. Partitioned monthly on `occurred_at`; every query carries the partition-key predicate.
8. Retention follows the plan tier, and the **legal minimum applies independently and wins** when
   it is longer.
9. A tenant can read its own trail, including operator impersonation performed against it. Hiding
   that would defeat the purpose of logging it.

## Data *(normative)*

| Table            | Key invariants                                                                                                                                                                                                                                                                |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core.audit_log` | append-only; `tenant_id`, `cell_id`; `actor_type`, `actor_id`; `entity_type`, `entity_id`; `action`; `diff` JSONB with sensitive values masked; `correlation_id`; `occurred_at`; monthly RANGE partition; index (`tenant_id`, `entity_type`, `entity_id`, `occurred_at DESC`) |

## API *(normative)*

| Endpoint                 | Class      | Permission            | Budget                                            |
| ------------------------ | ---------- | --------------------- | ------------------------------------------------- |
| `GET /v1/core/audit-log` | Management | `core.audit_log.read` | p95 \<1 s, cursor pagination, time range required |

The time range is **required**, not defaulted — it is the partition key, and an unbounded audit
query is a full scan of the largest table in the schema.

## Events *(normative)*

None. The audit log is a consumer of everything and a producer of nothing; emitting an event about
having audited something would be a loop with no consumer.

## Acceptance criteria *(normative)*

1. A command that rolls back leaves no audit row.
2. Every command handler in the codebase writes an audit row — enforced by a lint rule, not by
   review.
3. The diff of an update contains exactly the changed fields, with old and new values.
4. A national ID appearing in a diff is masked.
5. Reading a full national ID produces an audit row naming the actor and the contact.
6. An audit query without a time range is rejected with a typed error.
7. Update and delete attempts fail at the database grant level, not only in application code.
8. **Negative:** no audit row exists with an untyped or absent actor.

## Execution

Single slice, synchronous command. Schema and partitions in TS-001; the handler helper in
`packages/core` so every module writes the same shape.

## Open questions

| # | Question                                                                                     | Decides          | By                       |
| - | -------------------------------------------------------------------------------------------- | ---------------- | ------------------------ |
| 1 | Is the legal minimum retention 5 years (tax-adjacent) or shorter for non-financial entities? | Daniel + abogado | before commercial launch |
| 2 | Do we hash-chain rows now, or accept append-only plus grants for F1?                         | Daniel           | before approval          |

## Changelog

| Version | Date       | Change        | Why | Author                 |
| ------- | ---------- | ------------- | --- | ---------------------- |
| 0.1.0   | 2026-08-17 | Initial draft | —   | daniel + claude-opus-5 |

## Delivery record

*Not implemented yet.*
