PR5b, the final code piece of the WhatsApp intake track. A daily cron
(04:15) enforces the channel's retention table; the receipt itself stays
7-year WORM under BFL and is never touched.
Retention actions (lib/retention.ts, each isolated and idempotent):
- whatsapp_messages transcripts past 90 days: body_text + raw_payload
cleared in id batches under a wall-clock budget; the row skeleton
(wamid, direction, timestamps, status, inbox_item_id) survives for
audit. Only rows still carrying content match.
- Rows with phone_link_id IS NULL (unknown senders, orphans) past
30 days: deleted.
- Link codes expired more than 24h ago: deleted, used or not.
- Sender rate counters idle 2+ days: deleted (minute/day window keys
are dead weight after that).
- Links revoked 90+ days ago: phone_enc crypto-shredded to '' (column
is NOT NULL), one-shot via neq guard; phone_hash and phone_masked
kept for uniqueness history and audit display.
Route mirrors the sweep cron exactly: withCronContext + registry gate
(503 EXTENSION_DISABLED when the extension is off). vercel.json gets
the schedule and both Docker crontabs are regenerated.
Compliance: new whatsapp.receipt_intake activity in .compliance/ropa.yaml
covering purpose, Art 6(1)(b)/(c)/(f) bases with the Art 14(5)(b) note
for third-party attendee names, Meta Platforms Ireland as processor
(Cloud API, EU SCC addendum, Local Storage region DE), the differentiated
retention table, and security measures.
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>