KEEL.
CRM для ВЭД · клиент → менеджер → декларант
Обезличенность как свойство продукта

Менеджер знает клиента.
Декларант знает дело.
Друг о друге они не знают ничего.

Keel — веб-CRM для внешнеэкономической деятельности, в которой разграничение доступа держат сервер и физически раздельные базы, а не скрытые поля на экране. Если данные сотруднику не положены — поля не существует ни в интерфейсе, ни в ответе API, ни в поиске.

Rust · axum + sqlx Vue 3 + TypeScript 11 сервисов + коннекторы каналов Postgres operational + vault поставка на ваш контур
Экран менеджера заявка R-2026-0412
Клиент
ООО «Балтик Транзит» раскрыто 11:04 · причина: звонок клиента
Контактное лицо
Кузнецова А. В. автоскрытие через 2:00
Направление
Таможенное оформление · импорт
Операц. дело
case_ref C-2026-0311
Декларант
поля нет в ответе API
Экран декларанта дело C-2026-0311
Контрагент
c_2026_004417
Код ТН ВЭД
8471 30 000 0
Направление
Таможенное оформление · импорт
SLA
выпуск ДТ — осталось 6 ч 12 мин
Клиент, контакты
поля нет в ответе API

Одна и та же работа, две аудитории. Псевдоним контрагента — HMAC(ключ аудитории, субъект): у менеджера и у декларанта он разный для одного и того же клиента, поэтому сопоставить записи между аудиториями нельзя даже при полном доступе к операционной базе.

Проблема

Почему типовая CRM не годится там, где заявку ведёт один человек, а оформление делает другой.

Что ломается в обычной CRM на процессе ВЭД

Каждая из этих ситуаций — не пробел в настройках, а следствие того, что разграничение доступа сделано на уровне интерфейса поверх общей базы, где все данные лежат рядом.

Доступ разграничен на экране, а не в API

Поле спрятано настройкой формы, а эндпоинт отдаёт карточку целиком. Проверяется одним запросом мимо интерфейса.

→ В Keel ответ собирает сервер по правам вызывающего: состояния «поле есть, но заблокировано» в продукте нет.

База уходит вместе с человеком

Список контрагентов с телефонами выгружается одной кнопкой, и следа об этом не остаётся.

→ Право «знать клиента» (client.read) выдаётся отдельно от права вести заявку, а каждое раскрытие ПД попадает в append-only журнал.

Декларант получает клиента «в нагрузку»

Исполнителю нужны груз, коды и сроки — но вместе с заявкой ему достаются реквизиты и контакты. Это готовый мост, по которому сделка уходит мимо компании.

→ Операционное дело живёт в отдельном сервисе и отдельной базе, где нет ни одной колонки о клиенте и заявке.

Доказать, кто видел персональные данные, нечем

На запрос «кто открывал данные этого контрагента 14 марта» система отвечает логами приложения — в которых те же самые ПД и лежат.

→ Раскрытие — отдельная операция с причиной и лимитом частоты; в операционной базе, логах и текстах ошибок персональных данных нет, и это проверяет CI.

Переписка с клиентом живёт в личном телефоне

Клиент пишет в Telegram и WhatsApp, отвечает конкретный менеджер со своего аккаунта. У компании этой истории нет — она есть у сотрудника.

→ Канальный контур принимает диалоги на учётные записи компании, привязывает их к заявке и псевдонимизирует контакт до записи в операционную базу.

Заявка, задачи и документы — в трёх системах

CRM не знает про сроки задач, таск-трекер не знает про SLA заявки, документы лежат в облачном диске без версий и прав.

→ Заявка, дело, задачи, Гант, учёт времени, риски и документы с экспортом в DOCX и PDF — один контур с единой моделью прав.

Красные линии

Четыре правила продукта. Их нарушение считается дефектом, а не замечанием по стилю.

Обещания, у которых есть механизм и проверка

Формулировка «мы заботимся о данных» ничего не стоит без ответа на вопрос, чем именно она обеспечена и как это проверить снаружи. Ниже — обе половины ответа.

Персональные данные не попадают в операционную базу

Механизм
Два физически раздельных инстанса Postgres: vault (ФИО, телефоны, адреса, реквизиты, название организации, оригинальные имена файлов) и operational (только псевдонимы и коды). Сетевой доступ к vault есть у одного сервиса из одиннадцати.
Проверка
Скрипт в CI разбирает схему операционной базы; дамп operational не содержит читаемых ПД — включая тексты ошибок и записи журналов.

Права проверяются на каждом запросе, а не в токене

Механизм
Полномочия не кэшируются ни в сессии, ни в токене: сервис-владелец данных спрашивает их в момент обработки запроса. Для долгоживущих запросов доступ перепроверяется ещё раз перед отправкой ответа.
Проверка
Отзыв права администратором действует немедленно и не воскресает при повторном входе; окно ожидания висящего запроса его не переживает.

Если данных не положено — поля не существует

Механизм
Ответ API формируется по правам вызывающего. Запрещены не только скрытые поля, но и частичные маски вида n***@domain: они подтверждают существование значения и его форму.
Проверка
Чужой и несуществующий псевдоним отвечают одинаково — 404. Иначе разница ответов сама по себе подтверждает, что субъект есть.

Раскрытие персональных данных — операция, а не JOIN

Механизм
Явное действие с указанием причины из справочника: проверка права → лимит частоты → запись в журнал → выдача из vault → таймер автоскрытия на экране. Права pii.view, pii.reveal и download независимы, назначаются и отзываются администратором.
Проверка
Журнал аудита append-only: актор, время, причина, результат. Отказы в доступе пишутся наравне с успехами.
Аудитория

Кому продукт нужен, в каких ролях он живёт и чего в нём сознательно нет.

Для компаний ВЭД, где сделку и оформление ведут разные люди

Таможенные представители, брокеры, экспедиторы, отделы ВЭД производственных компаний — от десятка до двух сотен сотрудников. Особенно там, где декларанты работают как подрядчики или в отдельном юрлице.

Собственник

Клиентская база остаётся у компании

Знание клиента — отдельное право, а не побочный эффект доступа к заявке. Уход менеджера или смена подрядчика-декларанта не уносит контакты.

Служба ИБ и ответственный за ПД

Разграничение можно предъявить

Ролевая матрица снимается запросами к живому стенду, а не выводится из чтения кода. Журнал аудита читают только администратор и офицер ИБ.

Руководитель операций

Сроки видны до того, как сгорят

Кокпит с плитками проекта, просроченным и «горящим», ближайшей вехой и максимальным открытым риском; SLA заявки с эскалациями.

Менеджер и декларант

Одно рабочее место вместо пяти

Заявка, дело, доска задач, Гант, учёт времени, документы с блочным редактором и экспортом, диалоги с клиентом и внутренний чат — в одном окне.

Роли поставки по умолчанию — состав прав настраивается конструктором ролей
Роль Знает клиента Персональные данные Журнал аудита
Менеджер да · client.read раскрытие по причине · pii.reveal нет
Декларант нет — только c_2026_004417 не существуют нет
Финансист да · client.read видны постоянно · pii.view нет
Офицер ИБ нет не существуют да
Администратор да · client.read по настройке роли да

Чего в первом релизе нет намеренно. Мультитенантного SaaS — поставка идёт изолированной инсталляцией на заказчика, on-prem или в его облаке. Мобильного приложения — только адаптивная вёрстка. Публичного портала клиента. Каждый пункт отложен решением, а не забыт.

Реализация

Ключевые решения и то, что было отвергнуто. Нумерация — из журнала решений проекта.

Решения, принятые при реализации

Тридцать шесть зафиксированных решений и двадцать ADR. Ниже — те, что видно снаружи: они определяют, что продукт может пообещать, а чего не станет обещать никогда.

Контур безопасности

AD-2

Собственный OIDC-провайдер и шлюз. Сторонние identity-провайдеры и внешние авторизационные движки не используются: контур входа целиком свой, обновляется вместе с продуктом.

AD-24

Подпись токенов — EdDSA (Ed25519). Многоключевой JWKS с выбором ключа по отпечатку.

× Отвергнут RS256: вместе с ним из графа зависимостей полностью убран криптографический крейт с открытыми уязвимостями — аудит зависимостей стал зелёным.

AD-32

Сессия входа — строка в базе, а не подписанная кука. Секрет в httpOnly-куке, в базе — его SHA-256. Отзыв сессии срабатывает за миллисекунды.

× Отвергнута кука без состояния: её нельзя погасить до истечения срока, а список отозванных всё равно пришлось бы хранить в базе — два источника правды вместо одного.

AD-33

Срок сессии: 12 часов абсолютный, 2 часа простоя. Скользит только простой; рабочий день — естественная единица, два часа отделяют обед от ухода домой.

× Отвергнут скользящий абсолютный срок: сессия, продлевающая сама себя, не истекает никогда.

AD-36

Перезагрузка страницы не разлогинивает, но и не воскрешает права. Сессия хранит личность, не полномочия: после тихого входа права пересчитываются из базы на момент вызова, а каждый запрос перепроверяется отдельно.

Разделение хранения

AD-16

Операционное дело — отдельный сервис с отдельной базой. CRM-ядро хранит только ссылку на дело, а база дел не хранит о клиенте и заявке ничего. Разграничение обеспечено физическим разделением, а не аккуратностью кода.

AD-20

Аудитория псевдонима выводится из ролей вызывающего, а не приходит параметром. Попросить псевдоним чужой аудитории нельзя. Чужой и несуществующий псевдоним неотличимы снаружи.

AD-19

Сквозного поиска нет. Каждый сервис ищет только в своих данных, результаты склеиваются на экране раздельными группами по источникам.

× Отвергнут поисковый агрегатор: сервис, видящий по одному запросу и заявку, и дело, и документ, — корреляционный оракул, отменяющий всё разделение хранения.

AD-26

Комната внутреннего чата ни к чему не привязана. В её схеме нет колонок о клиенте, заявке, деле, задаче или документе: чат менеджера с декларантом существует, но вне комнаты они друг друга не видят.

× Отвергнута привязка комнаты к объекту: общий контекст стал бы мостом между аудиториями.

AD-18

Содержимое документа — контент-адресуемый объект в S3, не поле в базе. Ссылки со сроком жизни к хранилищу не выдаются никогда: байты идут потоком через сервис, и право проверяется на каждый запрос, а не однажды при выдаче ссылки.

Каналы и доставка сообщений

AD-10

Канальный контур написан с нуля. Telegram, электронная почта и WhatsApp подключаются адаптерами по единому контракту: модель сообщения, политика вложений и матрица возможностей канала.

ADR-18

Тела внешних сообщений — третий класс хранилища. Отдельный инстанс с режимом защиты как у vault: входящее сообщение клиента считается персональными данными всегда.

× Отвергнуто хранение переписки в vault: сервис личности стал бы основным хранилищем переписки компании.

ADR-19

Привязка диалога к заявке — только однозначная. При сомнении система отказывается связывать и просит человека, а не угадывает. Возможности, которых у канала нет, не показываются кнопкой, которая всё равно не сработает.

AD-29

На исчерпание лимита ожидающих запросов отвечает 503, а не пустая страница. Пустой ответ клиент воспринял бы как «время вышло» и повторил бы запрос немедленно — под нагрузкой это превратило бы каждого ждущего в плотный цикл ровно тогда, когда нагрузку надо снижать.

Работа, документы, процесс

AD-17

Расписание Ганта считает сервер. Перетаскивание задачи — предварительный расчёт без записи, применение — с проверкой ревизии плана; календарной арифметики в браузере нет, поэтому два человека не разъедутся в разные версии графика.

AD-9

Экспорт DOCX и PDF — на Rust, внутри сервиса документов. Без внешних конвертеров и офисных пакетов на сервере; шрифты — только со свободной лицензией.

AD-11

Контракты API после первой вехи меняются только аддитивно. Шестнадцать спецификаций в репозитории: обновление инсталляции не ломает интеграции заказчика.

§4.4

В приёмку каждого релиза входит состязательная проверка «вытащи персональные данные» — поиск, автодополнение, ленты, уведомления, экспорт, каждый эндпоинт. Зелёные тесты и одобренное ревью доказательством не считаются.

AD-12

Поставка — изолированная инсталляция. Собственный контур заказчика, свои ключи и бэкапы. Режим с несколькими арендаторами, если появится, будет инстансом на арендатора — признак арендатора в модель данных не закладывается.

Состояние

Продукт в активной разработке. Ниже — фактические цифры, а не планы.

Где проект сейчас

Разработка идёт волнами: каждая заканчивается приёмкой в собранном стенде — сценарии проходят кликами под всеми ролями, а не отметкой в задаче.

11сервисов на стенде плюс коннекторы каналов
≈91 000строк Rust в рабочей области
16замороженных контрактов API
20ADR с отвергнутыми альтернативами
1 · Фундамент 2 · Срез 3 · Платформа 4 · Домен CRM 5 · Задачи, Гант, риски 6 · Документы и экспорт 7 · Диалоги и коннекторы 8 · Стабилизация 9 · Роли и права 10 · Вход и сессии 11 · Канальный контур — в работе 12 · Продуктизация

Волны 1–10 приняты с отчётами о приёмке в репозитории. Текущая работа — внешние диалоги с клиентами: очередь доставки, честные статусы отправки, псевдонимизация входящего контакта. Дальше — установка, обновление, мониторинг и документация к Релизу 1.

Ядро CRM

Клиент и контрагент с управляемым доступом к ПД, заявка со статусами и SLA, операционное дело декларанта, история полей, комментарии.

Управление работой

Проекты, доски с лимитом задач в колонке, зависимости и вехи, Гант с критическим путём, учёт времени, риски матрицей 5×5, кокпит.

Документы

Блочный редактор, версии, вложения, поиск, экспорт в DOCX, PDF, HTML и Markdown.

Платформа

Журнал аудита, уведомления, конструктор справочников и ролей, админка, метрики и проверки готовности у каждого сервиса.