---
type: Axis
title: retention (data-lifetime axis)
description: The retention/data-lifetime architecture axis — how long persisted data is kept and on what basis it is removed (none → TTL → retention-policy → legal-hold). A per-entity-with-default axis whose commitment surface is the persistent-data schema (the lifetime rule + purge machinery). DEEP (RFC #75 5A.5 / P1 #851); worked example is the action-log / WAL growth and the append-log monotonic-growth scar.
tags: [axis, architecture, retention, lifetime, ttl, purge, legal-hold, rfc-75, deep]
timestamp: 2026-06-20T00:00:00Z
---

# Retention (data-lifetime axis)

> **DEEP axis (P1 #851).** Full §6 depth with a worked example from the skill's
> own history — the unbounded action-log / WAL growth (82MB WAL, #769) and the
> append-log monotonic-growth scar. Distinct from `temporality` (whether you
> record *when* something happened) and `persistence` (how *durably* it
> survives): retention is how long it is *kept* and what *forces* its removal.

## Concept

Retention is the **data-lifetime** axis (RFC #75 §3): how long an entity's data
is kept and on what basis it is removed. The ladder runs none (kept forever; no
removal mechanism) → TTL (a time-to-live; rows expire after a fixed age) →
retention-policy (a tiered/conditional policy: keep N versions, archive after
M, purge on event) → legal-hold (a policy plus an overriding hold that *freezes*
deletion for named data regardless of TTL). Its **commitment surface is the
persistent-data schema** (§4): the lifetime rule binds to the store — a purge
that ran is irreversible, and data kept past its policy is a liability. Its
**scope is per-entity-with-default** (§5): a system-wide default ("keep
forever") with per-entity lifetime rules that must be declared.

## Why it is an axis (discriminator test — §2)

1. **Ordered tiers** — each rung adds a stronger, more conditional removal rule
   (none ⊂ TTL ⊂ policy ⊂ policy-with-hold).
2. **Cross-cutting bundle** — a rung dictates a purge/archive job, the storage
   budget, the audit of what was removed, the legal-hold override path, and the
   tests that prove expiry happens (and that a hold blocks it).
3. **Asymmetric migration cost** — adding retention to a store that grew
   unbounded is a one-way purge (you cannot un-delete what TTL removed);
   conversely, adopting legal-hold on a store that was already purging means the
   data needed for the hold may already be gone.
4. **Requirement-keyed selection** — falsifiable probes (below) pick the rung.

## Tiers (the ladder)

| Tier | Adds | Commitment surface | Forecloses next-cost |
|---|---|---|---|
| `none` | nothing | persistent-data-schema | trivial → add a TTL |
| `ttl` | age-based expiry | persistent-data-schema | low; coarse, no exceptions |
| `retention-policy` | conditional/tiered rules | persistent-data-schema | medium; needs an override path |
| `legal-hold` | deletion freeze | durable-runtime-state | high; hold state everywhere |

## Schema view (the §6 entry — validated by `axis_catalog`)

```yaml
id: retention
kind: architecture-axis
scope: per-entity-with-default
default_tier: none
maintenance_discipline: >-
  data is KEPT FOREVER by default — no row is removed unless a declared lifetime
  rule exists. A per-entity TTL/policy is declared in the entity record's
  axis-materialization facet; the purge job that enforces it is idempotent and
  auditable (it records WHAT it removed). The discipline forbids unbounded
  append-only growth without an explicit "none — keep forever, by decision"
  declaration (the 82MB-WAL / action-log scar, #769): unbounded must be CHOSEN,
  not defaulted-into by neglect. A legal-hold OVERRIDES any TTL and must be
  checked before every purge. Per-entity deviations live in the entity record.
selection:
  probes:
    - "Is there any obligation or budget to remove old data? → no → none (but declare it)."
    - "Should rows simply expire after a fixed age? → ttl."
    - "Do different rows have different lifetimes / archive-then-purge stages? → retention-policy."
    - "Must deletion be FREEZABLE for litigation/audit on named data? → legal-hold."
    - "Is the store growing without bound and nobody chose that? → it wants at least a TTL."
composes_with:
  - "per-tenant retention = tenancy × temporality (and retention sets the lifetime)"
  - "secure expiry = sensitivity × retention (PII often MUST expire)"
  - "history retention = retention × temporality (how long the append-log is kept)"
applies_when: []
tiers:
  - tier: none
    commitment_delta:
      surface: persistent-data-schema
      delta: "data is kept indefinitely; there is no removal mechanism at all. Must be a deliberate decision, not neglect."
    admin_tool: "none — a storage-growth report is the only signal."
    enforcement:
      - "no purge runs; storage grows monotonically (acceptable only by explicit decision)."
    tests:
      - "old rows are still present after an arbitrary age (the keep-forever contract holds)."
    forecloses: "any storage-budget guarantee; bounded growth."
    migration_cost_to_next: "low — add a created_at-based TTL + a purge job."
  - tier: ttl
    commitment_delta:
      surface: persistent-data-schema
      delta: "rows expire after a fixed age; a purge job removes anything older than the TTL. Coarse, uniform, no per-row exceptions."
    admin_tool: "TTL config + purge-job runner + a removed-count report."
    enforcement:
      - "the purge job runs on a schedule and removes rows past the TTL; it is idempotent."
    tests:
      - "a row older than the TTL is purged; a younger row survives; re-running the purge is a no-op."
    forecloses: "keeping any row past the uniform TTL; per-row lifetimes."
    migration_cost_to_next: "medium — generalize to a conditional/tiered retention policy."
  - tier: retention-policy
    commitment_delta:
      surface: persistent-data-schema
      delta: "a tiered/conditional policy: keep N versions, archive after M, purge on event; different rows have different lifetimes."
    admin_tool: "policy editor; archive store + an audit of what was archived/purged when."
    enforcement:
      - "the policy engine evaluates each row's class/age/event and archives or purges accordingly; every removal is audited."
    tests:
      - "a row in class A is archived while class B is purged; the removal audit records both."
    forecloses: "treating all rows uniformly; the policy is now load-bearing."
    migration_cost_to_next: "high — add a legal-hold override that freezes deletion for named data."
  - tier: legal-hold
    commitment_delta:
      surface: durable-runtime-state
      delta: "a hold state that OVERRIDES the retention policy: named data cannot be deleted while held, even past its TTL."
    admin_tool: "hold-management console (place/release hold); held-data inventory."
    enforcement:
      - "every purge checks for an active hold first and skips held rows; placing a hold is itself audited."
    tests:
      - "held data survives a purge that would otherwise remove it; releasing the hold lets the next purge remove it."
    forecloses: "unconditional deletion; the purge path now branches on hold state."
    migration_cost_to_next: "n/a — top of the ladder."
examples:
  - "The skill's own climb (mostly NONE by decision): the canonical store keeps issues/discussions forever (none — chosen). The action-log + WAL grew UNBOUNDED until it bit — an 82MB WAL starved the daemon (#769) and forced a TRUNCATE-checkpoint watchdog; that is the 'unbounded by neglect, not decision' failure this axis names. The append-only history log (#270) is the secure-expiry tension: an append-log at retention=none can never expunge a row, which collides with sensitivity (a PII/secret value written there is permanent)."
```

## Selection procedure (§8)

Bidirectional check per entity under the keep-forever default. The escalation
pass asks "is the store growing without bound, and did anyone *choose* that?" —
unbounded-by-neglect forces at least a TTL. The de-escalation pass is
constrained by holds and obligations (a PII column often *must* expire).
Recorded in the entity record; the systems-analysis D-checks flag an unbounded
append-only store with no declared retention decision (5A.5).

## Composition (§5)

Retention is half of several **products** (derived, not authored here): secure
expiry = **sensitivity × retention** (PII frequently *must* be removable);
history retention = **retention × temporality** (how long the append-log /
bitemporal record is kept); per-tenant retention extends the
**tenancy × temporality** product with a per-tenant lifetime. The interaction
table derives these (5A.3).
