Owner: platform team Last reviewed: 2026-05-28
This doc specifies how long logs are kept by tier and by domain, and what triggers a purge. It exists because:
- Compliance audits want to know retention windows up front.
- On-call needs to know how far back they can grep when triaging long-tail issues.
- DBAs need to know what storage to provision for log indexes.
Tiers#
| Tier | Retention | Storage | Notes |
|---|---|---|---|
| Application (info+) | 7 days | CloudWatch / Loki hot | Service-level events, healthy operation |
| Application (debug) | 24 hours | CloudWatch / Loki hot | Only on when a debug flag is toggled |
| Error | 30 days | CloudWatch / Loki hot + S3 cold copy at 7d | Stack traces, 5xx responses, unhandled exceptions |
| Audit (security) | 90 days hot, 7 years cold | Elasticsearch / S3 (Glacier) | Tamper-evident; never editable in place |
| Access (request) | 14 days | CloudWatch / Loki hot | HTTP request line, status, duration |
| Audit (admin) | 365 days hot, 7 years cold | Elasticsearch / S3 (Glacier) | Privileged-user actions, immutable |
| Privacy events (consent, deletion) | 7 years | Elasticsearch / S3 (Glacier) | GDPR Article 30 record-of-processing |
Per-domain retention overrides#
Only document overrides here; the tier above is the default.
| Domain | Override | Why |
|---|---|---|
| aphrodite/payment | Audit logs at 10 years hot/cold | PCI scope, financial records |
| lilith/svc-auth | Login audit kept 180 days hot | Investigations on suspicious-login chains often span >90 days |
| iris/api memory snapshots | Per-user-controlled retention via consent records | Users opt-in to a retention window between 24h and 12mo |
Purge mechanism#
- CloudWatch / Loki hot tier — automatic by log-group retention setting. Set via Terraform; never via console.
- S3 cold tier — lifecycle rule per bucket: transition to Glacier at the retention boundary; delete at the audit boundary.
- Elasticsearch — ILM policy per index alias, deletes the closed index when older than retention.
- Postgres
audit_log/audit_logstables (bellona, hathor, isis, sophia, yemaya) — the sweeper atscripts/retention/audit-log-retention.tsdeletes rows older than the configured retention window. Defaults to the "Audit (security)" 90-day tier above; per-domain overrides go through<DOMAIN>_AUDIT_RETENTION_DAYS(e.g.LILITH_AUDIT_RETENTION_DAYS=180for the svc-auth override). Cron schedule:15 3 * * * npx tsx scripts/retention/audit-log-retention.ts --apply. The script defaults to--dry-run; the cron expression must include--applyexplicitly. It batches DELETEs (10_000 rows / batch with a 100ms pause) so it never holds a long table lock, and exits non-zero if any configured domain hit a SQL error so the cron alert routing fires. - Oshun BFF
admin_audit_event— retained for the canonical 2,555-dayaudit_eventcompliance window. The BFF evaluates expiry on startup and at least daily, and defers deletion while a referenced actor, target, subject, tenant, or owner has an active legal hold or retention exception. Successful reads requireadmin:*, write and durably flush anaudit.access.*event, and are HMAC-SHA-256 chained with a production key held outside PostgreSQL. The operator-readable contract isGET /v1/admin/audit-log/policy; the machine-reviewed control record isdocs/audits/eve-sota-retention-access-policy/2026-09-15.json. - Tombstone audit events (
oshun_v1_tombstone_audit_events) — these are governed by the privacy-event tier (7 years) and pruned by the privacy-team workflow, not by the retention cron above. Consent records are never deleted by the sweeper.
What is NOT a "log" under this policy#
- Database row-level audit columns (
created_at,updated_at,deleted_at) — these are part of the schema retention policy, not log retention. - User-generated content (posts, recordings, profile fields) — governed by the
per-domain data-retention rules in
docs/compliance/data-retention.md(TBD). - Backup files — governed by
docs/runbooks/database-backups.md(Postgres) anddocs/runbooks/object-storage-backups.md(TBD, task #108).
Adding a new log stream#
- Tag every event with the tier name (
app.info,audit.security, etc.) at emit time so the routing layer can apply the right retention. - If the stream needs an override, add it to the table above in the same PR that introduces the stream.
- If the override is more permissive than the default (longer retention), confirm with the platform team — the budget impact is real.