Если у вас один продукт живёт сразу в двух местах, в Telegram Mini App и в обычном вебе, то готовьтесь к тому, что каждая новая фича будет норовить появиться в одном фронте и тихо не появиться в другом. И это не разовый косяк, который один раз поймал и забыл, а постоянная фоновая боль, которая копится сама собой, пока вы заняты чем-то другим. У меня в Картаре ровно так: два обязательных клиента, и вся прошедшая неделя в итоге свелась к одному жёсткому правилу, которое я записал прямо в память агенту, чтобы больше об этом не думать. Всегда делается всё и туда, и туда, без исключений. Плюс отдельная грабля, на которой я чуть не сошёл с ума: фронт нельзя собирать на сервере, потому что окружение там пустое, и юзера просто выкидывает как разлогиненного, хотя по факту с ним всё в порядке.
Дальше расскажу, как это выглядело по дням, что именно разъезжалось и сколько это стоит по времени и нервам.
Откуда вообще два фронта
Картара — это Telegram Mini App с AI-персонажем, и логично, что основной вход у людей идёт через Telegram. Но у продукта есть и веб-версия, отдельный сайт, куда человек заходит с браузера, и это не прихоть, а нормальная история для такого формата, потому что кому-то удобнее в телеге, а кому-то с компа. Технически это два разных приложения: Mini App собран на Vite с React, обычная SPA, а веб сделан на Next.js, со своими роутами и своими адресами страниц, причём и кириллическими, и латинскими. То есть это не «один код, две сборки», а две кодовые базы, которые должны выглядеть и вести себя одинаково для пользователя, но при этом написаны по-разному.
И вот тут зарыта собака. Когда вы пилите фичу, вы по инерции пилите её там, где сейчас смотрите. Открыл Mini App, добавил блок, проверил, обрадовался. А веб? А веб остался как был. И пользователь, который зашёл с браузера, видит старую версию и справедливо недоумевает, почему ему обещали одно, а показывают другое.
Как это вскрылось на нумерологии
Стартовый случай недели был именно такой. Мы выкатили новую нумерологию, я проверяю в Mini App, всё на месте, красиво, работает. Захожу в веб, а там пусто, ничего этого нет. И у меня буквально вырвалось: «НеТ погоди блять. Почему в ТМА есть а вебе этого нет?» Вот эта фраза и есть вся суть проблемы в одном предложении. Я уже про эту нумерологию писал отдельно, как она прошла путь от «нумерология — нахуя?» до прода (неделя в Cartara), но тогда я не учёл, что «на проде» для меня значило «на проде в одном из двух фронтов», а это, как ни крути, совсем не одно и то же.
Дальше пошло по нарастающей, потому что когда начинаешь сверять два клиента вдумчиво, вылезает не одно расхождение, а целая пачка. Разъехались тултипы, те самые знаки вопроса «?», которые подсказывают, что значит блок. Разъехался гороскоп. Поехал паритет астро-блоков, то есть в одном фронте блоков было больше, чем в другом. И самое коварное, поехала навигация: в вебе гороскоп лежал внутри раздела астрологии, а в Mini App он был сам по себе, отдельно. То есть человек, который пользовался обоими клиентами, попадал буквально в разные приложения с разной логикой, и хорошо если он этого не замечал. Про то, как два фронта расползаются даже на уровне отдельных компонентов, я разбирал отдельно — там та же болячка, только крупным планом.
Я тогда и продиктовал агенту мысль, которую стоило бы понять с самого начала: «паритет надо и в други доменах поддоменах смотреть а то в вебе гороскоп внутри астро а в тма - нет». Потому что расхождение в навигации — это хуже, чем отсутствующий блок. Отсутствующий блок человек просто не увидит и ничего не заподозрит, а вот разную структуру меню он почувствует как «что-то тут не так», даже если не сможет сформулировать, что именно.
Правило, которое ушло в память
В итоге я зафиксировал это не как «надо бы помнить», а как прямую инструкцию агенту в долгую память: «Пиши в память о том что всегда делается все и туда и туда». Звучит банально, согласен. Но банальное правило, которое ты реально соблюдаешь, бьёт умную систему, которую ты вечно забываешь применять. Когда я работаю через Claude Code и оркестрирую агентов, очень легко скатиться в режим «делаю там, где смотрю»: агент послушно докручивает один фронт до блеска, а про второй никто не вспомнил, потому что в задаче он не был прописан явно. Память тут работает как страховка, теперь любая фича по умолчанию означает обе кодовые базы, а не ту, что у меня сейчас перед глазами.
И это, кстати, общая болячка вайбкодинга и разработки с ИИ, потому что агент делает ровно то, что ты сказал, и ни на миллиметр больше. Если ты сказал «добавь блок нумерологии», он добавит блок нумерологии. Туда, где ты сейчас. Он не знает, что у тебя есть второй фронт, который ты держишь в голове, а в промпте не упомянул. Поэтому вся ответственность за паритет лежит на человеке, который держит картину целиком. ИИ закрывает исполнение, но рамку задаёт оркестратор, и это, пожалуй, главный навык в работе с агентами вообще.
Грабля сборки: пустое окружение и фантомный разлогин
Отдельная история, которая стоила мне отдельного приступа. Фронт, что Mini App, что веб, нельзя собирать прямо на сервере. Почему? Потому что при сборке переменные окружения подтягиваются в бандл, и если на сервере окружение пустое или не то, то фронт собирается с пустыми адресами и идёт на same-origin, то есть ломится сам в себя вместо нужного бэка. А для пользователя это выглядит так, будто его разлогинило: он заходит, а приложение его не узнаёт. И ты сидишь, проверяешь авторизацию, токены, сессии, а проблема вообще не там, проблема в том, что фронт собрали не в том месте, и он физически не знает, куда стучаться.
Это классическая ловушка, на которую напарываются многие, кто делает Telegram Mini App или любой фронт с переменными окружения на этапе билда. Ты привык, что бэкенд читает окружение в рантайме, и логично ждёшь того же от фронта. А фронт-то читает окружение в момент сборки, и после этого значения зашиты в статику намертво. Собрал с пустым, получил пустое навсегда, до следующей пересборки. Лечится тем, что сборка делается там, где окружение правильное, а на сервер уезжает уже готовая статика, а не исходники, которые сервер потом сам соберёт со своим скудным окружением.
Деплой, когда меняется контракт
Из всей этой двухфронтовой истории вытекает ещё одно правило, уже про деплой. Когда вы меняете контракт между фронтом и бэком, например переименовали поле в ответе API или поменяли формат, выкатывать надо в строгом порядке и желательно в одно окно: сначала миграция базы, потом API, потом Mini App, потом веб. Если порядок нарушить, то получите ровно то, чего боялись с самого начала, оба фронта падают, потому что один уже ждёт новый формат, а второй ещё отдаёт старый, или наоборот. У меня так уже было, когда бэк и фронт разошлись по одному числу в контракте и зря блокировали оплату, так что это не теоретическая страшилка.
И это снова про ту же самую дисциплину. Два фронта — это не «в два раза больше работы», это «в два раза больше мест, где можно проебать синхронизацию». Каждая точка касания умножается на два: миграция одна, бэк один, а вот клиентов, которые этот бэк потребляют, уже два, и оба надо обновить согласованно, иначе кто-то из них обязательно отвалится в самый неудачный момент.
Что я из этого вынес
Если коротко, то держать паритет двух фронтендов — это не разовая задача, которую можно закрыть и забыть, а постоянная привычка, которую надо встроить в сам процесс. Не «не забыть про веб», а «фича по определению не готова, пока не сделана в обоих местах». Я для себя свёл это к трём вещам, которые теперь работают по умолчанию. Первое — любая фича означает оба фронта сразу, и это записано в память агента, чтобы я не полагался на свою. Второе — паритет проверяется не только по наличию блоков, но и по навигации, потому что разная структура меню рушит ощущение единого продукта сильнее, чем недостающий блок. Третье — фронт собирается там, где правильное окружение, а не на сервере, иначе людей будет фантомно разлогинивать, и вы убьёте полдня в поисках бага, которого нет.
Стоило мне это, честно, целой недели возни и пары приступов раздражения, когда одно и то же расхождение всплывало в третьем месте подряд. Зато теперь у меня есть жёсткое правило вместо благих намерений, а правило, в отличие от намерения, не забывается посреди ночной сессии. Если вы делаете MVP с двумя клиентами на один бэкенд, не повторяйте мой путь через грабли, просто заложите паритет в процесс с первого дня. Дешевле выйдет.