График хранения данных (Data Retention Schedule) HablaYa

Версия: 1.1 Дата вступления в силу: 20 мая 2026 Последнее обновление: 22 мая 2026

Этот документ — единая таблица сроков хранения для backend, legal-документов и эксплуатации. Сроки в этом документе должны соответствовать реальной тех. реализации (TTL, cron-задачи, каскадное удаление). При расхождении приоритет имеет фактическая реализация — несоответствия фиксируются как баги и закрываются в течение одного спринта.


1. Цель и принципы

Базовые принципы retention в HablaYa:

  1. Минимизация хранения (GDPR Art. 5(1)(c)) — храним столько, сколько нужно для конкретной цели, и не дольше.
  2. Defined storage limitation (GDPR Art. 5(1)(e)) — для каждой категории данных есть конкретный конечный срок хранения. «Бессрочно» допускается только для технически невозможного удаления (например, hash-значений в logs аудита) с отдельным обоснованием.
  3. Право быть забытым (GDPR Art. 17) — удаление аккаунта запускает каскадную процедуру удаления/scrub'а PII во всех связанных таблицах и файловых хранилищах.
  4. Технический механизм исполнения обязателен — каждый retention-период должен быть реализован: scheduled job, TTL, on-delete cascade. «Будем делать вручную» не принимается.
  5. Audit удалений — мы фиксируем сам факт удаления (что и когда было удалено), не сохраняя содержимого удалённых данных.

2. Полная таблица retention

# Категория Хранилище / источник Срок Действие по истечении Триггер удаления
1 Профиль аккаунта PG: users, language_profiles, subscriptions Пока аккаунт активен + 30 дней grace Hard delete / анонимизация Запрос пользователя на удаление + истечение grace
2 Сохранённые слова / dictionary PG: saved_words Пока аккаунт активен Hard delete Удаление аккаунта (cascade)
3 История диалога (текст + метаданные) PG: sessions, session_turns, normalized_messages 12 месяцев rolling Hard delete Cron retention_sweeper (daily) + cascade на удалении аккаунта
4 Голосовые сообщения пользователя (raw audio) PG: provider_audio_store + файлы на FS (PROVIDER_AUDIO_STORE_PATH) 30 дней Hard delete файла + flag audio_evicted=TRUE на row'е Cron retention_sweeper (daily) + scrub on delete account + FIFO-cap (1 GiB) как fallback
5 Audit-логи провайдеров (тексты prompts/completions) PG: provider_call_log.request_payload, response_payload 90 дней для PII NULL request_payload и response_payload; metadata (kind, model, cost_usd, latency_ms, status) сохраняется для аналитики и финансовой отчётности Cron retention_sweeper (daily) + scrub on delete account
6 Help-запросы (Translate / What to reply) PG: help_requests 90 дней Hard delete Cron retention_sweeper (daily) + cascade на удалении аккаунта
7 Usage / квоты PG: usage_ledger, quota_counters 24 месяца Агрегация в anonymized summary или hard delete Cron retention_sweeper (monthly)
8 Idempotency-кэш Redis: idempotency:* keys 24 часа Авто-истечение TTL Redis TTL
9 TTS-кэш (синтезированные аудио-ответы) PG: tts_cache_entry + файлы (TTS_STORAGE_PATH) 90 дней LRU Hard delete файла + row Cron audio_cleanup (daily) + LRU eviction по размеру
10 Платёжные события PG: будущие billing_* таблицы 6 лет (Испания: 4 года Hacienda + 2 года buffer) Архив или hard delete по применимому праву Cron retention_sweeper (yearly)
11 Логи безопасности и инцидентов PG: session_event; journald systemd 180 дней Hard delete row'ов; ротация journald Cron retention_sweeper (weekly) + systemd journald rotation
12 Audit удалений (meta) PG: retention_audit_log (новая таблица) 3 года Hard delete Cron retention_sweeper (yearly)
13 DSAR-журнал PG: dsar_log (новая таблица) 3 года после закрытия запроса Hard delete Cron retention_sweeper (yearly)
14 Backups Postgres Файлы: /home/hablaya/app/backups/ 14 дней Hard delete файла cron /etc/cron.d/hablaya-pgbackup (daily rotation)

3. Env-переменные конфигурации retention

Все retention-сроки конфигурируются через env-переменные на VPS (/home/hablaya/app/.env.production). Это позволяет адаптировать сроки без code change и применять региональные override при необходимости в будущем.

# Retention в днях
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

# Будущие
RETENTION_BILLING_EVENTS_DAYS=2190

Принцип: сокращение сроков допустимо без согласований и применяется немедленно. Увеличение сроков требует пересмотра Privacy Policy и уведомления пользователей (см. Privacy Policy §12).


4. Каскад при удалении аккаунта

Когда пользователь запрашивает удаление аккаунта (через Mini App → Settings → Privacy → «Удалить аккаунт и данные» или через DELETE /api/v1/profile/account), выполняется следующая последовательность:

  1. Подтверждение запроса (typed confirmation на стороне UI).
  2. Грейс-период: аккаунт помечается pending_deletion на 30 дней. В течение грейса пользователь может отменить удаление (логин с тем же Telegram ID). После истечения — переход к шагу 3.
  3. Каскадное удаление в PG:
    • DELETE из saved_words, language_profiles, help_requests, session_turns, normalized_messages, sessions (где user_id = X).
    • UPDATE provider_call_log SET request_payload = NULL, response_payload = NULL WHERE user_id = X — scrub PII, оставляем metadata.
    • UPDATE provider_audio_store SET audio_evicted = TRUE, audio_path = NULL WHERE user_id = X.
  4. Файловая система:
    • Unlink файлов аудио в PROVIDER_AUDIO_STORE_PATH (через BackgroundTasks, idempotent).
    • Unlink файлов в TTS_STORAGE_PATH, привязанных к удалённому пользователю.
  5. Сохраняется:
    • users.id (как анонимный «tombstone» для целостности FK);
    • subscriptions, usage_ledger — для финансовой и налоговой отчётности (см. строки 7, 10 таблицы);
    • dsar_log запись о факте удаления (без PII).
  6. Audit: запись в retention_audit_log (user_id, action='account_purge', timestamp, counts затронутых строк).
  7. Уведомление: email на адрес, привязанный к запросу (если предоставлен), о завершении процедуры.

SLA процедуры: не более 30 дней с момента истечения грейса до завершения шага 6.


5. Реализация retention-механизмов

Механизм Где живёт Расписание Что делает
retention_sweeper (новый, Сессия 2 плана) backend/src/app/services/retention_sweeper.py Daily 03:00 CEST (после pgbackup) Применяет retention для категорий 3, 4, 5, 6, 7, 11
audio_cleanup (существующий) backend/src/app/services/audio_cleanup.py Daily Управляет TTS storage (категория 9)
provider_audio_store.evict_until_under_cap (существующий) backend/src/app/services/provider_audio_store.py Trigger on write FIFO-cap 1 GiB как fallback для категории 4
Redis TTL Встроено в Redis Auto Категория 8 (idempotency)
delete_user_account (существующий, расширяется в Сессии 2) backend/src/app/services/account_service.py On user request Каскад из §4
pgbackup cron /etc/cron.d/hablaya-pgbackup Daily 03:30 CEST Категория 14 (backups rotation 14 дней)
systemd journald Системный Auto Категория 11 (journald часть)

6. Audit удалений

Все массовые удаления и scrub-операции фиксируются в новой таблице retention_audit_log:

CREATE TABLE retention_audit_log (
    id BIGSERIAL PRIMARY KEY,
    executed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    operation TEXT NOT NULL,           -- 'sweep_session_turns', 'scrub_provider_call_log', 'account_purge', ...
    category TEXT NOT NULL,             -- 'session_turn', 'provider_call_log', 'provider_audio', ...
    rows_affected INTEGER NOT NULL,
    files_affected INTEGER NOT NULL DEFAULT 0,
    user_id BIGINT,                     -- NULL для массовых операций
    extra JSONB,                        -- {oldest_deleted_at: '...', cron_run_id: '...'}
    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;

Важно: в этой таблице фиксируется факт удаления (что и когда), но не содержимое удалённых данных. Это позволяет:

  • доказать compliance при аудите/DSAR-запросе пользователя;
  • отследить, что cron работает (rows_affected > 0 в реальной БД с трафиком);
  • расследовать аномалии (например, внезапное удаление миллиона row'ов).

Audit-таблица сама подчиняется retention: см. строку 12 (3 года).


7. Incident hold

В случае значимого инцидента (security breach, юридический запрос, расследование) администратор может приостановить применение retention для конкретного пользователя или временного диапазона:

  • Флаг retention_hold = TRUE на users или на специфических row'ах;
  • Обязательная дата пересмотра hold (retention_hold_review_at TIMESTAMPTZ NOT NULL) — не более 90 дней; продление требует явного действия и логируется.
  • retention_sweeper пропускает row'ы под hold.

На текущий момент incident hold — это подготовленный механизм, который активируется при первом реальном инциденте. Сейчас в коде заглушка/no-op.


8. Региональные override

В текущей версии (1.0) применяется единый retention для всех пользователей независимо от региона. Региональные override (например, более короткие сроки для residents Германии или более длинные для US billing) — не реализованы.

Если в будущем потребуется региональный режим (например, по запросу AEPD после первого реального DSAR из ЕС), он будет реализован через:

  • column users.retention_profile ('eu_default' | 'us_default' | 'strict_min');
  • override логика в retention_sweeper с приоритетом более строгого срока.

9. Open questions

Темы, которые могут потребовать пересмотра при росте сервиса или появлении новых требований:

  1. Анонимизация vs hard delete для категории 3 (история диалога): сейчас планируется hard delete через 12 мес. Альтернатива — анонимизация (strip user_id, оставить тексты для тренировочных датасетов). Решение отложено до момента, когда появится необходимость в тренировочных данных.
  2. Срок для голоса (категория 4): 30 дней — стандарт OpenAI. Для строго GDPR-минимального подхода рассматривался срок 7–14 дней. 30 дней выбраны как баланс между расследованием инцидентов и минимизацией.
  3. Региональные override — см. §8.
  4. Tomstone vs full delete для users.id: сейчас сохраняется как tombstone. При жёстком толковании GDPR Art. 17 даже tombstone должен быть удалён — это потребует переделки FK-схемы.
  5. Сроки для DSAR-журнала (категория 13): 3 года — наш выбор для статистики и доказательства compliance. Минимум по практике AEPD — 1 год.

Эти вопросы фиксируются здесь, чтобы при следующем audit'е сразу было понятно, какие компромиссы были сделаны осознанно.


10. Связь с другими документами

  • PRIVACY_POLICY.md — пользовательская версия retention (§5 в Privacy);
  • TOS.md — упоминание retention в контексте прекращения использования (§11);
  • CONSENT_COPY.md — копии для пользователей в Mini App;
  • docs/runbooks/retention.md — runbook эксплуатации (будет создан в Сессии 2 плана);
  • docs/open-questions/2026-05-04_provider-audit-gdpr-retention.md — исходный open-question, который этот документ закрывает.

11. История изменений

Версия Дата Что изменено Кто
1.0 2026-05-11 Первая публикуемая версия (заменяет draft от 2026-05-07) Aleksei Skopkarev
1.1 22 мая 2026 Выравнено с PRIVACY_POLICY §5 и §3+§6 — убрана формулировка «когда появятся» для платёжных событий (Paddle активен в коде ветки Phase 5–6 Paddle-миграции). Aleksei Skopkarev

Автор: Claude | Модель: Claude Opus 4.7 (1M context) | Режим: реализация (Paddle migration Phase 6 prep — legal-docs fix после ревью качества) | Размышление: не указано системой | Дата и время: 23 мая 2026, 00:00 (Europe/Madrid) [2026-05-23T00:00:50+0200]