CRM и аналитическая платформа для продавцов Amazon и их команд — товары, поставщики, заказы, отгрузки, фриланс-заявки, чаты и AI-инструменты — построенная из трёх сервисов на Node.js: REST API, фоновой очереди и real-time шлюза.
Моя роль
Ведущий backend-разработчик и архитектор всех трёх сервисов — API, очереди и real-time шлюза.
Стек
Node.js
Fastify
BullMQ
Socket.IO
PostgreSQL
Redis
OAuth 2.1
MCP
Задача
В командах продавцов много ролей — владельцы, супервизоры, исследователи, закупщики, кладовщики, фрилансеры, — и все работают с одними и теми же товарами, заказами и отгрузками. Платформа должна показывать каждому ровно то, что ему разрешено, реагировать в реальном времени, выполнять тяжёлые отчёты и AI-задачи в фоне и безопасно пускать к данным внешних AI-ассистентов.
Архитектура
Клиенты обращаются к API на Fastify с короткоживущими JWT и держат WebSocket-соединение со шлюзом на Socket.IO. API пишет в PostgreSQL и передаёт долгую или тяжёлую работу — отчёты, рассылку по чатам, AI-генерацию, задачи по расписанию — воркерам BullMQ через Redis. API и воркеры отправляют события реального времени через шлюз, который работает на нескольких инстансах за Redis adapter. Отдельный PostgreSQL хранит отчёты Amazon, которые записывает внешний парсер, а очередь пересчитывает по ним агрегаты в основную базу. AI-ассистенты подключаются через MCP с OAuth 2.1.
Ключевые решения
01Долгие AI-задачи — асинхронно, с long-polling
Почему
Генерация занимает минуты, а прокси обрывает соединение через 60 секунд, и уже оплаченные результаты терялись. Теперь запрос возвращает 202, клиент делает long-polling, Redis pub/sub будит его раньше, а источником истины остаётся строка в базе. У каждого запроса есть ключ идемпотентности, а автоматических повторов нет: повтор — это вторая оплата.
Компромисс
Больше движущихся частей — статусы, sweeper для зависших задач, — а бюджеты можно соблюдать только мягко.
02Контроль доступа слоями, а не в одном месте
Почему
Права на URL, роли, область владения в репозиториях, маскирование полей при сериализации и получатели уведомлений проверяются каждый на своём уровне. MCP-ключ получает пересечение своих прав и текущих прав владельца, поэтому AI-ассистент никогда не видит больше, чем пользователь.
Компромисс
Каждый новый эндпоинт нужно зарегистрировать на нескольких уровнях.
03Блокировки строк в фиксированном порядке для отгрузок
Почему
Две вкладки браузера, пересоздающие одну коробку, раньше давали дубли и удваивали остатки. Теперь пересоздание идёт в транзакции с SELECT … FOR UPDATE в фиксированном порядке и условным обновлением; проигравший запрос получает 409, а не портит данные.
Компромисс
Каждый эндпоинт, работающий с коробками, должен проходить через ту же защиту.
04Курсоры прочтения вместо строки на каждого читателя
Почему
Хранение того, кто прочитал каждое сообщение, растёт как число сообщений, умноженное на число участников. Один курсор на пользователя в чате — и в ветке — делает счётчики непрочитанного простыми, а таблицу маленькой.
Компромисс
Правила непрочитанного живут и в API, и в шлюзе, и их нужно держать согласованными.
Что я сделал
Три сервиса — REST API на Fastify, воркеры BullMQ и шлюз на Socket.IO — с общими PostgreSQL и Redis
OAuth 2.1 + PKCE провайдер и MCP-сервер: Claude и ChatGPT работают с данными CRM в пределах прав пользователя
Пять уровней контроля доступа: права на URL, роли, владение сущностями, маскирование полей и получатели уведомлений
Короткоживущие access-токены с ротацией refresh, лимитами сессий, саб-пользователями, имперсонацией и SSO для внутренних сервисов
Асинхронная AI-генерация с long-polling, ключами идемпотентности, отменой и бюджетами на пользователя и команду
Чаты с ветками, реакциями, закрепами и курсорами прочтения на многоинстансном Socket.IO с Redis adapter
Отдельная база отчётов, которую пополняет внешний парсер данных Amazon, и пересчёты по расписанию в очереди
Миграции без простоя: конкурентные индексы, внешние ключи NOT VALID и lock timeout