EcoMap — civic-tech продукт для Узбекистана: житель ставит на карте метку, прикладывает фото экологического нарушения и следит за ним до решения. Вход через Telegram, Telegram-бот, кнопка «Помочь решить», админка, три языка (ru/uz/en). Я сделал его в одиночку.
И сделал дважды.
Первая версия вышла на Next.js + Supabase примерно за четыре месяца. Вторая — полная переписка на собственном монорепо Bun/Elysia/Drizzle. Ниже — почему я выбросил работающий продукт, во что реально обошлась переписка, и одна задача, которая съела больше времени, чем всё остальное вместе взятое.
v1: Supabase быстро довёл до продукта
Первый заход — 51 коммит, с августа по декабрь — опирался на Supabase во всём, что нужно бэкенду в первый день: Postgres, авторизация, хранилище, row-level security, SDK и на сервере (SSR), и на клиенте. Сверху: TanStack Query, редактор Tiptap для тел отчётов, фоновая очередь с Telegram-ботом на grammY и прогон через OpenAI для категоризации входящих отчётов.
Вот честный аргумент за BaaS. У меня был реальный, мультиязычный, карто-центричный продукт с авторизацией и ботом за недели, а не за кварталы. Если ты проверяешь идею — это правильный трейд-офф.
И он же тихо копил причины, по которым я уйду.
Почему я ушёл с BaaS
Ни одна из них не про «Supabase плохой». Это форма сделки, которую ты заключаешь, когда твой бэкенд живёт внутри чужой платформы.
- Lock-in там, где это важнее всего. Авторизация, правила хранилища и слой данных были выражены в идиомах Supabase. Чем больше рос продукт, тем больше моей логики завязывалось на конкретную платформу.
- Швы вылезают по краям. Тяжёлым был не CRUD — а cross-cutting concerns: cross-origin сессии, фоновая работа, нативная обработка изображений. Именно там BaaS даёт меньше всего пространства.
- Мне нужен был единый type-safe хребет. Клиент и сервер, расходящиеся и залатанные сгенерированными типами и хуками запросов, — это ок, пока не перестаёт быть ок.
- Замена уже была. Я поддерживаю собственный boilerplate-монорепо на Bun/Elysia/Next.js (ShipKit), на котором работают несколько моих продуктов. Переписка была не риском с нуля — а переездом на стек, который я контролирую и переиспользую.
Так v2 стал осознанным уходом с платформы, а не переписыванием ради переписывания.
v2: стек, который я реально контролирую
Вторая версия — 94 коммита, февраль–март — это монорепо на Turborepo: apps/web, apps/admin, apps/api, apps/jobs плюс около десяти общих пакетов. Составные части:
- API: Elysia.js на Bun, типизирован сквозь клиент Next.js через Eden Treaty — без codegen, без расхождений.
- Данные: Drizzle ORM поверх чистого PostgreSQL.
- Авторизация: Better Auth с кешем сессий в Redis.
- Бот: grammY (+conversations) для Telegram.
- Медиа: sharp для обработки фото нарушений.
- Фронтенд: Next.js 16, React 19, Tailwind v4, shadcn/ui, next-intl для ru/uz/en.
Тот же продукт — на моих примитивах. Вот где стало интересно.
Что болело: Telegram-авторизация на вебе
Вход через Telegram тривиален внутри Telegram. В открытом вебе — нет, и именно здесь я потратил больше всего итераций с большим отрывом.
Бот аутентифицирует пользователя и отдаёт результат обратно на страницу через postMessage — payload { event: 'auth_result', result: {...} }. Страница должна превратить это в настоящую персистентную сессию. Каждая сложная проблема жила в этой передаче:
- Cross-origin cookies. Сессионную cookie ставит API на origin, отличном от страницы. Чтобы она реально «прилипла», понадобились
credentials: 'include'на клиенте, правильные атрибутыSameSite/secure у cookie иsetSessionCookieот Better Auth на сервере — и заставить все три совпасть, прежде чем хоть что-то заработает. USE_SECURE_COOKIES, а неNODE_ENV. Завязывать поведение secure-cookie наNODE_ENV— классическая ловушка: локальный HTTPS, staging и прод не раскладываются чисто на «development vs production». Я вынес это в явный флаг, чтобы безопасность cookie была осознанным конфигом, а не побочным эффектом режима сборки.- Дубли пользователей. Одна Telegram-личность может прийти не раз. Аккаунты, созданные ботом, я ключую по синтетическому валидируемому email (
telegram_{id}@ecomap.uz), чтобы вернувшийся пользователь резолвился в один профиль, а не плодил дубликаты, и синхронизирую свежие Telegram-данные вprofilesна/me.
Ничего из этого не гламурно. Но это ровно то, что модуль авторизации BaaS прячет — до того дня, когда тебе нужно, чтобы он вёл себя иначе, и тогда ты дерёшься с абстракцией, а не с задачей.
Тихие победы
Три вещи, которые были болезненны на платформе и чисты на собственном стеке:
- Единый type-safe хребет. Eden Treaty экспортирует Elysia-API прямо в клиент. У форм запроса и ответа один источник правды; рефактор на сервере всплывает как ошибка типов в приложении, а не как рантайм-сюрприз в четырёх приложениях монорепо.
- Нативная обработка изображений на моих условиях. sharp — нативная зависимость, её надо аккуратно собирать и деплоить: API я собираю с
--external sharpи запускаю под Bun. Владение рантаймом превратило это в строку конфига, а не в ограничение платформы. - Фоновая работа вне HTTP-пути. Деривативы фото, уведомления и AI-категоризация — место в очереди, а не в запросе. Отдельный сервис
apps/jobsплюс общий пакет джоб обрабатывают их под systemd, и веб-слой остаётся отзывчивым.
Как реально шёл рефактор
Я не набивал 94 коммита миграции руками. Переписка была plan-driven: docs/migration-plan/TASKS.md с чеклистом и таблицей прогресса, исполняемый через Claude Code — отметил задачу, обновил таблицу, дальше. Это тот же структурированный мультиагентный воркфлоу, который я гоняю по своим проектам, и во многом именно поэтому фулстек-переписка уложилась в несколько недель вечеров. (Об этом паттерне я писал отдельно.)
Если ты на BaaS и решаешь, мигрировать ли
Я не собираюсь советовать выдирать Supabase. Трейд-офф v1 был верным — он вывел реальный продукт к реальным пользователям быстро. Мигрируй тогда, и только тогда, когда платформа начинает стоить тебе на краях:
- Ты дерёшься с абстракциями авторизации/хранилища больше, чем пользуешься ими.
- Твоим cross-cutting concerns — сессиям, фоновым джобам, нативным зависимостям — нужен контроль, которого платформа не даёт.
- У тебя уже есть (или ты можешь обосновать) стек, который переиспользуешь, — тогда переписка накапливается, а не остаётся разовой.
Для EcoMap все три были правдой, и миграция окупилась контролируемой type-safe кодовой базой, которую я расширяю, не спрашивая разрешения.
EcoMap живёт на ecomap.uz. Если планируешь разработку или миграцию с BaaS — это как раз та работа, которую я беру.