---
title: "Compliance"
description: "Integrity, redact presets, GDPR vs append-only, retention windows, and the most common pitfalls when shipping audit logs to production."
canonical_url: "https://www.evlog.dev/use-cases/audit/compliance"
---
# Compliance

> Integrity, redact presets, GDPR vs append-only, retention windows, and the most common pitfalls when shipping audit logs to production.

Compliance frameworks (SOC2, HIPAA, GDPR, PCI) ask the same five questions of every audit log: **who, what, when, from where, with which outcome**, plus **how do we know it wasn't tampered with**. evlog answers each one through composition of the existing primitives.

## Integrity

Hash-chain the audit log so any tampering is detectable. Each event's hash includes the previous hash, so deleting a row breaks the chain forward of that point.

```typescript
auditOnly(
  signed(createFsDrain({ dir: '.audit' }), { strategy: 'hash-chain' }),
  { await: true },
)
```

<warning>

**Rotate secret for HMAC-signed audits annually.** When you rotate, embed a key id alongside the signature (e.g. extend `AuditFields` with `keyId` via `declare module`) so old events stay verifiable against the previous secret. Verifiers should look up the key by id, not assume a single global secret.

</warning>

See [Drains & Integrity](https://www.evlog.dev/use-cases/audit/pipeline#signed) for the difference between HMAC and hash-chain.

## Redact

Audit events run through your existing `RedactConfig`. Compose with the strict audit preset to harden PII handling:

```typescript
import { auditRedactPreset } from 'evlog'

initLogger({
  redact: {
    paths: [
      ...(auditRedactPreset.paths ?? []),
    ],
  },
})
```

The preset redacts `authorization`, `cookie`, `set-cookie`, and credential key names (`password`, `passwordHash`, `token`, `apiKey`, `secret`, `accessToken`, `refreshToken`, `cardNumber`, `cvv`, `ssn`) **at any nesting depth**, including inside `audit.changes.before` / `audit.changes.after`. The [`secrets` built-in](https://www.evlog.dev/learn/redaction) also catches any key ending in `password`, `passwd`, `secret` or `token`, so a key such as `newPassword` is caught even when the preset does not name it.

## GDPR vs append-only

Append-only audit logs collide with GDPR's right to be forgotten. Recommended pattern today:

1. Keep audit rows immutable.
2. Encrypt PII fields with a per-actor key (held outside the audit store).
3. To "forget" a user, delete their key. The audit row stays, the chain stays valid, the PII becomes unreadable.

A built-in `cryptoShredding` helper is on the [follow-up roadmap](https://github.com/evloghq/evlog/issues).

## Retention

Retention is a storage-layer concern by design. evlog's audit layer doesn't enforce retention windows because every supported drain already has a stronger, audited mechanism for it. Pick the one matching your drain:

<table>
<thead>
  <tr>
    <th>
      Drain
    </th>
    
    <th>
      Retention mechanism
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      FS
    </td>
    
    <td>
      Combine <code>
        createFsDrain({ maxFiles })
      </code>
      
       with a daily compactor.
    </td>
  </tr>
  
  <tr>
    <td>
      Postgres
    </td>
    
    <td>
      Schedule <code>
        DELETE FROM audit_events WHERE timestamp < now() - interval '7 years'
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      Axiom / Datadog / Loki
    </td>
    
    <td>
      Set the dataset retention policy in the platform.
    </td>
  </tr>
  
  <tr>
    <td>
      S3 Object Lock
    </td>
    
    <td>
      Configure lifecycle rules + Object Lock retention period.
    </td>
  </tr>
</tbody>
</table>

Document the chosen window in your security policy. Auditors care about the written rule, not the enforcing component.

## Common Pitfalls

- **Logging only successes.** Auditors care most about denials. Always pair `log.audit()` with `log.audit.deny()` on the negative branch of every authorisation check.
- **Leaking PII through changes.** `auditDiff()` runs through your `RedactConfig`, but only if the field paths are listed. Add `password`, `token`, `apiKey`, etc. once globally so you never have to think about it again.
- **Treating audits as observability.** Don't sample, downsample, or summarise audit events. Force-keep is on by default. Do not disable it.
- **Conflating actor.id with the session id.** `actor.id` is the stable user id (or system identity). Correlate sessions via `context.requestId` / `context.traceId`, never via the actor.
- **Forgetting standalone jobs.** Cron tasks, queue workers, and CLIs trigger audit-worthy actions too. Use `audit()` (no request) or `withAudit()` to keep coverage parity with your HTTP routes.
- **Skipping await: true on the audit drain.** Without it, audits are fire-and-forget. A crash between the event being emitted and the drain flushing means the action happened but no audit row exists.

---

- [Drains & Integrity](https://www.evlog.dev/use-cases/audit/pipeline)
- [Recipes](https://www.evlog.dev/use-cases/audit/recipes)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
