График хранения данных (Data Retention Schedule) HablaYa
Версия: 1.1 Дата вступления в силу: 20 мая 2026 Последнее обновление: 22 мая 2026
Этот документ — единая таблица сроков хранения для backend, legal-документов и эксплуатации. Сроки в этом документе должны соответствовать реальной тех. реализации (TTL, cron-задачи, каскадное удаление). При расхождении приоритет имеет фактическая реализация — несоответствия фиксируются как баги и закрываются в течение одного спринта.
1. Цель и принципы
Базовые принципы retention в HablaYa:
- Минимизация хранения (GDPR Art. 5(1)(c)) — храним столько, сколько нужно для конкретной цели, и не дольше.
- Defined storage limitation (GDPR Art. 5(1)(e)) — для каждой категории данных есть конкретный конечный срок хранения. «Бессрочно» допускается только для технически невозможного удаления (например, hash-значений в logs аудита) с отдельным обоснованием.
- Право быть забытым (GDPR Art. 17) — удаление аккаунта запускает каскадную процедуру удаления/scrub'а PII во всех связанных таблицах и файловых хранилищах.
- Технический механизм исполнения обязателен — каждый retention-период должен быть реализован: scheduled job, TTL, on-delete cascade. «Будем делать вручную» не принимается.
- 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), выполняется следующая последовательность:
- Подтверждение запроса (typed confirmation на стороне UI).
- Грейс-период: аккаунт помечается
pending_deletionна 30 дней. В течение грейса пользователь может отменить удаление (логин с тем же Telegram ID). После истечения — переход к шагу 3. - Каскадное удаление в PG:
- DELETE из
saved_words,language_profiles,help_requests,session_turns,normalized_messages,sessions(гдеuser_id = X). - UPDATE
provider_call_logSETrequest_payload = NULL,response_payload = NULLWHEREuser_id = X— scrub PII, оставляем metadata. - UPDATE
provider_audio_storeSETaudio_evicted = TRUE,audio_path = NULLWHEREuser_id = X.
- DELETE из
- Файловая система:
- Unlink файлов аудио в
PROVIDER_AUDIO_STORE_PATH(через BackgroundTasks, idempotent). - Unlink файлов в
TTS_STORAGE_PATH, привязанных к удалённому пользователю.
- Unlink файлов аудио в
- Сохраняется:
users.id(как анонимный «tombstone» для целостности FK);subscriptions,usage_ledger— для финансовой и налоговой отчётности (см. строки 7, 10 таблицы);dsar_logзапись о факте удаления (без PII).
- Audit: запись в
retention_audit_log(user_id, action='account_purge', timestamp, counts затронутых строк). - Уведомление: 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
Темы, которые могут потребовать пересмотра при росте сервиса или появлении новых требований:
- Анонимизация vs hard delete для категории 3 (история диалога): сейчас планируется hard delete через 12 мес. Альтернатива — анонимизация (strip user_id, оставить тексты для тренировочных датасетов). Решение отложено до момента, когда появится необходимость в тренировочных данных.
- Срок для голоса (категория 4): 30 дней — стандарт OpenAI. Для строго GDPR-минимального подхода рассматривался срок 7–14 дней. 30 дней выбраны как баланс между расследованием инцидентов и минимизацией.
- Региональные override — см. §8.
- Tomstone vs full delete для
users.id: сейчас сохраняется как tombstone. При жёстком толковании GDPR Art. 17 даже tombstone должен быть удалён — это потребует переделки FK-схемы. - Сроки для 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]