EcoMap is a civic-tech product for Uzbekistan: residents drop a pin on a map, attach photos of an ecological violation, and track it to resolution. Telegram login, a Telegram bot, a "Help solve" flow, an admin panel, three languages (ru/uz/en). I built it solo.
I also built it twice.
The first version shipped on Next.js + Supabase in about four months. The second is a full rewrite on my own Bun/Elysia/Drizzle monorepo. This is why I threw away a working product, what the rewrite actually cost, and the one problem that ate more of my time than everything else combined.
v1: Supabase got me to a product, fast
The first cut — 51 commits, August to December — leaned on Supabase for everything a backend needs on day one: Postgres, auth, storage, row-level security, an SDK on both the server (SSR) and the client. On top of it: TanStack Query, a Tiptap editor for report bodies, a background queue with the grammY Telegram bot, and an OpenAI pass to categorise incoming reports.
That's the honest case for BaaS. I had a real, multilingual, map-driven product with authentication and a bot in weeks, not quarters. If you're validating an idea, this is the correct trade.
It also quietly accumulated the reasons I'd leave.
Why I left BaaS
None of these are "Supabase is bad." They're the shape of the trade you make when your backend lives inside someone else's platform.
- Lock-in on the parts that matter. Auth, storage rules, and the data layer were all expressed in Supabase's idioms. The more the product grew, the more of my logic assumed that specific platform.
- The seams show up at the edges. The hard bits were never the CRUD — they were the cross-cutting concerns: cross-origin sessions, background work, native image processing. Those are exactly the places a BaaS gives you the least room.
- I wanted one type-safe spine. Client and server drifting apart, patched with generated types and query hooks, is fine until it isn't.
- I already had the replacement. I maintain a Bun/Elysia/Next.js monorepo boilerplate (ShipKit) that several of my products run on. The rewrite wasn't greenfield risk — it was moving onto a stack I control and reuse.
So v2 became a deliberate move off the platform, not a rewrite for its own sake.
v2: the stack I actually control
The second version — 94 commits, February to March — is a Turborepo monorepo: apps/web, apps/admin, apps/api, apps/jobs, plus about ten shared packages. The pieces:
- API: Elysia.js on Bun, typed end-to-end into the Next.js client with Eden Treaty — no codegen, no drift.
- Data: Drizzle ORM over plain PostgreSQL.
- Auth: Better Auth with a Redis session cache.
- Bot: grammY (+conversations) for the Telegram side.
- Media: sharp for violation-photo processing.
- Frontend: Next.js 16, React 19, Tailwind v4, shadcn/ui, next-intl for ru/uz/en.
Same product, my primitives. Here's where it got interesting.
The part that hurt: Telegram auth on the web
Telegram login is trivial inside Telegram. On the open web it is not, and this is where I spent the most iterations by a wide margin.
The bot authenticates the user and posts the result back to the page with postMessage — an { event: 'auth_result', result: {...} } payload. The page has to turn that into a real, persistent session. Every hard problem lived in that handoff:
- Cross-origin cookies. The session cookie is set by the API on a different origin than the page. Getting it to actually stick meant
credentials: 'include'on the client, the rightSameSite/secure attributes on the cookie, and Better Auth'ssetSessionCookieon the server — and getting all three aligned before anything worked. USE_SECURE_COOKIES, notNODE_ENV. Gating secure-cookie behaviour onNODE_ENVis a classic trap — local HTTPS, staging, and prod don't map cleanly to "development vs production." I split it into an explicit flag so cookie security is a deliberate config choice, not a side effect of the build mode.- Duplicate users. A Telegram identity can arrive more than once. I key bot-created accounts on a synthetic, validated email (
telegram_{id}@ecomap.uz) so a returning user resolves to one profile instead of spawning duplicates, and sync the latest Telegram data intoprofileson/me.
None of that is glamorous. All of it is the kind of thing a BaaS auth module hides until the day you need it to behave differently — and then you're fighting the abstraction instead of the problem.
The quieter wins
Three things that were painful on the platform and clean on my own stack:
- One type-safe spine. Eden Treaty exports the Elysia API straight into the client. Request and response shapes have a single source of truth; a refactor on the server surfaces as a type error in the app, not a runtime surprise across four apps in the monorepo.
- Native image processing, on my terms. sharp is a native dependency and needs care to build and deploy — I build the API with
--external sharpand run it under Bun. Owning the runtime made that a config line instead of a platform limitation. - Background work off the HTTP path. Photo derivatives, notifications, and AI categorisation belong in a queue, not in a request. A dedicated
apps/jobsservice plus a shared jobs package handles them under systemd, so the web tier stays responsive.
How the rewrite actually ran
I didn't hand-type 94 commits of migration. The rewrite was plan-driven: a docs/migration-plan/TASKS.md with a checklist and a progress table, executed through Claude Code — check off each task, update the table, move on. It's the same structured, multi-agent workflow I run across my projects, and it's most of why a full-stack rewrite fit into weeks of evenings. (I've written about that pattern separately.)
If you're on BaaS, deciding whether to move
I'm not going to tell you to rip out Supabase. The v1 trade was correct — it got a real product in front of real users fast. Migrate when, and only when, the platform starts costing you at the edges:
- You're fighting the auth/storage abstractions more than you're using them.
- Your cross-cutting concerns — sessions, background jobs, native deps — need control the platform won't give.
- You already have (or can justify) a stack you'll reuse, so the rewrite compounds instead of being a one-off.
For EcoMap all three were true, and the migration paid for itself in a controllable, type-safe codebase I can extend without asking permission.
EcoMap is live at ecomap.uz. If you're planning a build or a migration off BaaS, that's the kind of work I take on.