Back Original

We moved Railway's frontend off Next.js. Builds went from 10+ mins to under 2

Railway's entire production frontend no longer runs on Next.js. The dashboard, the canvas, railway.com, all of it now runs on Vite + TanStack Router, and we shipped the migration in two PRs with zero downtime.

Next.js got railway.com from zero to a production app serving millions of users monthly. It's an excellent framework, but it stopped being the right one for our product.

Frontend builds had crept past 10 minutes. Six of those minutes were Next.js alone, half of it stuck on "finalizing page optimization." For a team that ships multiple times a day, that kind of build time isn't a minor annoyance. It's like a very expensive tax on every single iteration.

Railway’s app is overwhelmingly client-side. The dashboard is a rich, stateful interface. The canvas is real-time. Websockets are everywhere. The server-first primitives in Next.js weren't something we used, and we'd ended up building our own abstractions on top of the Pages Router just to support layouts and routing concerns that the framework didn't handle the way we needed.

We were still on the Pages Router, which made shared layouts hacky. Every layout pattern was a bolted-on workaround rather than a first-class framework primitive. The App Router would have solved some of these problems, but it leans heavily into server-first patterns, and our product is intentionally client-driven. Adopting it would have meant rebuilding around a paradigm we don't need.

We wanted a stack that matches how we actually build: explicit, client-first, and fast to iterate on. It also helps that we genuinely enjoy working with it.

For the Product team, we wanted a few niceties that help us avoid thinking about how we needed to implement our front-end and found the following to really convince us.

Several of us tried TanStack Start over the holidays and the reaction was unanimous. We like building with it, and for a product like Railway's dashboard, that matters as much as any benchmark.

Once we made the choice, I got to work. Pre-squash before merge, I must have made 100s of commits.

PR 1 replaced everything Next.js-specific: next/image, next/head, next/router. Each was swapped for either a native browser API or a framework-agnostic alternative. This PR changed nothing about the framework itself. It just removed every dependency on it, so that PR 2 could be a clean swap.

PR 2 swapped the framework. 200+ routes migrated. We systematically extracted everything non-routing-related from page files into individual React components first, then generated all routes from the original page tree.

We then added Nitro as the server layer and replaced next.config.js with Nitro config, consolidating redirects (500+), security headers, and caching rules into one place. We also replaced Node.js APIs that Next.js had provided polyfills for (Buffer, url.parse, and others) with browser-native alternatives, which left us with cleaner code as a side effect.

Merged on an early Sunday morning. The team dogfooded immediately with a live war room in Discord, and a stream of fixes landed same day. No downtime.

Sure we gained a faster, more explicit stack, but not without trade-offs.

We run our production frontend the same way our users run theirs: preview deploys per PR, health checks, zero-downtime rollouts. When we swapped the entire build system and framework, we didn't touch infrastructure. We changed code, pushed it, and Railway handled the rest.

Fastly now serves most of our traffic directly from the edge. Marketing pages are cached, dynamic pages use ISR where needed, and our frontend servers are mostly idle as a result. Vite's asset model makes this work particularly well. Each module gets its own content-hashed chunk, so shipping a change to billing only invalidates that chunk. Returning users download kilobytes, not megabytes.

This is how we think frontends should be deployed: the build is fast, the assets are immutable and cache-friendly, and the infrastructure underneath handles rollouts, previews, and routing without you having to think about it. Your frontend framework should be optimized for iteration speed, and your infrastructure should make shipping those iterations invisible. That's the experience we're building for ourselves and for everyone on Railway.

The speed of iteration on a frontend matters more now than it ever has.

Builds that took 10+ minutes now finish in under two. The dev server starts instantly. Route changes are type-checked at the boundary. Layouts compose without workarounds.

The gap between writing code and getting it in front of users is the bottleneck, and everything we've done here, the framework swap, the edge caching, the asset model, is about closing that gap. Vite + TanStack sets us up for a world where shipping frontend changes is near-instant, and that's the world we're building toward.