Skip to content

Retention Lifecycle

Derived from registers/retention-schedule.md (UK GDPR Art 5(1)(e), controls PRIV-03/04, SEC-12) and technical/data-architecture.md. Retention is real and enforced by scheduled worker sweeps — not aspirational — but all durations are placeholders [counsel required] (C4) and are env-configurable so counsel's numbers need no code change.

Data class → retention → deletion

flowchart LR
  %% source: registers/retention-schedule.md (durations are counsel placeholders, C4)
  job["retention Cloud Run Job<br/>(scheduled sweep, gated by RETENTION_ENABLED)"]

  subgraph swept["Swept by the retention job"]
    sessions["app_sessions<br/>expired/revoked → delete after 30d"]
    presences["flight_presences<br/>expired (TTL ≤48h) → delete after 7d"]
    candidates["match_candidates (non-matched)<br/>terminal → delete after 30d"]
    convos["matched conversations + messages<br/>→ delete 90d after last activity<br/>(FK cascade via owning candidate)"]
    softdel["soft-deleted users<br/>→ hard delete after 7d grace"]
  end

  job --> sessions
  job --> presences
  job --> candidates
  job --> convos
  job --> softdel

  softdel --> cascade["FK cascades: profiles, identities, consents,<br/>actor-linked events, requests, rationales,<br/>blocks, messages"]

  subgraph onerasure["Deleted on account erasure (no independent sweep yet)"]
    profile["traveler_profiles — life of account"]
    linked["external_identities (LinkedIn PII) — life of account"]
    consents["user_consents — life of account"]
    blocks["user_blocks — life of account (safety)"]
    requests["connection_requests / accepted connections —<br/>independent duration TBD (counsel C28)"]
    rationales["match_rationales — cache expiry or erasure"]
  end
  cascade --> onerasure

  subgraph selfexp["Self-expiring (Redis)"]
    redis["OAuth state ~10 min · handoff codes 120 s ·<br/>rate-limit counters per window"]
  end

  subgraph outside["Outside the erasure boundary"]
    ml["Anonymous ML snapshots — k-anonymous aggregates,<br/>survive erasure by design (ML-07);<br/>independent retention TBD"]
  end

  subgraph pregage["Pre-GA gate"]
    seed["Tagged seed accounts (is_seed = true)<br/>→ purge-before-GA gate (ACT-004)"]
  end

An account's end of life

flowchart LR
  %% source: technical/data-architecture.md (2026-06-14)
  del["User calls DELETE /account"] --> soft["Soft delete: users.deleted_at set;<br/>ALL sessions revoked immediately;<br/>future auth rejected; LinkedIn no-revival guard"]
  soft --> grace["Grace window<br/>RETENTION_ACCOUNT_ERASURE_DAYS (default 7)"]
  grace --> hard["Worker hard delete"]
  hard --> gone["FK cascades remove the personal graph:<br/>profile, identities, consents, presences,<br/>requests, messages, blocks, rationales,<br/>actor-linked events"]
  gone --> survives["Survives by design: anonymous ML snapshot rows<br/>(no user FK — anonymization boundary, ML-07)"]

Remaining gaps (recorded, not hidden)

  • All durations await counsel ratification (C4) — sessions 30d, presences 7d, candidates 30d, conversations 90d, erasure grace 7d, and report archive 180d are working placeholders.
  • No independent sweep yet for connection requests, accepted-connection duration (C28), the rationale cache beyond read-time expiry, or report archive (180d target, C7).
  • Backups must not silently defeat erasure — restore-vs-erasure reconciliation is part of the recovery procedure; a restore drill is an open action (OPS-01 / ACT-005).
  • Message tombstoning for deleted senders (C6) and reports against deleted users (C7) are queued counsel calls; current code uses schema cascade behavior.

Authoritative table with per-class mechanisms and evidence: registers/retention-schedule.md.