# Как устроена

Страница для того, кто будет это ставить, обновлять и чинить. Она объясняет,
из каких частей собрана платформа и почему части именно такие — потому что от
этого зависит, что вы увидите на сервере и что придётся делать при обновлении.

## Три процесса, а не двадцать

Приложение одно, внутри разделено на модули с явными границами. Это **модульный
монолит**, а не микросервисы, и выбор сознательный: микросервисы решают проблему
«много команд мешают друг другу», а добавляют сетевые вызовы, распределённые
транзакции и пять способов выкатить неправильно.

На сервере после установки работает:

| Процесс | Что делает |
|---|---|
| **app** | Веб-интерфейс — админка, кабинет ученика, публичные страницы — и HTTP-API. Только быстрые операции |
| **worker** | Очереди: рассылки, шаги сценариев, исходящие вебхуки, отложенные операции, генерация файлов |
| **bot** | Telegram-бот. Отдельный процесс, потому что держит долгоживущее соединение |

Плюс **PostgreSQL** и **Redis**, плюс обратный прокси, который занимается TLS.

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

Воркер масштабируется отдельно: если очередь не успевает, запускается ещё один.

## Модули не лезут в таблицы друг друга

Восемь модулей: обучение, коммерция, CRM, сообщения, сценарии, вебинары, сайт,
аналитика. Плюс ядро (вход, права, пользователи, настройки, события) и интеграции.

Между собой они общаются **событиями**, а не прямыми вызовами. Оплата прошла —
модуль коммерции публикует `payment.succeeded` и на этом заканчивает. Дальше:

```
payment.succeeded
   ├─ commerce     закрывает заказ, создаёт покупки
   ├─ learning     открывает доступ к курсу
   ├─ messaging    письмо «доступ открыт» и сообщение в Telegram
   ├─ automation   запускает сценарии с триггером «оплата»
   ├─ crm          пересчитывает сегменты
   ├─ integrations шлёт исходящий вебхук вам
   └─ analytics    фиксирует событие для отчётов
```

Модуль коммерции ничего не знает ни про Telegram, ни про сценарии. Для вас это
значит одну практическую вещь: **новая интеграция подписывается на событие
и не требует правок в продукте**.

## Событие пишется в базу, а не в очередь

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

Поэтому событие записывается **в ту же транзакцию**, что и бизнес-операция, в таблицу
`Event`, а отдельный воркер публикует его в очередь. Либо коммитится всё, либо ничего.

Из этого следуют две вещи, которые видны снаружи:

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

**Порядок доставки не гарантируется.** Даже по одному объекту. Это честное
ограничение любой такой системы, и лучше сказать о нём прямо. Что из этого следует
получателю вебхука:

- **обработчик обязан быть идемпотентным** по идентификатору события: одно и то же
  событие может прийти дважды;
- **не полагайтесь на «предыдущее событие уже обработано»** — читайте текущее
  состояние объекта, а не достраивайте его из истории;
- упорядочить события можно самому: у каждого есть время создания и монотонный номер.

## Внешние сервисы — только через адаптеры

Платежи, фискализация, сообщения, видео, хранилище файлов подключаются адаптерами.
Ни один модуль не вызывает ЮKassa или Telegram напрямую.

Что это даёт школе: **замена провайдера не переписывает продукт**. Сменили платёжного
провайдера — поменялся один адаптер, а заказы, доступы, сценарии и отчёты остались
как были.

Оно же объясняет неочевидное: у разных провайдеров разная модель доверия. У ЮKassa
подписи уведомлений нет вообще — проверка по адресам плюс обязательный повторный
запрос статуса; у Продамуса — подпись HMAC. Поэтому итоговое состояние платежа
всегда берётся **повторным запросом к провайдеру**, а не из тела уведомления: тело
можно подделать, и оно может устареть.

## Что нужно от сервера

| | Минимум | Комфортно |
|---|---|---|
| Ядра | 2 | 4 |
| Память | 4 ГБ | 8 ГБ |
| Свободный диск | 20 ГБ | 40 ГБ |
| ОС | Ubuntu 22.04 / 24.04, Debian 12 | — |
| Docker | 24+ с плагином compose v2 | 29+ |

**Память здесь — про сборку, а не про работу.** Работающая установка укладывается
примерно в 1,5 ГБ. Но сборка образа съедает 2–3 ГБ разом: если на сервере уже
что-то живёт, сборка на нём — самый быстрый способ уронить соседей. Собирайте образ
на другой машине и переносите готовым либо добавьте своп на время сборки.

**Диск.** Сами образы около 2,3 ГБ, плюс данные, файлы школы и резервные копии.

Подробности и предустановочный чек-лист — на странице [Установка](/start/install/).

## Часовые пояса и локали

Одна вещь, о которой стоит знать заранее, потому что она касается вашего сервера.

Платформа **не берёт из среды ничего**: ни часовой пояс, ни локаль, ни язык системы.
Сервер работает в UTC, а всё, что видит человек, показывается в поясе и локали
школы. Причина простая: коробка стоит на машине, про которую нам никто ничего
не обещал, а «через шесть секунд» не должно превращаться в «через три часа» от того,
что сервер стоит западнее.

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