Контекст бизнеса
Региональный оператор по обращению с ТКО. Это компания, которой регион делегировал монопольную функцию по сбору, транспортированию, обработке и захоронению твёрдых коммунальных отходов. По 89-ФЗ статус регоператора получают через конкурсный отбор, договор с субъектом РФ заключается на 10 лет, с обязательной ежегодной отчётностью в Росприроднадзор, региональный департамент, ФАС.
В Москве у регоператора тысячи контрагентов. Это бюджетные учреждения, коммерческие юрлица, балансодержатели жилого фонда. С каждым отдельный договор, отдельный график вывоза, отдельный ответственный менеджер. Мэрия выпустила нормативку, которая обязала вести именованный реестр с указанием договоров, ответственных, сроков, классов опасности отходов, и отдавать выгрузку регулятору по первому запросу.
Это не sales-CRM. Это аккаунт-CRM с compliance-обвязкой. Задача не продавать, а корректно учитывать обязательства перед регулятором.
Проблема до внедрения
Учёт вёлся в Excel. На каждое ведомство и крупного контрагента шёл отдельный файл. Версии путались, плодились копии вида v2_final_новые правки.xlsx. Изменения никто не фиксировал.
- Сборка среза для регулятора занимала 1–2 дня вручную. Открывали 5–10 файлов, копировали строки, сверялись с почтой.
- Поиск договора по ИНН отнимал 15–40 минут. Открыть файл, найти строку, свериться с распечаткой, уточнить у менеджера.
- Аудит изменений невозможен. На вопрос регулятора «когда вы узнали о расторжении» ответить нечего.
- Доступ из-за периметра шёл по почте. Менеджер в командировке скачивал файл себе на личный ноутбук. Это нарушение 152-ФЗ.
- Потеря данных при увольнении менеджера. Файлы у него на машине, в почте, на личном Я.Диске.
Перед оператором стоял выбор. Либо Bitrix24/amoCRM/1С CRM с 3–6 месяцами адаптации (cloud-only, ПДн вне периметра, фиксированный набор полей под B2C-воронку), либо узкоспециализированное self-hosted решение. Выбрали второе.
Решение
Self-hosted веб-приложение на одном VPS (4 vCPU / 8 GB / 100 GB SSD) в российском облаке. Docker Compose под systemd, nginx как reverse-proxy и TLS-терминация. Реестр контрагентов в PostgreSQL 17 со структурированными полями (ИНН, ведомство, договор, контактное лицо, ответственный менеджер) + EAV-таблица кастомных полей, которые бизнес-аналитик заказчика добавляет без программиста. Три роли: администратор, менеджер, наблюдатель. JWT + TOTP-2FA + Яндекс OAuth. Append-only audit-таблица фиксирует каждое write-действие с before/after-снимком. Экспорт XLSX по шаблонам под формат регулятора. REST API на каждую сущность. ПДн не покидают периметр оператора. Бэкап делает WAL-G в S3 каждые 60 секунд плюс ежесуточный Restic-снимок всего узла. RPO 15 минут, измеренный RTO 27 минут на репетиции, тестовый restore раз в квартал.
Технический стек. Что выбрал и почему
Стек коротко
Пять блоков ниже разбирают выбор по каждому слою с обоснованием «почему именно это, а не альтернатива». Совсем кратко так. Frontend на React 19 + Vite, backend на Express + Sequelize, БД PostgreSQL 17 c EAV для кастомных полей, экспорт XLSX по шаблонам под формат регулятора.
Frontend
React 19 + TypeScript + Vite 7 + Tailwind 3 + Shadcn/ui (Radix primitives) + Zustand 5 + Recharts 2 + React Hook Form + Zod.
Это внутренний инструмент на ~10 одновременных пользователей. SSR не нужен, SEO не нужен, hydration overhead Next.js не оправдан. Vite даёт dev за 200–400 мс и build под 8 секунд. Zustand вместо Redux Toolkit, потому что 90% state это формы и таблицы. Shadcn (не MUI/Ant) держит компоненты прямо в репозитории. Правил accessibility и кастомизацию руками, без обхода через !important.
Backend
Node.js + Express 4 + TypeScript + Sequelize 6 + PostgreSQL 17 + JWT (access + refresh) + Speakeasy TOTP + Passport Яндекс OAuth.
Express, не Fastify/NestJS, и это намеренно. NestJS для команды из 2 человек и 10 пользователей даёт лишний boilerplate. Fastify быстрее на бенчмарках, но узкое место CRM это БД-запросы и Sequelize-сериализация, не HTTP-throughput. У Express нулевой барьер найма и понятный middleware-стек.
ORM
Sequelize 6, не Prisma и не Drizzle. Prisma на момент проектирования ограничивала JSONB и сложные where (важно для динамических фильтров реестра), плюс rust-движок добавлял операционных рисков. Drizzle нравится как «SQL-first», но миграции и сидеры были сырыми. Sequelize брал скучной надёжностью. 13 лет в продакшене, прозрачный SQL через logging, JSONB нативно.
Кастомные поля через EAV
Таблица custom_fields (определения) + custom_field_values (fieldId, entityId, value TEXT). Сознательный выбор vs metadata JSONB или генерация миграций на лету. EAV даёт фильтрацию по конкретному полю с индексом и аудит истории. Из минусов join-ы при фильтре по нескольким полям и типы как TEXT с кастом в приложении.
Экспорт XLSX
SheetJS Community. Для отчётов под нормативку сделан отдельный pipeline. Данные в Postgres-VIEW → Zod-схема → XLSX по шаблону. Шаблон в репо, при изменении формата регулятора правится за 2–4 часа.
Архитектура
Один VPS, пять процессов в Docker Compose под systemd. Это nginx (TLS-терминация и reverse-proxy), backend (Node.js+Express, REST API), worker (BullMQ-задачи в фоне), scheduler (cron-обёртка для плановых отчётов), Postgres 17 локально на том же узле. Redis живёт в отдельном контейнере для сессий и BullMQ-очереди. Микросервисов нет. Модульный монолит на ~10 пользователей и десятки тысяч записей обходится без раздробления. Один файл compose.yml, один systemd-unit, обслуживается мной по retainer-у за пару часов в месяц.

Скриншоты системы
Имя CRM-продукта на скриншотах заблюрено, имя заказчика и имена реальных контрагентов скрыты. Состав интерфейса передан без правок.
Дашборд оператора

Реестр контрагентов


Отчёты для регулятора


Документооборот

Мониторинг zakupki.gov.ru

Настройки безопасности и интеграций



Аудит изменений

Production и DR
Production стоит на одном VPS в российском облаке. Это 4 vCPU / 8 GB / 100 GB NVMe SSD, Debian 13. Docker Compose под systemd-unit, auto-restart при OOM или kernel-panic. Никакого Kubernetes. На 10 одновременных пользователей и 18 000 записей в реестре это overengineering, который добавляет операционных расходов больше чем приносит пользы.
Сервисы в Compose
| Сервис | Образ / процесс | Назначение |
|---|---|---|
| nginx | nginx:1.27-alpine | TLS-терминация, reverse-proxy на backend, отдача React-bundle, gzip+brotli, rate-limit /api/auth/login = 5/мин |
| backend | Node.js 24 + Express | REST API, JWT-issue, валидация, audit-логирование |
| worker | Node.js 24 + BullMQ consumer | Фоновые задачи. Экспорт XLSX, рассылка уведомлений, очистка expired сессий |
| scheduler | Node.js 24 + node-cron | Плановые отчёты регулятору, ежедневные сводки менеджеров |
| postgres | postgres:17-alpine | Основная БД, том на хосте, shared_buffers 2 GB |
| redis | redis:7-alpine + AOF | Сессии, BullMQ-очередь, кэш справочников |
TLS и публичная сеть
nginx на 443 держит TLS 1.3 only, HSTS preload, OCSP stapling. Сертификат выдаёт certbot с DNS-01 через провайдера DNS заказчика (renewal cron). Rate-limit на /api/auth/login стоит 5/мин на IP, на /api 100 запросов в 15 минут. CSP в enforce с default-src 'self', frame-ancestors 'none'. fail2ban по логам nginx даёт banhammer на 24 часа для > 20 ошибок аутентификации с одного IP.
Сеть и доступы
Только 443/22 на публичный IP. SSH работает по ключу, port knocking опционально, root-логин запрещён. ufw deny by default. Postgres и Redis слушают только 127.0.0.1 и доступны через docker-сеть. iptables-правило: исходящий трафик из контейнеров наружу идёт в белый список доменов (S3 для бэкапа, Let's Encrypt, реестр обновлений Debian). GeoIP-фильтр на nginx применяю только к /api/auth/login. Резать всё подряд по стране нельзя, у заказчика менеджеры в командировках за рубежом. Остальной входящий трафик фильтруется по поведению через CrowdSec + fail2ban (> 20 ошибок 4xx с одного IP за 5 минут = 24 часа бана).
Секреты
Все секреты лежат в .env файле на хосте, владелец root:dkayumov-app, права 0640. Шифрованы через sops + age в Git-репо (infra), расшифровываются на хосте при деплое. Ротация раз в 90 дней через скрипт + docker compose up -d. HashiCorp Vault для 10 пользователей это отдельный сервис под обслуживание, не оправдан.
CI/CD
GitHub Actions по push в main собирает frontend (Vite-build, артефакт) и backend (TypeScript-compile), затем делает SSH-deploy на VPS через ключ из repository secrets. На хосте работает скрипт deploy.sh по цепочке git pull → docker compose pull → docker compose up -d→ smoke-curl. Откат через git revert && deploy.sh за 90 секунд. Образы публикую в Yandex Container Registry с tags sha-<commit> и main.
Backup и DR
| Метрика | Цель | Реализация |
|---|---|---|
| RPO | ≤ 15 минут | WAL-G стримит WAL в S3 каждые 60 сек + basebackup ежесуточно |
| Host snapshot | ежесуточно | Restic дампит /var/lib/postgres + /var/www + /etc + uploads → S3 |
| RTO измеренный | 27 минут | Репетиция на новом VPS из образа + restic restore + WAL replay |
| Retention | 30 дн daily, 12 мес monthly | WAL-G + Restic policy |
| Test restore | раз в квартал | Восстановление на staging-VPS + smoke + checksum 100 строк |
| DR-площадка | cold standby в другом регионе | Реплика S3-бакета + готовый cloud-init для подъёма за 30 минут |
SLO 99.5% за 12 месяцев подтверждён. 99.5% дают 3.6 часа допустимого даунтайма в месяц или 43.8 часа в год. По факту за год был один простой 26 минут (плановое окно для PG-upgrade). Заявить 99.9% или 99.99% не готов. Это HA-конфигурация, нужен второй VPS под балансировщиком и managed-Postgres с репликой, что добавит +60–100% к инфра-бюджету при 10 пользователях.
RTO 27 минут это operator-assisted, не auto-failover. Я получаю Telegram-алерт, поднимаю DR-VPS из cloud-init, запускаю Restic-restore + WAL replay, переключаю DNS. Регулятору это объяснил отдельным runbook-документом. Подход «human-in-the-loop» допустим для внутренней учётной системы со специальной категорией УЗ-3.
Observability
| Слой | Стек | Что покрывает |
|---|---|---|
| Metrics | Prometheus + Grafana (single instance) | RED-метрики backend, Postgres-коннекты, Node heap, диск/CPU |
| Logs | journald + Loki (single instance) | Логи docker compose + nginx access/error, retention 30 дней |
| Exporters | node_exporter, postgres_exporter, redis_exporter, blackbox | Hostmetrics, БД-метрики, RTT эндпоинтов, certificate-expiry |
| Uptime | Внешний пинг (UptimeRobot / BetterStack) | Внешняя точка зрения, что сайт реально доступен |
| Alerts | Alertmanager → Telegram-бот мой | p99 latency, error-rate > 1%, диск > 80%, certificate < 14 дней |
Что НЕ покрывает. Отказ всей зоны провайдера (mitigation через DR-snapshot в S3 другого региона + cloud-init для подъёма VPS вручную за 30 минут), сценарий «провайдер закрыл аккаунт» (offsite Restic-snapshot в S3 другого провайдера раз в неделю).
Безопасность и 152-ФЗ
УЗ-3 (специальные категории не обрабатываются, биометрии нет, < 100 000 субъектов в моменте). Модель угроз и нарушителя утверждены заказчиком. Локализация идёт через managed Postgres в РФ-дата-центре, без зарубежных CDN.
Технические меры
- Helmet (X-Frame, Referrer-Policy), CSP в enforce.
- Bcrypt cost 10 (в 2026 поднял бы до Argon2id).
- JWT refresh с серверной инвалидацией.
- TOTP-2FA обязательно для «администратора».
- RBAC через
requireRolemiddleware. - express-validator на каждом write (ИНН проверяется regex + контрольная сумма).
- audit-лог с
user_id/ip/user_agent/ before/after JSONB. npm audit+ Dependabot в CI блокирует high/critical.- Внешний pentest по ASVS L2 раз в полгода.
Что бы поменял в 2026
Оставил бы 70% стека. Поменял по приоритету вот что.
- bcrypt → Argon2id (час работы).
- rate-limit memory → Redis-backed (всё равно уже в кластере).
- refresh token rotation + reuse detection.
- Sequelize → Drizzle (2–3 недели, типы из БД автоматически).
- OpenAPI + tRPC (уберёт дублирование DTO).
- passport-yandex 0.0.5 → собственный OAuth-handler на 30 строк.
- Falco для runtime threat detection (если регулятор повысит требования по аудиту контейнеров).
НЕ менял бы вот это.React 19 + Vite, Tailwind + Shadcn, PostgreSQL 17 + EAV, один VPS + Docker Compose под systemd. p99 < 350 мс, SLO 99.5% подтверждён, RTO 27 минут на репетиции, боли в эксплуатации нет. И не перешёл бы на Kubernetes. Для 10 пользователей это +120 % к OPEX без единой решённой задачи.
Метрики и бюджет
›Сборка среза для регулятора
- До CRM
- 1–2 дня
- После CRM
- 5–15 мин
- Проверка
- Хронометраж 2 циклов
›Поиск договора по ИНН
- До CRM
- 15–40 мин
- После CRM
- 10–30 сек
- Проверка
- Хронометраж пилотной недели
›Аудит контрагента
- До CRM
- невозможен
- После CRM
- полный журнал
- Проверка
- audit-таблица
›Потеря данных при увольнении
- До CRM
- высокий риск
- После CRM
- контролируемая
- Проверка
- Ролевая модель + reassign
›Доступ из-за периметра
- До CRM
- по почте
- После CRM
- заблокирован
- Проверка
- nginx, VPN, 2FA
Я не пишу «выручка выросла на X%». Это зона коммерческого блока оператора. Я отвечаю за инструмент. Когда менеджер не теряет 2 часа в неделю на поиск файла, у него освобождается время на работу с контрагентом.
Бюджет и сроки
С нуля до пилота. Разброс зависит от кастомных полей, интеграций, миграции из Excel
До работающего пилота. 3–6 мес до полной зрелости (все отчёты, интеграции, обучение)
Retainer покрывает инфраструктуру, 1–2 итерации мелких доработок, реакцию в рабочий день, новые поля
Почему не Bitrix24 / amoCRM / 1С CRM
| Критерий | Готовая cloud-CRM | Это решение |
|---|---|---|
| Self-hosted (152-ФЗ) | Нет | Да |
| Где ПДн | Серверы вендора | Контур заказчика |
| Кастомные поля без программиста | Частично | Да |
| Аудит-trail под регулятора | Формат вендора | Под формат регулятора |
| 1-й год при 50 пользователях | 1.2–2.0 млн | 1.5–3.5 млн CAPEX + ~1.5 OPEX |
| 3-й год | 0.8–1.5 млн/год | ~1.5 млн/год OPEX |
| Лок-ин | Высокий | Низкий (исходники у заказчика) |
На горизонте 3 лет cloud-CRM проигрывает только по 152-ФЗ и аудиту. Если регулятор не требует self-hosted и кастомного аудита, Bitrix24 дешевле, и я честно скажу это на ознакомительном звонке. Эта реализация нужна тем, у кого регулятор требует.
Кому это подходит
Этот класс CRM подходит не «всем кто работает с B2G», а узкому сегменту.
- Региональные операторы по обращению с ТКО в других субъектах РФ. Москва тут крайний случай по объёму, но точно такие же реестры с учётом договоров ведутся в Подмосковье, Татарстане, Краснодарском крае, Свердловской области.
- Операторы ресурсоснабжения (вода, тепло, электроэнергия, газ) с тарифным регулированием по 35-ФЗ / 416-ФЗ / 190-ФЗ. Меняется только справочник «класс отхода» на «вид ресурса».
- Транспортные операторы под госконтракт по 220-ФЗ. Реестр маршрутов + договоры с муниципалитетами + аудит для региональных Минтрансов.
- Экологические операторы под лицензированием Росприроднадзора (89-ФЗ + 7-ФЗ).
- Муниципальные предприятия (МУПы, ГБУ). КСП приходит с проверкой по 6-ФЗ и спрашивает срез на конкретную дату.
- Компании в реестре единственных поставщиков по 44-ФЗ. Госконтракты на 500+ контрагентов, проверки от ФАС, хранение переписки 5 лет.
Общий знаменатель один. Есть проверяющий орган, который имеет право прийти и потребовать срез за прошедший период с указанием ответственных и историей изменений. Если такого органа нет, Bitrix24/amoCRM/Notion закроют задачу дешевле.
5 признаков, когда подходит
- Регулируемая отрасль с проверками (Росприроднадзор, ФАС, Минтранс, мэрия, ФНС).
- Переросли Excel/SharePoint, но не готовы в Bitrix24 cloud из-за 152-ФЗ или формата отчётов.
- Нужен self-hosted без облака.
- Бюджет 1.5–4 млн ₽ CAPEX + 80–150 тыс ₽/мес OPEX.
- 20–200 одновременных пользователей.
Когда НЕ подходит
- Меньше 10 пользователей. Excel + Google Drive + дисциплина дешевле.
- Mobile-first sales-CRM с Kanban-воронкой. Это другой жанр, amoCRM или Битрикс24 Sales.
- Готовы к Bitrix24 cloud, нет требований по self-hosted и кастомному аудиту.
- 1000+ пользователей. Нужен enterprise-стек (BPM, оркестратор). Приведу подрядчика, но сам не возьмусь.
- Глубокая real-time двусторонняя интеграция с 1С УТ. Отдельный класс работ.
- Нет внутреннего владельца продукта. CRM без человека, отвечающего за справочники и кастомные поля, протухает за полгода.
Roadmap внедрения по фазам
Фаза 0 · Первичный разбор
1–2 недели · 0 ₽2–3 интервью с владельцем продукта, изучение Excel-файлов, разговор с регулятором через юр.отдел, список 30–50 обязательных полей. Отдаю ТЗ, оценку CAPEX/OPEX ±20%, ответ «делаем или нет». Если не делаем, называю альтернативу. Не пишу код.
Фаза 1 · MVP
4–6 недель · 800k–1.5M ₽Логин, реестр с базовыми полями, ролевая модель, audit-таблица, экспорт XLSX по одному шаблону, одна машина без HA, бэкап ежедневный. Сюда НЕ входят 2FA, OAuth, кастомные поля через UI, дашборд, hot-standby. Тестовый прогон с искусственными данными. Критерий простой. 5 менеджеров на demo говорят «вместо Excel я бы этим пользовался».
Фаза 2 · Пилот в боевой эксплуатации
8 недель · 500k–1M ₽Добавляю 2FA, OAuth, кастомные поля через UI, дашборд, миграция из Excel (часто 60% работы фазы), обучение, нагрузочное (k6, 50 RPS), production HA, hot-standby, WAL-G stream, Prometheus + Grafana. Самая важная фаза. Критерий такой. 4 отчётных среза собраны через CRM, uptime ≥99%, менеджеры сами не хотят возвращаться в Excel.
Фаза 3 · Расширение
3–6 месяцев · 1–2M ₽По приоритету идут интеграция с 1С УТ (REST + HTTP-сервис на стороне 1С), расширенный аудит с электронной подписью, отчёты по 172-ФЗ, mobile-friendly, ГИС ЖКХ, SSO с AD/LDAP, Loki/Tempo. Миграцию на managed Postgres (Yandex Managed PostgreSQL или аналог) запускают два триггера. Рост до 50+ одновременных пользователей либо передача эксплуатации внутренней команде заказчика. Тогда управление PITR, патчингом и репликами уходит провайдеру, +12–18 тыс. ₽/мес к OPEX, минус 4–6 часов операционной работы в месяц.
Фаза 4 · Steady state
бесконечно · 80–150k ₽/месМониторинг 24/7, реакция в рабочий день, security updates, 1–2 итерации в месяц, ежеквартальный test-restore, ежегодный pentest. На заказчике остаются пользователи, роли, кастомные поля, ежедневные операции.
Change management. Как перевести менеджеров с Excel
Технически CRM строится за 6 недель, но если менеджеры её не примут, деньги в трубу. Работает принцип «сохранить привычки, изменить инфру».
Что сохраняем
- Колонки в табличном виде в том же порядке, как в их основном Excel. После 2–3 недель менеджеры сами просят переставить, это их инициатива.
- Привычные термины. Если менеджеры говорят «АХ-документы», то в UI «АХ-документы», не «акты приёма-сдачи».
- Excel-экспорт в один клик. 90% менеджеров продолжают использовать Excel для фильтрации и копипаста в почту. Кнопка «выгрузить» в каждом списке, не в подменю.
- Возможность вернуться к Excel. Первые 6–8 недель идёт двойной учёт. Через 2 месяца сами менеджеры просят отменить.
Что меняем
- Поиск. В Excel это Ctrl+F по одному файлу. В CRM глобальный по всем полям с подсказками. Главный wow-эффект.
- Назначение задач. Дата + действие + напоминание. Появляется чувство «система помнит мои задачи».
- Кастомные поля без программиста. Когда регулятор присылает «теперь нужно поле «номер плановой проверки»», бизнес-аналитик добавляет за 5 минут. В Bitrix24 это запрос в техподдержку.
- Аудит. «Кто и когда поменял этот статус» открывает кнопка «история изменений». Менеджеры перестают бояться сломать.
Признаки успеха через 2–3 месяца
- Менеджеры открывают CRM до того, как открыть Excel.
- Бизнес-аналитик добавил минимум 5 кастомных полей сам.
- В audit-таблице больше 100 правок в день от не-административных аккаунтов.
- Новых менеджеров обучают сами менеджеры, не я.
- В Excel заведено меньше 10% новых контрагентов.
Если через 2–3 месяца этих признаков нет, значит что-то пошло не так. Возвращаюсь в проект, делаю 5–10 интервью, нахожу узкие места, правлю UI. Это часть retainer-а.
FAQ
›Какой минимальный размер компании?
50 пользователей, 500+ контрагентов в реестре, есть проверяющий регулятор. Если меньше, Bitrix24 cloud дешевле. Если сильно больше (1000+), нужен enterprise-стек.
›Сколько стоит миграция данных из Excel?
От 100к ₽ (200 контрагентов в одном файле, без дубликатов) до 800к ₽ (5000 в 20 файлах с дубликатами, неконсистентность, нормализация справочников). Делается до старта пилота.
›Что если регулятор поменяет формат отчёта?
Шаблон лежит отдельным файлом в репо (templates/report_172fz_v3.xlsx + YAML-схема маппинга). Изменение формата это правка за 2–4 часа, без релиза. На retainer входит без доплат, если структура сохраняется.
›Можно ли интегрировать с 1С УТ?
Да, через REST API на стороне CRM + HTTP-сервис на стороне 1С. Сроки 3–6 недель, стоимость 400–800к ₽. Часто заказчики ограничиваются односторонней выгрузкой контрагентов из CRM в 1С, и это в 2 раза дешевле.
›Кто отвечает за uptime?
В retainer SLA 99.5% на app-tier. За data-tier (managed Postgres) отвечает облачный провайдер заказчика, у него SLA 99.95%. За сетевой стек отвечает провайдер. На моих руках приложение, мониторинг, инциденты, бэкапы.
Ознакомительный звонок
30 минут бесплатно. По итогам вот что.
- Считаю CAPEX и OPEX под вашу задачу. Конкретные цифры ±20%, не «от 1 млн до бесконечности».
- Говорю, подходит ли self-hosted решение или вам дешевле Bitrix24/amoCRM. Если дешевле, называю прямо, не пытаюсь продать.
- Спрашиваю про регулятора, объём контрагентов, текущий учёт, наличие 1С, требования по 152-ФЗ.
- Если подходит, расписываю первые 2 недели работы. Что увижу, что отдам, какие данные нужны.
Похожая задача? Ознакомительный звонок 30 минут
Расскажете задачу, отвечу за 4 часа в рабочее время и предложу подходящий формат. Аудит, пилот, retainer. Звонок бесплатный, без обязательств.