Почему ваш канал связи — не ваш. История о том, как паранойя заставила меня написать свой мессенджер с нуля.

Вернул UIN-ы из нулевых и завернул всё в PWA: как я писал свой мессенджер, балансируя между ностальгией и Highload

Обложка: Почему ваш канал связи — не ваш. История о том, как паранойя заставила меня написать свой мессенджер с нуля.

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

Мне эта модель быстро наскучила.

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

Сразу зафиксируем рамки, чтобы не плодить фантомные ожидания:

- это не убийца Telegram;

- это не презентация для инвестора с KPI и growth loops;

- это не проповедь о том, как всем теперь жить.

Это мой личный цифровой бункер, моя песочница, мой учебный полигон и мой инженерный аттракцион. Я строю это прежде всего для себя: как резервный канал связи, как способ не зависеть от чужих решений и как честный highload-эксперимент, в котором можно не рассуждать про real-time в теории, а ловить его за горло руками.

И вот здесь началось смешное.

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

Что это вообще такое

На текущем этапе это PWA-мессенджер.

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

«настоящие пацаны пишут только натив». Можете не разогреваться, я это всё уже сам себе рассказал.

Почему все равно PWA:

- мне нужен был короткий путь от идеи до живого клиента;

- мне нужен был быстрый цикл выката без ритуальных танцев с

магазинами приложений;

- мне нужен был клиент, который можно быстро ломать, чинить,

пересобирать и снова кидать в бой;

- мне нужно было что заработает на разных платформах;

- мне нравится, что PWA живет в браузерной песочнице, а не лезет в телефон как хозяин квартиры.

У PWA есть реальные потолки:

- нет такой свободы по платформенным API, как у нативного клиента;

- фон, пуши и некоторые сценарии мобильной жизни работают хуже, чем

хотелось бы;

- нет нормальных VoIP-пушей (звонки работают только когда приложение

открыто), нет нормальной интеграции со списком вызовов телефона;

- по голой производительности натив его обгоняет.

И, да, Service Workers иногда ведут себя как подростки в пубертате, а iOS местами режет фоновую жизнь PWA так, будто лично обиделась на идею веб-клиента. Я в курсе. Выживаем с тем, что есть.

Но для моей задачи PWA дал главное: скорость эволюции. А в экспериментальном проекте это иногда важнее, чем стерильная идеология.

И да, работает эта штука подозрительно хорошо. Лучше, чем я ожидал, если честно.

Зачем вообще писать еще один мессенджер, когда мир и так ими забит

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

Потому что я люблю контролировать инфраструктуру, а не молиться на чужую.

Потому что читать статьи про очереди, ретраи, доставку, порядок сообщений и борьбу с race condition — это теория. А написать свое, увидеть, где оно течет, и потом руками это затыкать — уже ремесло.

Потому что мне скучно.

Да, это тоже важная часть правды. На обычной работе мозг периодически начинает зевать от предсказуемости. А когда ты в одиночку пытаешься собрать живой real-time-сервис, где есть сессии, доставка сообщений, поиск, синхронизация состояния, очереди и weird cases мобильной сети —

то зевота быстро заканчивается.

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

Где начинаются не разговоры про highload, а настоящая инженерная жизнь

Пока не собираешь такое сам, кажется, что мессенджер — это просто чатик.

Ну текстик, ну websocket, ну кнопка «отправить», господи. А потом начинается взрослая часть спектакля.

1. Сообщение должно не просто уйти, а дойти правильно

Самая скучная и самая дорогая ошибка — считать, что «отправил» значит

«доставил».

Нет. В реальной жизни между клиентом, сетью, сервером и хранилищем полно мест, где всё может стать интересно:

- клиент послал пакет, но сеть моргнула;

- сервер принял, но клиент не получил подтверждение;

- клиент решил, что ничего не дошло, и отправил повторно;

- внезапно у тебя уже не просто доставка, а идемпотентность,

дедупликация, подтверждения, ретраи и очень неприятные разборки с

дублями.

И это только один кусок.

2. Порядок сообщений — штука гораздо более капризная, чем кажется

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

Инженерная реальность куда веселее:

- локальный optimistic update на offline-first клиенте хочет быть

быстрым;

- серверная истина хочет быть правильной;

- база хочет жить в своей временной линии;

- несколько устройств одного пользователя могут в это время смотреть на

мир разными глазами.

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

3. Race condition — это когда баг уже произошел, но ты еще не знаешь, где именно тебя унизили

Race conditions — мой любимый жанр инженерного фольклора. Это когда всё прекрасно, пока ты смотришь. И ломается ровно в тот момент, когда отвлекся налить кофе.

Условно:

- один поток думает, что пользователь в онлайне;

- второй уже считает, что он отвалился;

- третий в это время честно пишет новое состояние;

- четвертый с невинным лицом отдает клиенту вчерашнюю правду.

А потом ты сидишь и объясняешь себе, почему в 99.7% случаев всё идеально, а в оставшихся 0.3% система внезапно начинает разговаривать голосами. А когда еще и пытаешься заставить работать реалтайм систему еще в кластере, то все сложности возводятся в квадрат.

4. Любая «маленькая фича» очень быстро приходит за твоей архитектурой с ножом

Захотел новую механику? Например, необычную резервацию UIN еще до регистрации? На бумаге выглядит забавно. На бэкенде начинается вечеринка:

- появляется состояние для еще не существующего пользователя;

- нужно резервировать ресурс так, чтобы его не вымели любопытные и жадные боты;

- нужно думать о TTL, освобождении, гонках, повторных запросах и кривом клиентском поведении;

- нужно следить, чтобы веселая фича не превратилась в атаку на собственную логику.

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

5. Поиск и история сообщений сначала кажутся простыми. Потом ты взрослеешь

Пока данных мало, Postgres прощает тебе оптимизм. Потом история растет, индексы начинают намекать, что дружба дружбой, а latency по расписанию, и любой неаккуратный запрос внезапно становится личным конфликтом с CPU.

И тут выясняется, что в реальном мессенджере мало просто «хранить сообщения». Нужно еще:

- быстро искать;

- не ломать горячий путь доставки;

- не превращать сервер в печку под нагрузкой;

- думать наперед, где потом начнется горизонтальное масштабирование, а

где сначала будет боль, потом переписывание, потом снова боль.

Чтобы это не выглядело как очередная литература про «сложности highload», вот живой пример. Это кусок поиска по групповым сообщениям в моем бекенде: с проверкой доступа, выборкой только текстовых сообщений и сортировкой по релевантности.

			```sql

SELECT

  m.id,

  m.chat_id,

  m.sender_id,

  m.content->>'text' AS text,

  m.inserted_at,

  c.title

FROM group_messages m

JOIN chats c

  ON c.id = m.chat_id

 AND c.shard_id = m.shard_id

WHERE EXISTS (

  SELECT 1

  FROM chat_members cm

  WHERE cm.chat_id = m.chat_id

    AND cm.shard_id = m.shard_id

    AND cm.user_id = :user_id

)

  AND m.content->>'text' IS NOT NULL

  AND m.content->>'text' ILIKE :like_query

ORDER BY similarity(m.content->>'text', :query) DESC,

         m.inserted_at DESC

LIMIT :limit;

```
		

Под этот запрос у меня лежит не молитва, а вполне конкретная опора: partial GIN index по выражению`(content->>'text') gin_trgm_ops`.Иначе такая красота очень быстро превращает сервер в отопление стойки за счет тупого перебора JSONB.

А вот так тот же самый функционал очень часто пишут новички — вроде работает, пока данных смешно мало:

			```sql

SELECT m.id, m.chat_id, m.sender_id, m.content, m.inserted_at

FROM group_messages m

WHERE m.chat_id IN (

  SELECT cm.chat_id

  FROM chat_members cm

  WHERE cm.user_id = :user_id

)

  AND m.content::text ILIKE '%' || :query || '%'

ORDER BY m.inserted_at DESC

LIMIT :limit;

```
		

Проблема тут не в эстетике. Проблема в том, что верхний вариант ищет по нужному полю, умеет нормально ранжировать результат и опирается на индекс. Нижний часто уезжает в тяжелый scan по JSONB, ищет по строковому представлению всего объекта и под нагрузкой начинает жечь CPU просто потому, что автору было лень договориться с базой по-хорошему.

На реальном объеме истории в проде разница здесь может быть уже не в процентах, а на порядок и выше. То есть это очень быстро превращается не в «чуть-чуть быстрее», а в 10x+, а дальше всё зависит от размера истории, доли текстовых сообщений, партиций и того, насколько сильно вы любите мучить PostgreSQL.

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

6. Очереди, ретраи и backpressure — это не украшения, а повод спать чуть спокойнее

В сетевом сервисе нельзя жить по принципу «ну отправили и ладно». Когда клиентов много, а сеть у части из них вечно в состоянии «между EDGE и молитвой», тебе нужны механизмы, которые умеют не только гнать трафик, но и не убивать систему собственным рвением.

То есть нужны:

- очереди (Kafka, или что вы там предпочитаете);

- повторные попытки (retry-логика);

- backpressure;

- вменяемая реакция на временную деградацию (graceful degradation);

- circuit breaker чтобы какой-нибудь маленький и вроде бы некритичный сервис не мог каскадно положить бы всю инфраструктуру;

- архитектура, которая умеет признавать, что мир не идеален.

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

Интерфейс я подсматривал у Telegram. И да, специально

Я не стал устраивать дизайнерский карнавал ради уникальности. У пользователя уже есть мышечная память. Она дороже моего самолюбия.

Если человек открывает новый мессенджер, он хочет быстро разобраться и написать сообщение. Ему не нужен артхаус-квест «найди кнопку отправки, автор так видит».

Поэтому UI здесь знакомый. Не потому что я беден фантазией, а потому что я делаю инструмент связи, а не выставку интерфейсного концептуализма.

Моя любимая инженерная шалость: UIN-рулетка до регистрации

Тут я, конечно, немного похулиганил.

Я сделал резервацию UIN еще до этапа регистрации. То есть можно зайти, поймать красивый номер, а уже потом решить, нужен тебе вообще этот зоопарк или нет.

С человеческой стороны это фан, ностальгия и немного ICQ-вайба.

С серверной стороны это пачка вопросов:

- как хранить состояние для «почти-пользователя»;

- как не раздать красивые номера слишком щедро;

- как не дать мимо проходящему ботнету выгрести пул;

- как освобождать резервы и не плодить мусор;

- как не застрелить логическую целостность ради красивой игрушки.

С практической точки зрения миру, возможно, и не нужна эта фича. С инженерной — она просто слишком вкусная, чтобы я прошел мимо.

Про безопасность — без дешевой магии и без сказок для инвестора

Нет, я не изобретал свою криптографию. Я вообще считаю, что желание «сейчас быстренько придумаю свой защищенный протокол» должно автоматически активировать тревожную сирену. Да, создать защищенный протокол возможно, но это сложнее и дольше чем кажется из-за необходимости учета множества edge-cases; и в реальности сделанные на коленке протоколы чаще ненадежны, и, что еще хуже, об их уязвимостях могут знать лишь немногие пронырливые и порой не самые добросовестные люди.

Поэтому мой подход скучный, взрослый и не очень годится для красивых презентаций:

- транспорт закрыт современным TLS 1.3 (который на текущий момент, март 2026, взломать прямым криптографическим методом практически невозможно);

- клиент живет как PWA в браузерной песочнице, какая уже сама по себе имеет ряд уровней защиты;

- поверх этого я стараюсь не творить откровенной дичи;

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

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

Если вам нужна зрелая крепость для деликатной переписки — берите зрелые решения, которым вы уже доверяете.

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

Почему я вообще тащу это именно на TProger

Потому что TProger — это место, где тексту мало быть наглым. Здесь за дерзость прощают только одно: если под ней есть мясо.

А у меня как раз тот случай, когда мне интереснее не «собрать лайки», а получить нормальную техническую реакцию:

- где архитектура выглядит спорно;

- где логика доставки сообщений может начать чудить;

- где PWA упрется в реальные ограничения платформы;

- где резервация UIN создает лишнюю поверхность атаки;

- где под нагрузкой начнет хрустеть то, что в одиночных тестах ведет себя прилично;

- где в протоколе, порядке событий, ретраях или кешировании я недооценил крайний случай.

Мне не нужен хор в духе «вау, круто».

Мне нужен ваш внутренний вредный сеньор. Тот самый тип, который открывает статью, хмыкает, а потом начинает мысленно разбирать чужую систему на части.

Вот ему я и пишу.

Что я от вас хочу

По сути — честной драки, но умной.

- Хотите проверить, как это переживает плохую сеть — отлично.

- Хотите посмотреть, как выглядит поведение в edge-cases — вообще прекрасно.

- Хотите понять, насколько легко читается логика протокола — давайте.

- Хотите найти баг в порядке сообщений, в кеше, в подтверждениях, в резервации UIN, в клиентском поведении — тем более.

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

Мне это полезнее, чем аплодисменты.

И да: я не заявляю, что построил новый и единственно верный мессенджер.

Я заявляю другое. Я собрал свой рабочий велосипед, уже довольно злой и живой, и мне интересно, где именно TProger попробует вставить ему отвертку в спицы, и на какой секунде этот велосипед упадет.

Что можно делать прямо сейчас

- Если вы параноик или устали от мейнстрима и навязанных решений — держите это как запасной канал связи.

- Если вы соскучились по временам красивых UIN — приходите ловить UIN-номер.

- Если вы любите sniff, разбор трафика и сетевые игры — смотрите что идет по проводу.

- Если у вас профессиональная привычка первым делом искать edge-cases — вот, собственно, ради вас всё это и принесено.

И если что-то положите, вскроете или красиво разберете — пришлите детали. Я не обидчивый. Я, строго говоря, именно ради этого и открыл двери.

Что дальше

Планы без корпоративной шелухи, но вполне понятные:

- дальше допиливаю PWA-версию;

- нативные Android и iOS-клиенты — в планах, но пока не в приоритете;

- клиентскую часть, вероятно, позже открою, когда там станет меньше творческого барокко;

- идея с SDK / библиотекой для кастомных клиентов мне нравится отдельно: если люди захотят писать свои оболочки, это будет уже совсем красивый уровень игры.

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

Итог

На сегодня это экспериментальный PWA-мессенджер, который родился из упрямства, паранойи, скуки и любви к инженерным задачам, от которых нормальные люди обычно стараются держаться подальше.

Я делал его в первую очередь для себя. Но он уже дорос до стадии, когда его интересно не только строить, но и показывать наружу.

Если будете пробовать — лучше сразу добавить его на домашний экран (PWA так устанавливается). Так приложению жить будет заметно удобнее: платформа позволит заработать пуш-уведомлениям, появится нормальное кеширование, а оффлайн-режим перестанет быть декоративной надписью.

Если хотите просто еще один канал связи — пожалуйста.

Если хотите посмотреть и проверить, насколько крепок мой цифровой эксперимент, — тем более пожалуйста.

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

Добро пожаловать.

Посмотрим, кто кого утомит первым: вы — мой сервер, я — ваши попытки его удивить, или реальность — нас обоих.

PS:

Меня там можно найти по нику IGRYM или по UIN 10001. Также есть внутренняя группа для первых пользователей (Early Birds, доступна на экране списка чатов через «+» вверху экрана)

Ссылки:

- Приложение: https://beta.plumb-app.ru/

- Телеграм-канал с новостями проекта: https://t.me/plumb_channel

- Телеграм-группа обсуждения: https://t.me/plumb_group

Рекомендуем