Модель данных PostgreSQL, REST API, ошибки, вебхуки и контракты очередей.
1. Общие принципы
- PostgreSQL 16, кодировка UTF-8, timezone UTC.
- Первичные ключи:
uuid (v7) для пользовательских сущностей, bigserial для логов/событий.
- Все таблицы содержат
created_at, updated_at.
- Мягкое удаление:
deleted_at там, где нужна история.
- Деньги:
numeric(12,2), только RUB в MVP.
- Секреты:
bytea/text с шифрованием AES-256-GCM + key_version.
- JSON-поля:
jsonb с валидацией на уровне приложения.
- Внешние ключи с
ON DELETE RESTRICT по умолчанию; каскады только для логов.
2. ERD (упрощённо)
users 1──N sessions
users 1──N oauth_identities
users 1──N connections 1──N activations
users 1──N activations N──1 bundles N──1 services (source/target)
activations 1──N runs 1──N run_logs
activations 1──N webhook_endpoints
activations 1──N subscriptions N──1 payments
users 1──N notifications
users 1──N audit_log (actor)
services 1──N service_versions
services 1──N triggers/actions (в манифесте, кэш в БД)
bundles 1──N bundle_versions
3. Таблицы
3.1. users
| Колонка | Тип | Ограничения | Описание |
|---|
| id | uuid | PK | |
| email | citext | UNIQUE NOT NULL | Логин |
| password_hash | text | NULL | Argon2id; NULL для OAuth-only |
| name | text | NULL | |
| locale | text | DEFAULT 'ru' | |
| timezone | text | DEFAULT 'Europe/Moscow' | |
| status | enum | active, blocked, deleted | |
| email_verified_at | timestamptz | NULL | |
| last_login_at | timestamptz | NULL | |
| marketing_opt_in | boolean | DEFAULT false | |
| created_at/updated_at/deleted_at | timestamptz | | |
Индексы: lower(email), status.
3.2. sessions
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| user_id | uuid FK | |
| token_hash | text UNIQUE | SHA-256 от токена |
| ip | inet | |
| user_agent | text | |
| expires_at | timestamptz | |
| revoked_at | timestamptz | |
| created_at | timestamptz | |
Индексы: (user_id, expires_at), token_hash.
3.3. oauth_identities
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| user_id | uuid FK | |
| provider | text | vk, yandex |
| provider_user_id | text | |
| created_at | timestamptz | |
UNIQUE (provider, provider_user_id).
3.4. services
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| key | text UNIQUE | telegram, tilda |
| name | text | |
| category | text | crm, messengers, tables, forms, sms, payments |
| icon_url | text | |
| description | text | |
| docs_url | text | |
| status | enum | active, beta, disabled |
| created_at/updated_at | timestamptz | |
3.5. service_versions
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| service_id | uuid FK | |
| version | text | SemVer |
| manifest | jsonb | Полный манифест |
| is_current | boolean | |
| published_at | timestamptz | |
UNIQUE (service_id, version).
3.6. bundles (каталог)
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| slug | text UNIQUE | tilda-to-telegram |
| title | text | |
| description | text | |
| category | text | |
| source_service_id | uuid FK | |
| source_trigger_key | text | |
| target_service_id | uuid FK | |
| target_action_key | text | |
| mapping_template | jsonb | |
| filter_template | jsonb | NULL |
| required_scopes | jsonb | |
| price_rub | numeric(12,2) | DEFAULT 200 |
| status | enum | draft, published, deprecated |
| popularity | integer | Для сортировки |
| seo_title/description | text | |
| created_at/updated_at | timestamptz | |
Индексы: status, category, GIN по title/description (pg_trgm).
3.7. bundle_versions
История изменений шаблона связки: id, bundle_id, version, snapshot jsonb,
created_at. Нужна для воспроизводимости активаций.
3.8. connections
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| user_id | uuid FK | |
| service_id | uuid FK | |
| external_account_id | text | ID аккаунта/бота |
| external_account_name | text | Отображаемое имя |
| auth_type | text | oauth2, token, bot_token, basic, webhook |
| credentials_encrypted | bytea | Шифртекст |
| key_version | integer | Версия ключа |
| scopes | text[] | |
| status | enum | active, needs_reauth, revoked, error, disabled |
| expires_at | timestamptz | NULL |
| last_checked_at | timestamptz | |
| metadata | jsonb | Несекретные данные |
| created_at/updated_at | timestamptz | |
Индексы: (user_id, service_id), status.
3.9. activations
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| user_id | uuid FK | |
| bundle_id | uuid FK | |
| bundle_version | text | |
| source_connection_id | uuid FK | |
| target_connection_id | uuid FK | |
| name | text | Пользовательское имя |
| mapping | jsonb | |
| filter | jsonb | NULL |
| status | enum | draft, trial, active, paused, error, deleted |
| trial_ends_at | timestamptz | |
| activated_at | timestamptz | |
| paused_at | timestamptz | |
| last_run_at | timestamptz | |
| last_error_at | timestamptz | |
| created_at/updated_at/deleted_at | timestamptz | |
Индексы: (user_id, status), bundle_id.
3.10. webhook_endpoints
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| activation_id | uuid FK | |
| token_hash | text UNIQUE | SHA-256 от токена URL |
| secret_encrypted | bytea | Для подписи, если нужен |
| provider | text | |
| status | enum | active, disabled |
| last_event_at | timestamptz | |
| created_at | timestamptz | |
Токен в URL — случайные 32+ байта base64url; в БД только хеш.
3.11. events
| Колонка | Тип | Описание |
|---|
| id | bigserial PK | |
| activation_id | uuid FK | |
| dedup_key | text | |
| provider_event_id | text | NULL |
| payload | jsonb | Маскированный/нормализованный |
| headers | jsonb | Без секретов |
| status | enum | received, queued, processed, skipped, failed |
| received_at | timestamptz | |
| processed_at | timestamptz | |
UNIQUE (activation_id, dedup_key); индекс (activation_id, received_at DESC).
Партиционирование по месяцу.
3.12. runs
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| activation_id | uuid FK | |
| event_id | bigint FK | NULL для ручных/тестовых |
| user_id | uuid FK | Денормализация для запросов |
| status | enum | queued, running, succeeded, failed, skipped, retrying |
| trigger_type | text | webhook, polling, schedule, manual, test |
| attempt | integer | |
| started_at/finished_at | timestamptz | |
| duration_ms | integer | |
| error_type | text | |
| error_code | text | |
| error_message | text | |
| request_id | text | |
| is_test | boolean | |
| created_at | timestamptz | |
Индексы: (activation_id, created_at DESC), (user_id, created_at DESC),
(status, created_at). Партиционирование по месяцу.
3.13. run_logs
| Колонка | Тип | Описание |
|---|
| id | bigserial PK | |
| run_id | uuid FK | |
| level | enum | debug, info, warn, error |
| step | text | ingest, validate, filter, map, action, retry |
| message | text | |
| data | jsonb | Маскировано |
| created_at | timestamptz | |
Индекс (run_id, id).
3.14. subscriptions
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| user_id | uuid FK | |
| activation_id | uuid FK | |
| status | enum | trial, active, past_due, paused, canceled |
| price_rub | numeric(12,2) | |
| started_at | timestamptz | |
| current_period_start/end | timestamptz | |
| next_charge_at | timestamptz | |
| canceled_at | timestamptz | |
| created_at/updated_at | timestamptz | |
UNIQUE activation_id (одна подписка на активацию).
3.15. payment_methods
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| user_id | uuid FK | |
| provider | text | yookassa |
| provider_method_id | text | |
| title | text | Карта •••• 4242 |
| is_default | boolean | |
| status | enum | active, expired, removed |
| created_at | timestamptz | |
3.16. payments
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| user_id | uuid FK | |
| subscription_id | uuid FK | NULL |
| amount_rub | numeric(12,2) | |
| currency | text | RUB |
| status | enum | pending, succeeded, canceled, failed, refunded |
| provider | text | yookassa |
| provider_payment_id | text UNIQUE | |
| description | text | |
| receipt_url | text | |
| idempotency_key | text UNIQUE | |
| paid_at | timestamptz | |
| created_at/updated_at | timestamptz | |
3.17. refunds
id, payment_id, amount_rub, reason, status, provider_refund_id,
created_at, processed_at.
3.18. notifications
| Колонка | Тип | Описание |
|---|
| id | uuid PK | |
| user_id | uuid FK | |
| channel | enum | email, telegram, in_app |
| type | text | run_failed, payment_failed, trial_ending, reauth |
| payload | jsonb | |
| status | enum | queued, sent, failed, read |
| sent_at/read_at | timestamptz | |
| created_at | timestamptz | |
Индекс (user_id, status, created_at DESC).
3.19. audit_log
| Колонка | Тип | Описание |
|---|
| id | bigserial PK | |
| actor_type | enum | user, admin, system |
| actor_id | uuid | |
| action | text | activation.pause, refund.create |
| entity_type | text | |
| entity_id | text | |
| before/after | jsonb | Маскировано |
| ip | inet | |
| user_agent | text | |
| created_at | timestamptz | |
Индексы: (entity_type, entity_id), (actor_id, created_at DESC).
3.20. connector_health
connector_key, status (ok/degraded/down), latency_ms, error_rate,
checked_at, message.
3.21. usage_counters
id, activation_id, period_start, runs_count, updated_at.
UNIQUE (activation_id, period_start).
3.22. feature_flags
key, enabled, description, updated_at, updated_by.
3.23. legal_acceptances
id, user_id, document (offer, privacy, consent), version,
accepted_at, ip, user_agent.
3.24. support_requests (P1)
id, user_id, subject, message, run_id, status, created_at.
4. Правила целостности
- Нельзя активировать связку с неактивным подключением.
- Нельзя удалить подключение, если есть активная активация (без подтверждения).
- При удалении пользователя: анонимизация, связи с платежами сохраняются
в объёме, требуемом законом.
- При удалении активации: вебхук отключается, очередь очищается, история
помечается на удаление через 90 дней.
- Подписка не может быть активной без активации.
- Платёж идемпотентен по
idempotency_key.
5. REST API
5.1. Общие правила
- Base URL:
https://api.easy-connect.ru/api/v1.
- Формат: JSON UTF-8.
- Аутентификация: httpOnly cookie сессии для web;
Authorization: Bearer для
будущего публичного API.
- Версионирование в пути.
- Пагинация:
?limit=20&cursor=..., ответ { data, next_cursor }.
- Сортировка:
?sort=-created_at.
- Идемпотентность: заголовок
Idempotency-Key для POST платежей/активаций.
- Rate limit: заголовки
X-RateLimit-*, 429 с Retry-After.
- Каждый ответ содержит
request_id.
5.2. Формат ошибки
{
"error": {
"code": "CONNECTION_INVALID_TOKEN",
"message": "Telegram не принял токен. Проверьте, что бот существует и токен скопирован полностью.",
"details": { "field": "token" },
"request_id": "req_01HX..."
}
}
| HTTP | Код | Когда |
|---|
| 400 | VALIDATION_ERROR | Неверные поля |
| 401 | UNAUTHENTICATED | Нет/истекла сессия |
| 403 | FORBIDDEN | Нет прав |
| 404 | NOT_FOUND | Объект не найден |
| 409 | CONFLICT | Дубль, состояние |
| 422 | BUSINESS_RULE | Нельзя активировать |
| 429 | RATE_LIMITED | Лимит |
| 500 | INTERNAL | Внутренняя ошибка |
| 502 | PROVIDER_ERROR | Ошибка провайдера |
| 503 | UNAVAILABLE | Сервис недоступен |
5.3. Аутентификация
| Метод | Путь | Описание |
|---|
| POST | /auth/register | {email,password,accept_offer,accept_privacy} |
| POST | /auth/login | {email,password,remember} |
| POST | /auth/logout | Завершить сессию |
| POST | /auth/logout-all | Все сессии |
| POST | /auth/refresh | Обновить сессию (если используется) |
| POST | /auth/forgot-password | Отправить письмо |
| POST | /auth/reset-password | {token,password} |
| POST | /auth/verify-email | {token} |
| GET | /auth/me | Текущий пользователь |
| GET | /auth/oauth/:provider/start | OAuth-вход |
| GET | /auth/oauth/:provider/callback | Callback |
| POST | /auth/2fa/setup | TOTP (P1) |
| POST | /auth/2fa/verify | TOTP (P1) |
5.4. Каталог
| Метод | Путь | Описание |
|---|
| GET | /catalog/bundles | ?q=&category=&source=&target=&sort=&limit=&cursor= |
| GET | /catalog/bundles/:slug | Детали + связанные |
| GET | /catalog/categories | Категории и счётчики |
| GET | /catalog/services | Сервисы |
| GET | /catalog/services/:key | Сервис и его связки |
| POST | /catalog/requests | Заявка на новую связку |
| GET | /catalog/status | Статус коннекторов |
Пример ответа каталога:
{
"data": [
{
"slug": "tilda-to-telegram",
"title": "Заявка Tilda → сообщение в Telegram",
"source": { "key": "tilda", "name": "Tilda", "icon_url": "..." },
"target": { "key": "telegram", "name": "Telegram", "icon_url": "..." },
"category": "forms",
"price_rub": 200,
"popularity": 98,
"status": "published"
}
],
"next_cursor": null
}
5.5. Подключения
| Метод | Путь | Описание |
|---|
| GET | /connections | Список |
| POST | /connections/oauth/:service/start | Начать OAuth, вернуть URL/redirect |
| GET | /connections/oauth/:service/callback | Callback (state) |
| POST | /connections/token | {service, auth_type, fields} |
| POST | /connections/:id/verify | Проверить |
| POST | /connections/:id/reauth | Переподключить |
| DELETE | /connections/:id | Удалить (с проверкой) |
| GET | /connections/:id | Детали (без секретов) |
5.6. Активации
| Метод | Путь | Описание |
|---|
| GET | /activations | Список с метриками |
| POST | /activations | Создать из bundle_slug + подключения + mapping |
| GET | /activations/:id | Детали |
| PATCH | /activations/:id | Имя, mapping, filter |
| POST | /activations/:id/test | Тестовый запуск |
| POST | /activations/:id/activate | Включить (trial/оплата) |
| POST | /activations/:id/pause | Пауза |
| POST | /activations/:id/resume | Возобновить |
| POST | /activations/:id/duplicate | Дублировать (P1) |
| DELETE | /activations/:id | Удалить |
Создание:
{
"bundle_slug": "tilda-to-telegram",
"name": "Заявки с сайта",
"source_connection_id": "uuid",
"target_connection_id": "uuid",
"mapping": {
"chat_id": { "type": "static", "value": "-1001234567890" },
"text": { "type": "template", "value": "Новая заявка: {{name}}, {{phone}}" }
},
"filter": null
}
5.7. Запуски
| Метод | Путь | Описание |
|---|
| GET | /runs | ?activation_id=&status=&from=&to=&limit=&cursor= |
| GET | /runs/:id | Детали + логи |
| POST | /runs/:id/retry | Повторить |
| GET | /runs/export | CSV (P1) |
Ответ детали:
{
"id": "run_01HX...",
"status": "succeeded",
"trigger_type": "webhook",
"attempt": 1,
"duration_ms": 1240,
"started_at": "2026-10-18T09:41:02Z",
"finished_at": "2026-10-18T09:41:03Z",
"logs": [
{ "step": "ingest", "level": "info", "message": "Событие получено" },
{ "step": "map", "level": "info", "message": "Сопоставлено 3 поля" },
{ "step": "action", "level": "info", "message": "Сообщение отправлено" }
],
"input_masked": { "name": "Иван", "phone": "+7•••••1234" },
"error": null
}
5.8. Биллинг
| Метод | Путь | Описание |
|---|
| GET | /billing/summary | Баланс, следующий платёж, активные подписки |
| GET | /billing/subscriptions | Подписки |
| POST | /billing/subscriptions/:id/cancel | Отменить продление |
| POST | /billing/checkout | Создать платёж (ЮKassa) |
| GET | /billing/payments | История |
| GET | /billing/payments/:id/receipt | Чек |
| POST | /billing/payment-methods | Добавить способ (P1) |
| DELETE | /billing/payment-methods/:id | Удалить (P1) |
| POST | /webhooks/yookassa | Вебхук провайдера (без auth, подпись) |
Checkout:
{
"activation_ids": ["uuid"],
"save_payment_method": true,
"return_url": "https://easy-connect.ru/app/billing/success"
}
Ответ: { "confirmation_url": "https://yoomoney.ru/..." }.
5.9. Уведомления
| Метод | Путь | Описание |
|---|
| GET | /notifications | Список |
| POST | /notifications/:id/read | Прочитать |
| POST | /notifications/read-all | Прочитать все |
| PUT | /users/me/notifications | Настройки каналов |
| POST | /users/me/telegram/link | Привязка Telegram |
5.10. Пользователь
| Метод | Путь | Описание |
|---|
| GET | /users/me | Профиль |
| PATCH | /users/me | Имя, timezone |
| POST | /users/me/password | Смена пароля |
| GET | /users/me/sessions | Сессии |
| DELETE | /users/me/sessions/:id | Завершить |
| GET | /users/me/export | Экспорт (async) |
| DELETE | /users/me | Удаление (grace 7 дней) |
| GET | /users/me/legal | Принятые документы |
5.11. Админка
| Метод | Путь | Описание |
|---|
| GET | /admin/users | Поиск |
| GET | /admin/users/:id | Карточка |
| POST | /admin/users/:id/block | Блокировка |
| GET | /admin/bundles | Каталог |
| POST | /admin/bundles | Создать |
| PATCH | /admin/bundles/:id | Изменить |
| POST | /admin/bundles/:id/publish | Опубликовать |
| GET | /admin/runs | Поиск |
| POST | /admin/runs/:id/retry | Повторить |
| GET | /admin/payments | Платежи |
| POST | /admin/payments/:id/refund | Возврат |
| GET | /admin/incidents | Инциденты |
| POST | /admin/incidents | Создать |
| GET | /admin/audit | Аудит |
5.12. Служебные
| Метод | Путь | Описание |
|---|
| POST | /hooks/in/:token | Приём вебхука (public) |
| GET | /health/live | Liveness |
| GET | /health/ready | Readiness |
| GET | /metrics | Prometheus (внутренний) |
| GET | /openapi.json | OpenAPI 3.1 |
6. Внутренние события (очереди)
6.1. event.received
{
"event_id": 123456,
"activation_id": "uuid",
"dedup_key": "sha256...",
"trigger_type": "webhook",
"received_at": "2026-10-18T09:41:02Z"
}
6.2. run.requested
{
"run_id": "uuid",
"activation_id": "uuid",
"event_id": 123456,
"attempt": 1,
"trace_id": "..."
}
6.3. run.finished
{
"run_id": "uuid",
"activation_id": "uuid",
"status": "succeeded",
"duration_ms": 1240,
"error_type": null
}
6.4. connection.needs_reauth
{
"connection_id": "uuid",
"user_id": "uuid",
"service": "telegram",
"reason": "invalid_grant"
}
7. OpenAPI и контракты
- Спецификация генерируется из NestJS (
@nestjs/swagger), публикуется /openapi.json.
- Клиентские типы —
openapi-typescript.
- Валидация запросов/ответов — Zod-схемы, общие для web и api (пакет
@easy-connect/contracts).
- Breaking changes — только в новой версии API (
/api/v2), с периодом поддержки ≥ 6 месяцев.
- Публичный API (P1) использует API-ключи со scopes:
catalog:read, activations:read,
activations:write, runs:read.
8. Вебхуки наружу (P2)
- Подписка на события:
run.succeeded, run.failed, activation.paused.
- Подпись
X-EC-Signature: t=...,v1=HMAC-SHA256(t.body).
- Повторы 5 раз, DLQ, журнал доставок.
- Секрет per endpoint, ротация.
9. Требования к API
- p95 ответа ≤ 300 мс для чтения, ≤ 500 мс для записи (без внешних вызовов).
- Идемпотентность POST с
Idempotency-Key (24 часа).
- Никаких секретов в ответах и логах.
- Все списки — с пагинацией; лимит по умолчанию 20, максимум 100.
- Все даты — ISO 8601 UTC.
- Локализация ошибок — русский, коды стабильны.
- CORS: только
https://easy-connect.ru, https://www.easy-connect.ru, staging.
- CSP, HSTS,
X-Content-Type-Options: nosniff.
- Аудит всех мутаций в админке.