How we migrated lovable.dev away from Next.js and turned it into another Lovable app | LovableSkip to main content
Get started
Log inGet started
All postsPublished August 18, 2026 in Inside Lovable<br>How we migrated lovable.dev away from Next.js and turned it into another Lovable app
lovable.dev is a pretty busy website: we have 42M+ monthly unique visitors. It's also pretty complex: close to 400 routes, 910K+ lines of non-generated code, supports over 150 agent tools, and even has a small IDE with syntax highlighting inside. It's now hosted on Lovable, the same way as any other Lovable app.
Why we did it
Before we migrated, lovable.dev was a Next.js app hosted on Vercel. (We originally hosted it elsewhere, but started to hit issues as we scaled, like build compatibility between local & prod or poor loading times in certain geographies.) Vercel performed admirably, and our issues went away as soon as we moved to it. That said, we decided to migrate our hosting to Lovable for several reasons.
The main reason to use our own product is dogfooding. We want to feel our users' pain and we want to have the shortest possible feedback loop to keep making our product better for users.
We also want to push the frontier of single-app scaling. We were already good at hosting tens of millions of apps, where the median app is small and low-traffic. Supporting tens of millions of visitors for a single app is a different problem with its own unique challenges. Every improvement we make for ourselves automatically benefits every builder who runs their app on Lovable.
And finally, we wanted to pass any internal knowledge on making the best web apps back to our builder agent—and make every user see the benefits for their apps. With a unified tech stack it's easier than ever.
Background on how we host apps
Today Lovable builds and hosts primarily TanStack Start apps. The framework fits our needs for isomorphic execution, simplicity of deployment and type safety particularly well; see our post “Building apps using TanStack Start” for more background on reasoning and how we use the framework. Each published Lovable app is built as its own worker for Cloudflare's workerd runtime. Then a single entry worker serves every app by loading the code, creating a worker dynamically and dispatching a request to it.
Every dynamic worker lives in its own V8 isolate sandbox. An isolate is a private V8 heap that lives inside a controlling process—a cheaper alternative to using a separate process. Think containers vs. virtual machines. Isolate instantiation cost is proportional to the worker bundle size and could be 4ms at the cheap end up to 1s for a huge bundle. Isolates are cached and reused, so that loading and instantiation overhead is amortized across many requests. This is the part that makes running millions of apps economical. Isolates are typically evicted on an LRU basis or when they get over the memory limit. Eviction is not always graceful—we'll get to that later.
We migrated lovable.dev to TanStack Start to serve it the same way—now it's just one of the possible destinations among 60M+ of our users' apps. There are fewer than 200 lines of code unique to the lovable.dev serving path (mainly handling a different release metadata format and a different observability setup).
lovable.dev is served by the same app loader worker as every other Lovable app—it is just one more bundle among 60M+.
Migration strategy
Migrating a rapidly-developed app is a kind of race where the finish line is running away from you. I started it as a lone developer looking at 350K lines of code. By the time the migration was complete, six months later, the app had grown to over 850K lines (and we've already added 60K on top since then). People here just never stop shipping new things.
Migration progress, week by week. The total kept growing under us the entire time.
The overall plan was shaped by one of my big professional regrets from before Lovable: working on a big-bang migration in parallel with the old system that was still running and switching after achieving feature parity. This time I chose a different approach: we'd rewrite it gradually while keeping everything working on both frameworks. In retrospect this proved to be the most important single choice made in the migration.
Running two frameworks in parallel
We needed to run both frameworks in parallel and dispatch requests to the right one. This way the migration would happen route by route rather than all at once.
A proxy worker in front of both frameworks decides, per route and per user, which one serves the request.
One important aspect of this setup is user experience. Crossing the line between frameworks means hard navigation—the user's browser needs to load a new document and a new set of resources. Compared to that, internal (soft) navigation only loads some scripts and the data for the new route, reusing everything else. In practice hard navigation is much slower (~5s vs....