Перейти к содержимому
easy-connect
Содержание ТЗ
Техническое заданиеВерсия 1.0 · 18.10.2026

05. Модель данных и API

Модель данных 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

КолонкаТипОграниченияОписание
iduuidPK
emailcitextUNIQUE NOT NULLЛогин
password_hashtextNULLArgon2id; NULL для OAuth-only
nametextNULL
localetextDEFAULT 'ru'
timezonetextDEFAULT 'Europe/Moscow'
statusenumactive, blocked, deleted
email_verified_attimestamptzNULL
last_login_attimestamptzNULL
marketing_opt_inbooleanDEFAULT false
created_at/updated_at/deleted_attimestamptz

Индексы: lower(email), status.

3.2. sessions

КолонкаТипОписание
iduuid PK
user_iduuid FK
token_hashtext UNIQUESHA-256 от токена
ipinet
user_agenttext
expires_attimestamptz
revoked_attimestamptz
created_attimestamptz

Индексы: (user_id, expires_at), token_hash.

3.3. oauth_identities

КолонкаТипОписание
iduuid PK
user_iduuid FK
providertextvk, yandex
provider_user_idtext
created_attimestamptz

UNIQUE (provider, provider_user_id).

3.4. services

КолонкаТипОписание
iduuid PK
keytext UNIQUEtelegram, tilda
nametext
categorytextcrm, messengers, tables, forms, sms, payments
icon_urltext
descriptiontext
docs_urltext
statusenumactive, beta, disabled
created_at/updated_attimestamptz

3.5. service_versions

КолонкаТипОписание
iduuid PK
service_iduuid FK
versiontextSemVer
manifestjsonbПолный манифест
is_currentboolean
published_attimestamptz

UNIQUE (service_id, version).

3.6. bundles (каталог)

КолонкаТипОписание
iduuid PK
slugtext UNIQUEtilda-to-telegram
titletext
descriptiontext
categorytext
source_service_iduuid FK
source_trigger_keytext
target_service_iduuid FK
target_action_keytext
mapping_templatejsonb
filter_templatejsonbNULL
required_scopesjsonb
price_rubnumeric(12,2)DEFAULT 200
statusenumdraft, published, deprecated
popularityintegerДля сортировки
seo_title/descriptiontext
created_at/updated_attimestamptz

Индексы: status, category, GIN по title/description (pg_trgm).

3.7. bundle_versions

История изменений шаблона связки: id, bundle_id, version, snapshot jsonb, created_at. Нужна для воспроизводимости активаций.

3.8. connections

КолонкаТипОписание
iduuid PK
user_iduuid FK
service_iduuid FK
external_account_idtextID аккаунта/бота
external_account_nametextОтображаемое имя
auth_typetextoauth2, token, bot_token, basic, webhook
credentials_encryptedbyteaШифртекст
key_versionintegerВерсия ключа
scopestext[]
statusenumactive, needs_reauth, revoked, error, disabled
expires_attimestamptzNULL
last_checked_attimestamptz
metadatajsonbНесекретные данные
created_at/updated_attimestamptz

Индексы: (user_id, service_id), status.

3.9. activations

КолонкаТипОписание
iduuid PK
user_iduuid FK
bundle_iduuid FK
bundle_versiontext
source_connection_iduuid FK
target_connection_iduuid FK
nametextПользовательское имя
mappingjsonb
filterjsonbNULL
statusenumdraft, trial, active, paused, error, deleted
trial_ends_attimestamptz
activated_attimestamptz
paused_attimestamptz
last_run_attimestamptz
last_error_attimestamptz
created_at/updated_at/deleted_attimestamptz

Индексы: (user_id, status), bundle_id.

3.10. webhook_endpoints

КолонкаТипОписание
iduuid PK
activation_iduuid FK
token_hashtext UNIQUESHA-256 от токена URL
secret_encryptedbyteaДля подписи, если нужен
providertext
statusenumactive, disabled
last_event_attimestamptz
created_attimestamptz

Токен в URL — случайные 32+ байта base64url; в БД только хеш.

3.11. events

КолонкаТипОписание
idbigserial PK
activation_iduuid FK
dedup_keytext
provider_event_idtextNULL
payloadjsonbМаскированный/нормализованный
headersjsonbБез секретов
statusenumreceived, queued, processed, skipped, failed
received_attimestamptz
processed_attimestamptz

UNIQUE (activation_id, dedup_key); индекс (activation_id, received_at DESC). Партиционирование по месяцу.

3.12. runs

КолонкаТипОписание
iduuid PK
activation_iduuid FK
event_idbigint FKNULL для ручных/тестовых
user_iduuid FKДенормализация для запросов
statusenumqueued, running, succeeded, failed, skipped, retrying
trigger_typetextwebhook, polling, schedule, manual, test
attemptinteger
started_at/finished_attimestamptz
duration_msinteger
error_typetext
error_codetext
error_messagetext
request_idtext
is_testboolean
created_attimestamptz

Индексы: (activation_id, created_at DESC), (user_id, created_at DESC), (status, created_at). Партиционирование по месяцу.

3.13. run_logs

КолонкаТипОписание
idbigserial PK
run_iduuid FK
levelenumdebug, info, warn, error
steptextingest, validate, filter, map, action, retry
messagetext
datajsonbМаскировано
created_attimestamptz

Индекс (run_id, id).

3.14. subscriptions

КолонкаТипОписание
iduuid PK
user_iduuid FK
activation_iduuid FK
statusenumtrial, active, past_due, paused, canceled
price_rubnumeric(12,2)
started_attimestamptz
current_period_start/endtimestamptz
next_charge_attimestamptz
canceled_attimestamptz
created_at/updated_attimestamptz

UNIQUE activation_id (одна подписка на активацию).

3.15. payment_methods

КолонкаТипОписание
iduuid PK
user_iduuid FK
providertextyookassa
provider_method_idtext
titletextКарта •••• 4242
is_defaultboolean
statusenumactive, expired, removed
created_attimestamptz

3.16. payments

КолонкаТипОписание
iduuid PK
user_iduuid FK
subscription_iduuid FKNULL
amount_rubnumeric(12,2)
currencytextRUB
statusenumpending, succeeded, canceled, failed, refunded
providertextyookassa
provider_payment_idtext UNIQUE
descriptiontext
receipt_urltext
idempotency_keytext UNIQUE
paid_attimestamptz
created_at/updated_attimestamptz

3.17. refunds

id, payment_id, amount_rub, reason, status, provider_refund_id, created_at, processed_at.

3.18. notifications

КолонкаТипОписание
iduuid PK
user_iduuid FK
channelenumemail, telegram, in_app
typetextrun_failed, payment_failed, trial_ending, reauth
payloadjsonb
statusenumqueued, sent, failed, read
sent_at/read_attimestamptz
created_attimestamptz

Индекс (user_id, status, created_at DESC).

3.19. audit_log

КолонкаТипОписание
idbigserial PK
actor_typeenumuser, admin, system
actor_iduuid
actiontextactivation.pause, refund.create
entity_typetext
entity_idtext
before/afterjsonbМаскировано
ipinet
user_agenttext
created_attimestamptz

Индексы: (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.

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. Правила целостности

  1. Нельзя активировать связку с неактивным подключением.
  2. Нельзя удалить подключение, если есть активная активация (без подтверждения).
  3. При удалении пользователя: анонимизация, связи с платежами сохраняются в объёме, требуемом законом.
  4. При удалении активации: вебхук отключается, очередь очищается, история помечается на удаление через 90 дней.
  5. Подписка не может быть активной без активации.
  6. Платёж идемпотентен по 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КодКогда
400VALIDATION_ERRORНеверные поля
401UNAUTHENTICATEDНет/истекла сессия
403FORBIDDENНет прав
404NOT_FOUNDОбъект не найден
409CONFLICTДубль, состояние
422BUSINESS_RULEНельзя активировать
429RATE_LIMITEDЛимит
500INTERNALВнутренняя ошибка
502PROVIDER_ERRORОшибка провайдера
503UNAVAILABLEСервис недоступен

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/startOAuth-вход
GET/auth/oauth/:provider/callbackCallback
POST/auth/2fa/setupTOTP (P1)
POST/auth/2fa/verifyTOTP (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/callbackCallback (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/exportCSV (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/liveLiveness
GET/health/readyReadiness
GET/metricsPrometheus (внутренний)
GET/openapi.jsonOpenAPI 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.
  • Аудит всех мутаций в админке.