Gwion Robertson

Engineering
4
m
The 785-Line Invoice
The 785-Line Invoice
Why Querio moved from Next.js and Vercel to TanStack Start
This is not a post about Next.js being bad. It is a post about fit.
Querio is a reactive notebook runtime. The frontend maintains a live connection to a Python kernel, streams cell output, and keeps a dataflow graph in sync as people work. It is a stateful, long-lived, bidirectional application.
At some point, we found ourselves maintaining a 785-line proxy layer.
That file existed to translate between how our application needed to behave and how our hosting platform expected applications to behave. It worked. But it had become expensive to understand, expensive to change, and very easy to break.
The proxy was not merely a code smell. It was a bill.
What we needed
We moved to TanStack Start because we wanted three things.
First: one runtime. Node locally, Node in CI, Node in production. No edge-runtime caveats, preview-only differences, or surprises created by an abstraction layer we did not control.
Second: a server we could understand. Our production server is a small Node process. It serves the client bundle, compresses and caches static assets, applies sensible cache headers to content-hashed files, and relays the long-lived connections our product depends on. It is not magic; it is code we can read in one sitting.
Third: a deployment story we could explain quickly. We prune the monorepo, install frozen dependencies, build the app, copy the production output into a slim Node image, and run it. Kamal deploys that image to our server. That is the whole path from source to production.
For a product built around persistent work, that clarity matters.
How we migrated without stopping everything
We did not rewrite the application in one heroic pass. That is how migrations acquire a ceremonial launch date and then quietly become everyones second job.
Instead, we used a compatibility layer. Small shims stood in for the Next.js APIs our components already used: links, images, navigation, headers, fonts, dynamic imports, and server-only modules. Hundreds of components could continue to operate while we changed the foundation beneath them.
It was not elegant, and it was not meant to be permanent.
That distinction matters. A compatibility layer is technical debt if nobody owns its removal. It is technical financing if it lets you make a deliberate transition, then repay it. We treated it as the latter: use it to move quickly, remove it as the new architecture settled.
Routing moved to TanStack file-based routes. Server logic moved out of framework-specific conventions and into a plain server directory. The result is not that the system became simple; a reactive notebook product is not simple. Its complexity is now closer to the product itself.
What we gave up
There were real trade-offs. We no longer have automatic preview deployments for every branch. We also lost the surrounding machinery: scripts for cleaning up old preview environments and the operational overhead that came with them.
We gave up some zero-config platform features, including image optimisation. That means we own more decisions ourselves.
We also took responsibility for the production environment. The frontend runs as a container on our own infrastructure, behind kamal-proxy, with TLS at the edge and timeouts suited to requests that can legitimately run for minutes.
Our production frontend is not a globally distributed fleet. It is one machine, one container, one process. For now, that is enough.
The lesson
Platforms encode assumptions. The further your product moves from those assumptions, the more translation code you accumulate.
Vercel is excellent for the kinds of applications Next.js was designed to serve. We had gradually become a different kind of application: one that relies on persistent connections, long-running work, and a server that is part of the product rather than a place to render pages.
The 785-line proxy was the invoice for that mismatch.
If you have a file like that in your own codebase, a file nobody wants to touch, built solely to translate between your application and its host, do not dismiss it as maintenance overhead.
Read it. It may be telling you that the architecture around your product no longer fits the product inside it.
Written by

