← Все статьи
Архитектура сервиса пересылки уведомлений: 5 решений, которые стоит обсудить
24 мая 2026
В этой статье я разберу несколько архитектурных решений, принятых при разработке Эхофона. Не «как мы сделали», а почему именно так — с альтернативами, компромиссами и выводами.
1. Hash-роутинг вместо Next.js/React Router
Что выбрали: чистый HTML/JS с hash-роутингом (#home, #echocam, #tariffs). Никаких фреймворков.
Почему не React/Vue/Next.js:
- Проект стартовал как прототип. Первая версия была написана за вечер на чистом JS.
- SPA-фреймворки (React, Vue) решают проблему синхронизации состояния с DOM. У нас интерфейс простой: список сообщений, карточки тарифов, форма входа. Синхронизировать нечего — данные приходят с сервера и рендерятся.
- Next.js даёт SSR и роутинг на файловой системе. Но SSR нам не нужен: основной контент (сообщения) — сугубо клиентский, индексировать его не нужно. А для SEO-страниц блога мы сделали отдельные HTML-файлы.
Почему именно hash-роутинг:
- Не требует настройки сервера. Никаких
try_files с fallback на index.html. URL с # никогда не уходит на сервер — браузер просто меняет hash.
- Работает на любом хостинге, даже статическом. Если завтра переедем с Node.js на nginx-раздачу статики — роутинг не сломается.
Где проиграли:
- Google не индексирует hash-страницы как отдельные. Каноническая страница для поисковика — только
/. Это создало проблемы с SEO, которые мы решали отдельно (блог на статических HTML).
- Нельзя дать прямую ссылку «посмотреть тарифы» — только
#tariffs, что выглядит как якорь на главной.
Вывод: для сервиса, где основной функционал — личный кабинет пользователя (авторизованная зона), hash-роутинг — отличное решение. Для публичных SEO-страниц — статические HTML.
2. PostgreSQL вместо MongoDB
Что выбрали: реляционная БД с жёсткой схемой, внешними ключами и транзакциями.
Почему не MongoDB:
- Деньги. Платежи, баланс, транзакции — это ACID. Если баланс списывается, а подписка не создаётся — это катастрофа. MongoDB с транзакциями появилась только в 4.0 (2018), и до сих пор не все операции поддерживают multi-document transactions.
- Связи. У нас классическая реляционная модель: пользователь → подписки → сервисы, пользователь → устройства → сообщения. MongoDB с её вложенными документами была бы неудобна: либо денормализовать (дублировать данные), либо делать $lookup (аналог JOIN, медленный).
- Миграции. Реляционная схема заставляет думать о структуре данных заранее. С MongoDB легко попасть в ловушку «добавим поле в документ, потом ещё одно, потом переименуем старое» — и через месяц схема документа непредсказуема.
Где проиграли:
- Гибкость. Добавление нового поля требует
ALTER TABLE. В MongoDB достаточно начать писать новое поле в документы.
- Горизонтальное масштабирование. PostgreSQL умеет репликацию, но шардирование — это боль. В MongoDB шардирование из коробки. Но для наших масштабов это не проблема.
Вывод: если у вас есть деньги пользователей — берите реляционную БД. ACID-транзакции спасают от багов, которые невозможно отладить.
3. Polling вместо WebSocket
Что выбрали: клиент опрашивает сервер каждые 30 секунд (setInterval → GET /sms-forwarder/messages).
Почему не WebSocket:
- WebSocket требует постоянного соединения. Если у вас 100 пользователей — это 100 открытых сокетов. Node.js держит их в памяти. При росте до 1000 пользователей понадобится Redis Pub/Sub и горизонтальное масштабирование.
- WebSocket сложнее дебажить. Соединение может рваться из-за прокси, таймаутов, переключения сети. Нужен механизм переподключения, heartbeat, восстановления состояния.
- 30-секундный интервал — достаточен для уведомлений. SMS и push не требуют миллисекундной доставки. 30 секунд — разумный компромисс между нагрузкой и скоростью.
Оптимизация polling'а:
- Клиент не перерисовывает список, если сообщения не изменились (сравниваем ID).
- Сервер отдаёт только непрочитанные, если клиент передал
last_message_id.
- При отсутствии активности (пользователь не заходил > 1 дня) polling отключается.
Где проиграли:
- Задержка до 30 секунд. Если пользователь ждёт SMS с кодом подтверждения — это заметно.
- Лишние запросы. 100 пользователей × 2 запроса в минуту = 12 000 запросов в час. PostgreSQL справляется, но это нагрузка.
Вывод: для MVP polling — отличное решение. WebSocket стоит внедрять, когда задержка в 30 секунд станет критичной для пользователей.
4. API Key для Android вместо JWT
Что выбрали: для аутентификации Android-приложения используется статический API-ключ (32 случайных байта, hex). Для веб-клиента — JWT.
Почему не JWT для Android:
- JWT истекает (у нас — 7 дней). Телефон должен уметь обновлять токен. Это добавляет логику refresh token'а, обработку 401, повтор запроса.
- API-ключ живёт вечно. Пока устройство привязано к аккаунту, ключ валиден. Не нужно обновлять.
- API-ключ можно отозвать. Отвязал устройство — ключ удалён из БД.
Почему JWT для веба:
- Браузер — менее доверенная среда. Если JWT украдут через XSS, он истечёт через 7 дней. API-ключ пришлось бы отзывать вручную.
- JWT не требует запроса к БД при каждом запросе — проверяется подпись. API-ключ требует
SELECT * FROM devices WHERE api_key = $1.
Вывод: гибридная модель оправдана. Для доверенных устройств — долгоживущий ключ. Для браузера — короткоживущий токен.
5. Сквозное шифрование, а не TLS до сервера
Что выбрали: сообщения шифруются на телефоне ключом, производным от пароля пользователя. Сервер хранит зашифрованные данные и не может их прочитать.
Почему не просто HTTPS:
- HTTPS защищает канал, но не защищает данные на сервере. Если сервер взломан или БД утекла — злоумышленник читает всё.
- Сквозное шифрование означает, что даже разработчик не может прочитать сообщения пользователя. Это важно для доверия.
- Юридическая защита. Если сервер не хранит открытый текст, мы не можем его выдать по запросу.
Где проиграли:
- Поиск по сообщениям невозможен. Сервер не видит текст, поэтому поиск — только на клиенте, после расшифровки.
- Восстановление пароля = потеря всех сообщений. Если пользователь забыл пароль и сбросил его — старые сообщения расшифровать невозможно, потому что ключ изменился.
- Отладка сложнее. Мы не можем посмотреть в БД и увидеть текст сообщения — только Base64. Приходится верить, что шифрование работает.
Вывод: сквозное шифрование — это не «фича для галочки», а архитектурное решение, которое влияет на поиск, восстановление пароля и отладку. Для сервиса, который передаёт коды подтверждения и банковские SMS, это обязательный стандарт.
Итог
Каждое из этих решений — компромисс. Hash-роутинг прост, но плох для SEO. PostgreSQL надёжен, но требует дисциплины. Polling легко реализовать, но даёт задержку. API-ключи удобны для устройств, но требуют дополнительной защиты. Сквозное шифрование — стандарт безопасности, но жертвует удобством.
При создании своего сервиса не копируйте решения вслепую. Понимайте, почему выбрано именно так, и какие альтернативы были отвергнуты.
Попробуйте сервис в действии
Первые 7 дней бесплатно. Все архитектурные решения можно пощупать руками.
Попробовать →