Data baseline
Зачем нужно
Документ задаёт обязательные правила работы с PostgreSQL для всех 7 доменов: типы, ключи, времена, миграции, идемпотентность, retention.
БД на сервис
- Каждый backend-сервис имеет свою отдельную БД PostgreSQL 16+.
- Один сервис не имеет прямого доступа к чужой БД.
- Нет shared schemas.
Расширения
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
Идентификаторы
- Primary key:
id UUID PRIMARY KEY DEFAULT gen_random_uuid(). - Внешние ссылки на сущности других доменов хранятся как UUID без FK (они в чужой БД).
- Внешние provider IDs хранятся отдельным столбцом + столбцом
provider.
Naming
- Имена таблиц —
snake_caseво множественном числе (crm_payments,lms_courses,external_teacher_profiles). - Имена логических сущностей в data-model, ownership и ADR —
snake_caseв единственном числе (crm_entitlement_consumption_log,external_teacher_profile). - Если логическая сущность и таблица отличаются только singular/plural, это не считается naming drift.
Время
- Все timestamps —
TIMESTAMPTZ, хранение в UTC. - Стандартные столбцы:
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),updated_at TIMESTAMPTZ NOT NULL DEFAULT now(). - Триггер на
updated_atили явный update в коде через ORM.
Soft delete
- Допустим для бизнес-сущностей:
deleted_at TIMESTAMPTZ. - Запрещён для финансовых, security и audit записей.
- Чтение по умолчанию исключает
deleted_at IS NOT NULL(через query helper).
Денежные значения
NUMERIC(12,2)илиNUMERIC(14,2).- Никогда
FLOAT/DOUBLE. - Валюта отдельной колонкой
CHAR(3).
Email и phone
- Email:
CITEXT, lowercased, нормализован. - Phone:
TEXTв E.164 (с+), без спецсимволов.
JSON
JSONBдля расширяемых metadata.- Не хранить обязательные связи в JSON.
- Индексы на JSONB:
GINили функциональные.
Индексы
- На все FK по которым ходим;
- на колонки фильтрации списков;
- partial-индексы для
deleted_at IS NULL; - уникальные ограничения на бизнес-ключи (
UNIQUE (account_id, identity_user_id, role)и т.п.).
Constraints
NOT NULLпо умолчанию; nullable требует обоснования;CHECKна enum-подобные строки или используемCREATE TYPE ... AS ENUM;FK ... ON DELETEявный (CASCADE/RESTRICT/SET NULL);UNIQUEдля естественных уникальных значений (slug, email, phone+is_primary).
Enums
Для статусов, типов, ролей внутри домена — CREATE TYPE. Изменение enum-значения добавляется (PostgreSQL не позволяет удаление без пересоздания), удаления — через миграцию данных.
Идемпотентность
- Таблица
idempotency_keys:key TEXT PRIMARY KEY,scope TEXT NOT NULL,request_hash TEXT NOT NULL,response_status INT NOT NULL,response_body JSONB NOT NULL,created_at TIMESTAMPTZ NOT NULL DEFAULT now(),expires_at TIMESTAMPTZ NOT NULL - TTL — 24 часа (для финансовых — 7 дней).
Outbox
- Для гарантированной публикации событий — таблица
outbox:message_id UUID PRIMARY KEY,message_kind TEXT NOT NULL CHECK (message_kind IN ('event','command')),message_type TEXT NOT NULL,message_payload JSONB NOT NULL,occurred_at TIMESTAMPTZ NOT NULL,published_at TIMESTAMPTZ,attempts INT NOT NULL DEFAULT 0 - Запись в outbox в одной транзакции с бизнес-данными;
- background-publisher отправляет в шину и помечает
published_at.
Inbox
- Для дедупликации входящих событий — таблица
processed_messages:message_id UUID PRIMARY KEY,message_kind TEXT NOT NULL CHECK (message_kind IN ('event','command')),message_type TEXT NOT NULL,processed_at TIMESTAMPTZ NOT NULL DEFAULT now(),expires_at TIMESTAMPTZ NOT NULL - TTL ≥ 14 дней для normal retry window.
- Replay за пределами TTL
processed_messagesвыполняется только в explicit replay mode: без side effects, через business idempotency key, или в отдельный snapshot/watermark target. Financial consumers обязаны хранить business idempotency keys весь срок финансового retention.
Audit
- Логическая таблица
audit_logв каждом домене (см.observability.md). Физическое имя может бытьaudit_logв отдельной service DB или<domain>_audit_logsв shared/dev DB, но контракт полей, append-only и retention едины. - Append-only: триггер запрещает UPDATE/DELETE.
- Партиционирование по месяцу для retention.
Миграции
- Инструмент: Prisma Migrate.
- Каждая миграция — атомарная (один логический change).
- Миграции откатываемые: где возможно — обратная миграция.
- Никаких миграций, ломающих обратную совместимость со старой версией кода (zero-downtime: 1) добавить колонку nullable, 2) задать default, 3) задеплоить код, 4) сделать NOT NULL).
- Long-running data migrations выполняются вне миграции схемы (отдельной фоновой задачей).
Zero-downtime для breaking changes
| Change | Процедура |
|---|---|
DROP COLUMN | 1) перестать читать/писать колонку в коде, 2) задеплоить, 3) проверить отсутствие обращений по логам, 4) удалить колонку отдельной миграцией |
RENAME COLUMN | 1) добавить новую колонку, 2) dual-write, 3) backfill, 4) перевести read на новую, 5) удалить старую после окна совместимости |
ALTER TYPE enum delete/rename | 1) добавить новое значение, 2) мигрировать данные, 3) перевести код, 4) пересоздать enum в maintenance migration только после отсутствия старых значений |
CHANGE TYPE | 1) добавить новую колонк у нужного типа, 2) backfill батчами, 3) dual-write, 4) переключить read, 5) удалить старую |
NOT NULL | 1) добавить nullable, 2) backfill, 3) задеплоить код с обязательной записью, 4) добавить NOT NULL |
Retention и партиционирование
- Аудит — 7 лет, партиционирование по месяцу;
- Logs/messages outbox — TTL по политике из
observability.md; - User PII — пока активен пользователь и 6 месяцев после soft delete; затем анонимизация.
Бэкапы
- Daily full + WAL archiving.
- Retention бэкапов: 30 дней daily + 12 monthly.
- Тестовое восстановление раз в квартал.
Запрещено
- Использовать SERIAL/BIGSERIAL для PK новых таблиц.
- Хранить пароли plain.
- Хардкодить ENV-зависимые ID.
- Кросс-сервисные FK между БД.
- Изменять опубликованные миграции.