Calendario de Conservación de Datos de HablaYa

Versión: 1.1 Fecha de entrada en vigor: 20 de mayo de 2026 Última actualización: 22 de mayo de 2026

Este documento es la fuente única de verdad para los plazos de conservación en backend, documentos legales y operaciones. Los plazos aquí deben coincidir con la implementación técnica real (TTL, cron jobs, eliminación en cascada). En caso de discrepancia, prevalece la implementación real — las discrepancias se registran como bugs y se cierran en un sprint.

Prevalencia legal: la versión en inglés es la jurídicamente prevalente. En caso de conflicto entre versiones lingüísticas, prevalece el texto en inglés.


1. Finalidad y principios

Principios fundamentales de conservación en HablaYa:

  1. Minimización del almacenamiento (RGPD Art. 5(1)(c)) — conservar los datos solo durante el tiempo necesario para una finalidad concreta, y no más.
  2. Limitación del plazo de conservación definido (RGPD Art. 5(1)(e)) — cada categoría de datos tiene un plazo concreto y finito. "Indefinido" se permite solo cuando la supresión es técnicamente imposible (p. ej. valores hash en logs de auditoría), con justificación separada.
  3. Derecho al olvido (RGPD Art. 17) — la eliminación de la cuenta dispara un procedimiento en cascada de supresión/scrub de PII en todas las tablas y almacenamientos de archivos relacionados.
  4. El mecanismo técnico de cumplimiento es obligatorio — cada plazo debe estar implementado: scheduled job, TTL, cascada on-delete. "Se hará a mano" no se acepta.
  5. Auditoría de eliminaciones — registramos el hecho de la eliminación (qué y cuándo), sin conservar el contenido de los datos eliminados.

2. Tabla completa de conservación

# Categoría Almacenamiento / fuente Plazo Acción al expirar Disparador
1 Perfil de cuenta PG: users, language_profiles, subscriptions Mientras activo + 30 días de gracia Hard delete / anonimización Solicitud del usuario + expiración de gracia
2 Palabras guardadas / diccionario PG: saved_words Mientras la cuenta esté activa Hard delete Eliminación de cuenta (cascada)
3 Historial de diálogo (texto + metadatos) PG: sessions, session_turns, normalized_messages 12 meses rolling Hard delete Cron retention_sweeper (diario) + cascada en eliminación de cuenta
4 Mensajes de voz del usuario (audio bruto) PG: provider_audio_store + archivos en FS (PROVIDER_AUDIO_STORE_PATH) 30 días Hard delete del archivo + flag audio_evicted=TRUE en la fila Cron retention_sweeper (diario) + scrub al eliminar cuenta + FIFO cap (1 GiB) como fallback
5 Logs de auditoría de proveedores (textos de prompts/completions) PG: provider_call_log.request_payload, response_payload 90 días para PII NULL en request_payload y response_payload; metadatos (kind, model, cost_usd, latency_ms, status) se conservan para analítica y reporting financiero Cron retention_sweeper (diario) + scrub al eliminar cuenta
6 Solicitudes de ayuda (Translate / What to reply) PG: help_requests 90 días Hard delete Cron retention_sweeper (diario) + cascada en eliminación de cuenta
7 Uso / cuotas PG: usage_ledger, quota_counters 24 meses Agregación a resumen anonimizado o hard delete Cron retention_sweeper (mensual)
8 Caché de idempotencia Redis: claves idempotency:* 24 horas Expiración TTL automática Redis TTL
9 Caché TTS (respuestas de audio sintetizadas) PG: tts_cache_entry + archivos (TTS_STORAGE_PATH) 90 días LRU Hard delete del archivo + fila Cron audio_cleanup (diario) + eliminación LRU por tamaño
10 Eventos de pago PG: futuras tablas billing_* 6 años (España: 4 años Hacienda + 2 años buffer) Archivo o hard delete según ley aplicable Cron retention_sweeper (anual)
11 Logs de seguridad e incidentes PG: session_event; journald systemd 180 días Hard delete de filas; rotación journald Cron retention_sweeper (semanal) + rotación de journald de systemd
12 Auditoría de eliminaciones (meta) PG: retention_audit_log (tabla nueva) 3 años Hard delete Cron retention_sweeper (anual)
13 Registro DSAR PG: dsar_log (tabla nueva) 3 años tras el cierre de la solicitud Hard delete Cron retention_sweeper (anual)
14 Backups de Postgres Archivos: /home/hablaya/app/backups/ 14 días Hard delete del archivo cron /etc/cron.d/hablaya-pgbackup (rotación diaria)

3. Variables de entorno de configuración

Todos los plazos son configurables vía variables de entorno en el VPS (/home/hablaya/app/.env.production). Esto permite adaptar plazos sin cambios de código y aplicar overrides regionales si se necesita.

# Plazos en días
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

# Futuro
RETENTION_BILLING_EVENTS_DAYS=2190

Principio: acortar plazos está permitido sin aprobaciones y se aplica de inmediato. Ampliar plazos requiere revisión de la Política de Privacidad y notificación a los usuarios (ver Política de Privacidad §12).


4. Cascada al eliminar la cuenta

Cuando el usuario solicita la eliminación de la cuenta (vía Mini App → Ajustes → Privacidad → "Eliminar cuenta y datos" o vía DELETE /api/v1/profile/account), se ejecuta la siguiente secuencia:

  1. Confirmación de la solicitud (typed confirmation en UI).
  2. Periodo de gracia: la cuenta se marca pending_deletion durante 30 días. Durante la gracia, el usuario puede cancelar la eliminación (loginándose con el mismo Telegram ID). Tras la expiración — paso 3.
  3. Eliminación en cascada en PG:
    • DELETE FROM saved_words, language_profiles, help_requests, session_turns, normalized_messages, sessions WHERE user_id = X.
    • UPDATE provider_call_log SET request_payload = NULL, response_payload = NULL WHERE user_id = X — scrub de PII, se conservan los metadatos.
    • UPDATE provider_audio_store SET audio_evicted = TRUE, audio_path = NULL WHERE user_id = X.
  4. Sistema de archivos:
    • Unlink de archivos de audio en PROVIDER_AUDIO_STORE_PATH (vía BackgroundTasks, idempotente).
    • Unlink de archivos en TTS_STORAGE_PATH asociados al usuario eliminado.
  5. Se conserva:
    • users.id (como "tombstone" anónimo para integridad de FK);
    • subscriptions, usage_ledger — para reporting financiero y fiscal (ver filas 7, 10);
    • entrada en dsar_log sobre el hecho de la eliminación (sin PII).
  6. Auditoría: entrada en retention_audit_log (user_id, action='account_purge', timestamp, counts de filas afectadas).
  7. Notificación: correo a la dirección asociada a la solicitud (si se proporcionó), confirmando la finalización.

SLA del procedimiento: no más de 30 días desde la expiración de la gracia hasta la finalización del paso 6.


5. Mecanismos de cumplimiento

Mecanismo Ubicación Programación Función
retention_sweeper (nuevo, Sesión 2 del plan) backend/src/app/services/retention_sweeper.py Diario 03:00 CEST (tras pgbackup) Aplica retention para categorías 3, 4, 5, 6, 7, 11
audio_cleanup (existente) backend/src/app/services/audio_cleanup.py Diario Gestiona TTS storage (categoría 9)
provider_audio_store.evict_until_under_cap (existente) backend/src/app/services/provider_audio_store.py Trigger on write FIFO cap 1 GiB fallback para categoría 4
Redis TTL Integrado en Redis Auto Categoría 8 (idempotencia)
delete_user_account (existente, ampliado en Sesión 2) backend/src/app/services/account_service.py A solicitud del usuario Cascada §4
pgbackup cron /etc/cron.d/hablaya-pgbackup Diario 03:30 CEST Categoría 14 (rotación backups 14 días)
systemd journald Sistema Auto Categoría 11 (parte journald)

6. Auditoría de eliminaciones

Todas las eliminaciones masivas y operaciones de scrub se registran en la nueva tabla retention_audit_log:

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;

Importante: esta tabla registra el hecho de la eliminación (qué y cuándo), pero no el contenido de los datos eliminados. Esto permite:

  • probar cumplimiento ante auditoría/DSAR del usuario;
  • rastrear que el cron funciona (rows_affected > 0 en una BD real con tráfico);
  • investigar anomalías (p. ej. eliminación súbita de un millón de filas).

La propia tabla de auditoría está sujeta a retention: ver fila 12 (3 años).


7. Incident hold

En caso de incidente significativo (security breach, requerimiento legal, investigación), un administrador puede suspender la retención para un usuario concreto o rango temporal:

  • Flag retention_hold = TRUE en users o filas específicas;
  • Fecha de revisión obligatoria (retention_hold_review_at TIMESTAMPTZ NOT NULL) — no más de 90 días; la prórroga requiere acción explícita y se registra.
  • retention_sweeper omite filas en hold.

Actualmente, el incident hold es un mecanismo preparado que se activará en el primer incidente real. Por ahora, el código contiene un stub/no-op.


8. Overrides regionales

En la versión actual (1.0) se aplica una retention única para todos los usuarios independientemente de la región. Los overrides regionales (p. ej. plazos más cortos para residentes de Alemania o más largos para US billing) no están implementados.

Si en el futuro se requiere un régimen regional (p. ej. tras requerimiento de la AEPD después del primer DSAR real de UE), se implementará vía:

  • columna users.retention_profile ('eu_default' | 'us_default' | 'strict_min');
  • lógica de override en retention_sweeper priorizando el plazo más estricto.

9. Open questions

Temas que pueden requerir revisión al crecer el servicio o aparecer nuevos requisitos:

  1. Anonimización vs hard delete para categoría 3 (historial de diálogo): actualmente, hard delete planificado a los 12 meses. Alternativa — anonimización (strip user_id, conservar textos para datasets de entrenamiento). Decisión aplazada hasta que se necesite training data.
  2. Plazo para la voz (categoría 4): 30 días — estándar OpenAI. Para el mínimo estricto RGPD se consideró 7–14 días. 30 días se eligieron como equilibrio entre investigación de incidentes y minimización.
  3. Overrides regionales — ver §8.
  4. Tombstone vs full delete para users.id: actualmente se conserva como tombstone. Bajo una interpretación estricta del Art. 17 RGPD, incluso el tombstone debería eliminarse — esto requeriría rediseño del esquema de FK.
  5. Plazos para el registro DSAR (categoría 13): 3 años — nuestra elección para estadísticas y prueba de cumplimiento. Mínimo según práctica AEPD — 1 año.

Estas cuestiones se registran aquí para que en la próxima auditoría quede claro qué compromisos se asumieron conscientemente.


10. Relación con otros documentos

  • PRIVACY_POLICY.md — versión user-facing de la retention (§5 en Privacy);
  • TOS.md — mención de retention en contexto de resolución (§11);
  • CONSENT_COPY.md — copias UI para usuarios en la Mini App;
  • docs/runbooks/retention.md — runbook de operaciones (se creará en la Sesión 2);
  • docs/open-questions/2026-05-04_provider-audit-gdpr-retention.md — open-question original que este documento cierra.

11. Historial de cambios

Versión Fecha Cambios Autor
1.0 2026-05-11 Primera versión publicable (sustituye al draft del 2026-05-07) Aleksei Skopkarev
1.1 22 de mayo de 2026 Alineación con PRIVACY_POLICY §5 y §3+§6 — eliminado el matiz «cuando se habiliten» para los eventos de pago (Paddle está activo en el código de la rama de migración Paddle de las Fases 5–6). Aleksei Skopkarev

Autor: Claude | Modelo: Claude Opus 4.7 (1M context) | Modo: implementación (Paddle migration Phase 6 prep — corrección de legal-docs tras revisión de calidad) | Razonamiento: no comunicado por el sistema | Fecha: 23 de mayo de 2026, 00:00 (Europe/Madrid) [2026-05-23T00:00:53+0200]