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

Реализация — `app/models/` + миграция `alembic/versions/20261003_0001_initial_schema.py` (7 таблиц). Постгрес в докере (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` |
| `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). **Креды каналов (токен бота, SMTP) — не здесь, а в .env** |
| `routing_rules` | Правила «условия → действия» | `conditions` JSONB (`source`, `subjects[]`, `actions[]`, `priority_min`), `template` (Jinja2 `{{ payload.x }}`), `weight` (порядок; в MVP применяется первое подошедшее), `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` |
| `deliveries` | Доставка в конкретную цель | `event_id` (CASCADE), `rule_id` (SET NULL — правило удалим, историю оставим), `channel`, `channel_target_id`, `status` ∈ `pending/delivered/failed`, `attempts`, `last_error`, `next_retry_at` (+индекс `status,next_retry_at` — скан ретраев), `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 — вставляется и читается; демо-данные удалены, схема оставлена пустой.