Migrating from Next.js to Vite: Building a Static-First PBX Frontend

PBX.IM Tech Lead Developer Victor MalaiVictor Malai·
blog cover

At PBX.IM, our engineers focus on keeping the platform performant, secure, and scalable. At the end of 2025, we hit deployment constraints that led us to rethink our frontend build architecture. In PBX 2.0.0, we migrated from Next.js to a Vite-built React SPA deployed as static assets.

This post walks through why we made the switch, what changed, and what the new architecture enables.

TL;DR: We replaced Next.js with Vite + React to produce pure static builds with no Node runtime dependency. Same UI, same features: simpler deployment model.


Why We Moved Away from Next.js

Our frontend was a Next.js app, but we weren't using it like one. The entire UI ran as a client-side SPA — routing was handled by React Router, data came from our Laravel backend and SIP APIs, and we'd disabled SSR entirely. Our catch-all route looked like this:

// frontend/src/app/[[...]]/page.tsx
"use client";

import dynamic from "next/dynamic";

const AppLayout = dynamic(() => import("@/layouts/app-layout"), {
    ssr: false,
    loading: () => <div style={{ display: "none" }} />,
});

export default function CatchAllPage() {
    return <AppLayout />;
}

A single catch-all page with SSR explicitly disabled. We were paying for Next.js's server runtime, file-system router, and build complexity while opting out of every feature that justified that cost.

The specific pain points:

  • Deployment coupling. We need to build the UI once and serve it across different environments — staging, QA, on-premise, and cloud. Next.js ties the build to a Node server via next start. Getting pure static output required fighting the framework's defaults.
  • Unused overhead. No SSR, no ISR, no API routes, no server components. The framework features we carried added build time and complexity without value.

A single catch-all page with SSR explicitly disabled. We were paying for Next.js's server runtime, file-system router, and build complexity while opting out of every feature that justified that cost.

The specific pain points:

  • Deployment coupling. We need to build the UI once and serve it across different environments — staging, QA, on-premise, and cloud. Next.js ties the build to a Node server via next start. Getting pure static output required fighting the framework's defaults.
  • Unused overhead. No SSR, no ISR, no API routes, no server components. The framework features we carried added build time and complexity without value.
  • Routing mismatch. We already used React Router for all navigation. Next's file-system routing was a layer we worked around, not with.

Vite's architecture — build static assets, serve them however you want — matched what we were already doing, without the workarounds.

What We Changed

The migration touched 723 files but was deliberately narrow in scope: replace the build pipeline and runtime, keep everything else. Components, business logic, hooks, stores, and queries stayed intact.

Build System

The core change was swapping Next.js's build pipeline for Vite:

// frontend/vite.config.ts
export default defineConfig(({ mode }) => ({
    plugins: [react(), svgr({ include: "**/*.svg" })],
    resolve: {
        alias: { "@": path.resolve(__dirname, "./src") },
    },
    build: {
        sourcemap: mode !== "production",
    },
    server: {
        port: 3000,
        host: true,
    },
}));


React plugin, SVG support, path aliases, and environment-aware sourcemaps. Every build produces a `dist/` directory of static files — no server process required.


Entry Point

Next's app/ directory and catch-all route were replaced with a standard SPA entry point:

<!-- frontend/index.html -->
<body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
</body>

main.tsx mounts the React app with React Router handling all navigation — the same routing setup we already had, minus the Next.js wrapper.

Removed Dependencies

We dropped everything Next.js-specific:

  • next, eslint-config-next, @next/bundle-analyzer, @svgr/webpack
  • @vercel/blob, @vercel/edge — server-side Vercel utilities we no longer needed
  • crypto-js — was used in the Next.js API proxy layer
  • The entire app/api/ directory — a proxy layer that forwarded requests to our SIP backend through Next's server runtime

That last point is worth emphasizing. We had ~200 lines of API proxy code (api-utils.ts, axios-instance.ts, qs-utils.ts) running on Next's server to forward requests to our Backend API. Removing the Node runtime meant the frontend talks directly to backend APIs, eliminating an unnecessary hop.

Linting

We moved from eslint-config-next to a framework-agnostic ESLint setup with @typescript-eslint, eslint-plugin-react, and eslint-plugin-react-hooks. Same linting rigor, no framework coupling.

What We Deliberately Kept

This was not a rewrite. The migration preserved:

  • All React components, hooks, and business logic
  • Zustand stores and TanStack Query data layer
  • Tailwind CSS and shadcn/ui component library
  • The Vue.js-based WebPhone component and its build pipeline
  • Playwright E2E test suite
  • Translation pipeline 

The diff is large by file count because hundreds of source files had minor cleanup (removing unused Next.js imports like "use client" directives), but the functional changes were confined to the build and configuration layer.

What This Enables

Environment-agnostic deployment. The dist/ output is a set of static files. Upload to S3, serve from a CDN, drop behind Nginx, or bundle with a Laravel deployment — no Node process to manage, no runtime to configure per environment.

Faster development cycles. Vite's dev server and HMR provide noticeably faster feedback loops than our previous Next.js setup with Turbopack.

Simpler CI/CD. Build artifacts are static files. Our deployment pipeline no longer needs to provision or manage a Node runtime in target environments.

A Note on Direction

This migration went the opposite direction from what's trendy. Many teams move from Vite to Next.js to adopt SSR, server components, or API routes. If your application benefits from those features, Next.js remains a strong choice.

Our requirements pointed the other way. We needed a static SPA that powers VoIP deployments across cloud, hybrid, and on-premise environments. Vite's build-static-assets-first architecture was the better fit. The right tool depends on what your deployment model actually requires.

Share:
Share via /images/facebook.svg
Share via /images/linkedin.svg
Share via /images/twitter.svg
Share via /images/mail.svg
PBX.IM Tech Lead Developer Victor Malai
AuthorVictor Malai

Victor Malai is the Team Lead Developer at PBX.IM, with over 13 years of experience in web development and deep expertise in React, Vue.js, TypeScript, and Laravel. Known for his curiosity and passion for clean architecture, he stays ahead of the latest tech trends while guiding his team in building scalable, well-structured solutions.