HablaYa Data Retention Schedule
Version: 1.1 Effective date: 20 May 2026 Last updated: 22 May 2026
This document is the single source of truth for retention periods across backend, legal documents, and operations. Periods here must match the actual technical implementation (TTL, cron jobs, cascade delete). In case of mismatch, the actual implementation prevails — discrepancies are recorded as bugs and resolved within one sprint.
Legal precedence: This English version is authoritative. In case of conflict between language versions, this English text prevails.
1. Purpose and principles
Core retention principles in HablaYa:
- Storage minimization (GDPR Art. 5(1)(c)) — store data only as long as needed for a specific purpose, and no longer.
- Defined storage limitation (GDPR Art. 5(1)(e)) — each data category has a concrete, finite retention period. "Indefinite" is allowed only where deletion is technically impossible (e.g., hash values in audit logs), with separate justification.
- Right to erasure (GDPR Art. 17) — account deletion triggers a cascade procedure of deletion/PII scrubbing across all related tables and file storage.
- Technical enforcement mechanism is mandatory — every retention period must be implemented: scheduled job, TTL, on-delete cascade. "Done manually" is not accepted.
- Deletion audit — we record the fact of deletion (what and when), without retaining the content of deleted data.
2. Full retention table
| # | Category | Storage / source | Period | Action at expiry | Deletion trigger |
|---|---|---|---|---|---|
| 1 | Account profile | PG: users, language_profiles, subscriptions |
While active + 30 days grace | Hard delete / anonymization | User deletion request + grace expiry |
| 2 | Saved words / dictionary | PG: saved_words |
While account active | Hard delete | Account deletion (cascade) |
| 3 | Dialog history (text + metadata) | PG: sessions, session_turns, normalized_messages |
12 months rolling | Hard delete | Cron retention_sweeper (daily) + cascade on account deletion |
| 4 | User voice messages (raw audio) | PG: provider_audio_store + files on FS (PROVIDER_AUDIO_STORE_PATH) |
30 days | Hard delete of file + flag audio_evicted=TRUE on row |
Cron retention_sweeper (daily) + scrub on delete account + FIFO cap (1 GiB) as fallback |
| 5 | Provider audit logs (prompt/completion texts) | PG: provider_call_log.request_payload, response_payload |
90 days for PII | NULL request_payload and response_payload; metadata (kind, model, cost_usd, latency_ms, status) retained for analytics and financial reporting |
Cron retention_sweeper (daily) + scrub on delete account |
| 6 | Help requests (Translate / What to reply) | PG: help_requests |
90 days | Hard delete | Cron retention_sweeper (daily) + cascade on account deletion |
| 7 | Usage / quotas | PG: usage_ledger, quota_counters |
24 months | Aggregation to anonymized summary or hard delete | Cron retention_sweeper (monthly) |
| 8 | Idempotency cache | Redis: idempotency:* keys |
24 hours | Automatic TTL expiry | Redis TTL |
| 9 | TTS cache (synthesized audio responses) | PG: tts_cache_entry + files (TTS_STORAGE_PATH) |
90 days LRU | Hard delete file + row | Cron audio_cleanup (daily) + LRU eviction by size |
| 10 | Payment events | PG: future billing_* tables |
6 years (Spain: 4 years Hacienda + 2 years buffer) | Archive or hard delete per applicable law | Cron retention_sweeper (yearly) |
| 11 | Security and incident logs | PG: session_event; journald systemd |
180 days | Hard delete of rows; journald rotation | Cron retention_sweeper (weekly) + systemd journald rotation |
| 12 | Deletion audit (meta) | PG: retention_audit_log (new table) |
3 years | Hard delete | Cron retention_sweeper (yearly) |
| 13 | DSAR log | PG: dsar_log (new table) |
3 years after request closure | Hard delete | Cron retention_sweeper (yearly) |
| 14 | Postgres backups | Files: /home/hablaya/app/backups/ |
14 days | Hard delete of file | cron /etc/cron.d/hablaya-pgbackup (daily rotation) |
3. Configuration env-variables
All retention periods are configurable via env-variables on the VPS (/home/hablaya/app/.env.production). This allows adapting periods without code changes and applying regional overrides if needed.
# Retention in days
RETENTION_PROVIDER_AUDIO_DAYS=30
RETENTION_PROVIDER_CALL_LOG_DAYS=90
RETENTION_SESSION_TURN_DAYS=365
RETENTION_HELP_REQUEST_DAYS=90
RETENTION_USAGE_LEDGER_DAYS=730
RETENTION_SECURITY_LOGS_DAYS=180
RETENTION_TTS_CACHE_DAYS=90
RETENTION_RETENTION_AUDIT_LOG_DAYS=1095
RETENTION_DSAR_LOG_DAYS=1095
RETENTION_DELETE_ACCOUNT_GRACE_DAYS=30
# Future
RETENTION_BILLING_EVENTS_DAYS=2190
Principle: shortening periods is allowed without approval and applied immediately. Extending periods requires Privacy Policy revision and user notification (see Privacy Policy §12).
4. Cascade on account deletion
When a user requests account deletion (via Mini App → Settings → Privacy → "Delete account and data" or via DELETE /api/v1/profile/account), the following sequence applies:
- Request confirmation (typed confirmation in UI).
- Grace period: account marked
pending_deletionfor 30 days. During grace, user can cancel deletion (sign in with same Telegram ID). After expiry — proceed to step 3. - PG cascade deletion:
- DELETE FROM
saved_words,language_profiles,help_requests,session_turns,normalized_messages,sessionsWHEREuser_id = X. - UPDATE
provider_call_logSETrequest_payload = NULL,response_payload = NULLWHEREuser_id = X— scrub PII, retain metadata. - UPDATE
provider_audio_storeSETaudio_evicted = TRUE,audio_path = NULLWHEREuser_id = X.
- DELETE FROM
- Filesystem:
- Unlink audio files in
PROVIDER_AUDIO_STORE_PATH(via BackgroundTasks, idempotent). - Unlink files in
TTS_STORAGE_PATHlinked to the deleted user.
- Unlink audio files in
- Retained:
users.id(as an anonymous "tombstone" for FK integrity);subscriptions,usage_ledger— for financial and tax reporting (see rows 7, 10);dsar_logentry on the fact of deletion (no PII).
- Audit:
retention_audit_logentry (user_id, action='account_purge', timestamp, counts of affected rows). - Notification: email to the address tied to the request (if provided), confirming completion.
Procedure SLA: no more than 30 days from grace expiry to step 6 completion.
5. Retention enforcement mechanisms
| Mechanism | Location | Schedule | Function |
|---|---|---|---|
retention_sweeper (new, plan Session 2) |
backend/src/app/services/retention_sweeper.py |
Daily 03:00 CEST (after pgbackup) | Applies retention for categories 3, 4, 5, 6, 7, 11 |
audio_cleanup (existing) |
backend/src/app/services/audio_cleanup.py |
Daily | Manages TTS storage (category 9) |
provider_audio_store.evict_until_under_cap (existing) |
backend/src/app/services/provider_audio_store.py |
Trigger on write | FIFO cap 1 GiB fallback for category 4 |
| Redis TTL | Built into Redis | Auto | Category 8 (idempotency) |
delete_user_account (existing, extended in plan Session 2) |
backend/src/app/services/account_service.py |
On user request | Cascade per §4 |
| pgbackup cron | /etc/cron.d/hablaya-pgbackup |
Daily 03:30 CEST | Category 14 (backups rotation 14 days) |
| systemd journald | System | Auto | Category 11 (journald part) |
6. Deletion audit
All mass deletions and scrub operations are recorded in a new retention_audit_log table:
CREATE TABLE retention_audit_log (
id BIGSERIAL PRIMARY KEY,
executed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
operation TEXT NOT NULL,
category TEXT NOT NULL,
rows_affected INTEGER NOT NULL,
files_affected INTEGER NOT NULL DEFAULT 0,
user_id BIGINT,
extra JSONB,
duration_ms INTEGER
);
CREATE INDEX idx_retention_audit_executed_at ON retention_audit_log (executed_at DESC);
CREATE INDEX idx_retention_audit_user_id ON retention_audit_log (user_id) WHERE user_id IS NOT NULL;
Important: this table records the fact of deletion (what and when), but not the content of deleted data. This allows:
- proving compliance during audit/user DSAR;
- tracking that cron is working (rows_affected > 0 in a real DB with traffic);
- investigating anomalies (e.g., sudden mass deletion of a million rows).
The audit table itself is subject to retention: see row 12 (3 years).
7. Incident hold
In case of a significant incident (security breach, legal request, investigation), an administrator may suspend retention for a specific user or time range:
retention_hold = TRUEflag onusersor specific rows;- Mandatory hold review date (
retention_hold_review_at TIMESTAMPTZ NOT NULL) — no more than 90 days; extension requires explicit action and is logged. retention_sweeperskips rows under hold.
Currently, incident hold is a prepared mechanism that will be activated upon the first real incident. The code contains a stub/no-op for now.
8. Regional overrides
The current version (1.0) applies a single retention policy to all users regardless of region. Regional overrides (e.g., shorter periods for residents of Germany or longer for US billing) are not implemented.
If a regional regime is required in the future (e.g., upon AEPD request after the first real EU DSAR), it will be implemented via:
- column
users.retention_profile('eu_default' | 'us_default' | 'strict_min'); - override logic in
retention_sweeperprioritizing the stricter period.
9. Open questions
Topics that may require revision as the service grows or new requirements emerge:
- Anonymization vs hard delete for category 3 (dialog history): currently hard delete planned after 12 months. Alternative — anonymization (strip user_id, retain texts for training datasets). Decision deferred until training data is actually needed.
- Period for voice (category 4): 30 days — OpenAI standard. For strict GDPR-minimum, 7–14 days was considered. 30 days was chosen as a balance between incident investigation and minimization.
- Regional overrides — see §8.
- Tombstone vs full delete for
users.id: currently retained as tombstone. Under strict GDPR Art. 17 interpretation, even tombstone should be deleted — this would require FK schema redesign. - Periods for DSAR log (category 13): 3 years — our choice for statistics and compliance evidence. AEPD practice minimum — 1 year.
These questions are recorded here so that during the next audit it is clear which trade-offs were made consciously.
10. Relation to other documents
- PRIVACY_POLICY.md — user-facing version of retention (§5 in Privacy);
- TOS.md — retention mention in termination context (§11);
- CONSENT_COPY.md — UI copies for users in Mini App;
docs/runbooks/retention.md— operations runbook (to be created in plan Session 2);docs/open-questions/2026-05-04_provider-audit-gdpr-retention.md— original open question that this document closes.
11. Change history
| Version | Date | Changes | Author |
|---|---|---|---|
| 1.0 | 2026-05-11 | First published version (replaces draft from 2026-05-07) | Aleksei Skopkarev |
| 1.1 | 22 May 2026 | Aligned with PRIVACY_POLICY §5 and §3+§6 — removed the "when enabled" qualifier for payment events (Paddle is active in the Phase 5–6 Paddle migration branch). | Aleksei Skopkarev |
Author: Claude | Model: Claude Opus 4.7 (1M context) | Mode: implementation (Paddle migration Phase 6 prep — legal-docs fix after quality review) | Reasoning: not reported by system | Timestamp: 23 May 2026, 00:00 (Europe/Madrid) [2026-05-23T00:00:53+0200]