# 04 — Схема БД (#34), v1

Реализация — `app/models/` + миграции `alembic/versions/` (7 таблиц; `0001` — исходная схема, `0002/0003` — `description` и полный Routing Engine). Постгрес в докере (postgres:17-alpine, том `pgdata`), Redis 7 (том `redisdata`).

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

## Конфигурация

```
sources ──< api_keys
   │
   └──< notification_types          (уникальность: source_id + subject + action)
   │
routing_rules ──< routing_rule_actions ──> channel_targets
```

| Таблица | Что хранит | Ключевые поля |
|---|---|---|
| `sources` | Источники событий (регистрирует админ) | `name` (slug, unique: `monitoring`), `label`, `description` («что это за сервис» для человека и ИИ-агента) |
| `api_keys` | Ключи `syn_…` к источникам | **`token_hash`** (sha256, unique), `token_hint` (последние 4 для UI), `revoked_at` (ротация: новый ключ, старая строка маркируется). Ключ — удостоверение; правила матчают `source`, не токен |
| `notification_types` | Реестр типов из контракта | тройка `(source_id, subject, action)` уникальна, `payload_schema` JSONB (опц., валидирует воркер) |
| `channel_targets` | Цели каналов: куда доставлять | `channel` ∈ `telegram/email/s2s/internal_log` (CHECK), `config` JSONB (chat_id, адрес, s2s-endpoint; s2s ещё `token_ref` — ссылка на секрет), `description` («что это за клиент-сервис»). **Креды каналов (токен бота, SMTP, HMAC-секреты) — не здесь, а в .env** |
| `routing_rules` | Правила «условия → действия» | `conditions` JSONB (`source`, `subjects[]`, `actions[]`, `priority_min`, `payload` — точный матч по top-level ключам), `template` (Jinja2 `{{ payload.x }}`), `weight` (порядок, важен в режиме first), `throttle_seconds` (шумодав: окно на (channel, target), critical проходит всегда), `enabled` |
| `routing_rule_actions` | Действия правила | `rule_id`, `channel` (CHECK), `target_id` → channel_targets (SET NULL при удалении цели), `template` — переопределение шаблона правила |

## Поток событий

```
events 1 ──< deliveries
```

| Таблица | Что хранит | Ключевые поля |
|---|---|---|
| `events` | Конверт события (контракт docs/05) | UUID pk, `source_id` (RESTRICT — события переживают удаление источника), `subject`/`action`/`priority` (CHECK по шкале), `payload` JSONB, `dedup_key` (+индекс с `created_at` — окно дедупликации), `expires_at` (= created+ttl_seconds), `scheduled_at` (резерв), `status` ∈ `queued/processing/done/failed` (done — замаршрутизировано; живые статусы в deliveries) |
| `deliveries` | Доставка в конкретную цель | `event_id` (CASCADE), `rule_id` (SET NULL — правило удалим, историю оставим), `channel`, `channel_target_id`, `status` ∈ `pending/delivered/failed/skipped` (skipped — срезано шумодавом), `attempts`, `last_error`, `next_retry_at` (+индекс `status,next_retry_at` — скан ретраей beat'ом), `rendered_message` (аудит: что реально уйдёт в канал) |

## Что сознательно НЕ в таблицах

- **Пользователи** — их делает gnexus-auth; Synapse пользователей не хранит.
- **Секреты каналов** — .env/`gnexus-creds`, не БД (`TELEGRAM_BOT_TOKEN`, SMTP и т.д.).
- **`tags` события** — убраны из v1 контракта (см. docs/05); вернутся, когда появится правило, требующее их.
- **История попыток отдельно от delivery** — одна запись с `attempts`/`next_retry_at`; поэлементный лог попыток не нужен MVP.

## Проверено

- `alembic upgrade head` поднимает всё из пустой базы (docker: api применяет миграции на старте).
- ORM roundtrip: Source → ApiKey → NotificationType → ChannelTarget → RoutingRule(+Action) → Event → Delivery — вставляется и читается; демо-данные удалены, схема оставлена пустой.