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.