DEV Community

Cover image for We ran vinext (Next.js on Vite) inside a Gadget app
Gadget for Gadget

Posted on

We ran vinext (Next.js on Vite) inside a Gadget app

Use vinext in Gadget to get a drop-in replacement for your Next apps on Gadget's infrastructure.

Cloudflare dropped something pretty fun the other week: they rebuilt a drop-in replacement for Next.js on top of Vite. It took them about a week and only cost $1100 in tokens.

The project is called vinext. It keeps the Next.js API but swaps the underlying toolchain for Vite, which means dramatically faster builds and smaller client bundles. The announcement blog post is worth a read if you care about those numbers.

My immediate reaction was: can we use this to power a Gadget frontend?

Gadget already uses Vite for its frontend tooling. If vinext also runs on Vite, we should be able to get it running. So I spent some time experimenting to get a Next-style frontend running inside a Gadget project.

As it turns out you can get something working pretty quickly!

So I figured I’d share the steps I took to set things up. This isn’t meant to be a strict tutorial, it’s more of a “here are the steps I took and how I used agents to do this ” guide.

Also, this is very experimental on both the vinext and Gadget side of things.

You can also check out the video:

The idea

A normal Gadget app contains everything you need to power a modern full-stack web app.

You get a managed Postgres database, a Node backend, authentication and multi-tenancy, and a generated API client. The frontend is a Vite + React Router project that is already wired up to that backend, and has built-in auth and session management.

The experiment was simple: keep all of that exactly the same, but replace the default frontend runtime with vinext.

Conceptually, the architecture becomes:


The backend still behaves like a normal Gadget app. The frontend just adopts the Next programming model running on Vite.

Starting with a normal Gadget app

I began by creating a new Gadget web app and pulled it down locally using the ggt CLI. Relevant for the vinext migration, all new apps have a package.json file, a vite.config.mts, and a web directory containing the frontend (in addition to everything needed to power and configure the database and backend).

The web directory is where everything interesting happens.

Normally, Gadget frontends use React Router. In this experiment that router disappears and the frontend becomes a vinext app instead.

Once that swap happens, the project suddenly feels a lot like a Next app. Instead of client-side routing handled by React Router, the project now uses vinext’s Next-style page routing. Page components, layouts, and routing conventions look exactly like what you’d expect from a new NextJS app.

What’s nice is that all the Gadget primitives still work exactly the same way. For example, grabbing the authenticated user in a component still looks like this:

const user = useUser();
Enter fullscreen mode Exit fullscreen mode

That hook comes from Gadget and continues to work normally as long as the GadgetProvider is setup properly. vinext is just responsible for routing and rendering.

Making Vite happy

The only part of the integration that required a bit of experimentation was the Vite configuration.

The project needs both the Gadget Vite plugin and the vinext plugin so the frontend can run the vinext runtime while still talking to the Gadget backend. (The Tailwind plugin was used too. It didn’t need to be touched.)

It seems like authentication needed a small passthrough configuration so auth-related requests resolve correctly during development. This was the major hangup I ran into during the migration experiment, and Codex helped me resolve it. It’s definitely possible (probable, even) that there’s a more “Next-native” way to handle this.

vite.config.mts

/**
 * Vite plugin that prevents vinext/RSC from handling Gadget backend routes.
 *
 * The @vitejs/plugin-rsc registers a catch-all middleware that writes a
 * response for ALL requests (returning 404 for unmatched App Router routes).
 * This prevents Gadget's platform from handling backend paths like /auth/*
 * (OAuth flows).
 *
 * This plugin patches the middleware stack after all plugins have registered,
 * wrapping every async handler so that backend paths call next() instead of
 * being handled by the RSC renderer.
 */
function gadgetBackendPassthrough(): Plugin {
  const backendPrefixes = ["/auth/"];

  function isBackendPath(url: string): boolean {
    const pathname = url.split("?")[0];
    return backendPrefixes.some((prefix) => pathname.startsWith(prefix));
  }

  return {
    name: "gadget-backend-passthrough",
    configureServer(server) {
      // Return a post-middleware setup function. This runs after all
      // plugins' configureServer hooks, including vinext and RSC.
      // By listing this plugin AFTER vinext, our setup function runs
      // after the RSC middleware is already in the stack.
      return () => {
        const stack = server.middlewares.stack;

        for (const layer of stack) {
          const fn = layer.handle;
          if (!fn || (fn as any).__gadgetWrapped) continue;

          // Wrap all 3-param middleware handlers (req, res, next)
          // to skip backend paths. This catches the RSC handler
          // regardless of its name.
          if (fn.length === 3 || fn.length === 2) {
            const original = fn;
            const wrapped = function (req: any, res: any, next: any) {
              const url = req.originalUrl ?? req.url ?? "";
              if (isBackendPath(url)) {
                return next();
              }
              return original(req, res, next);
            };
            (wrapped as any).__gadgetWrapped = true;
            layer.handle = wrapped;
          }
        }
      };
    },
  };
}

Enter fullscreen mode Exit fullscreen mode

Once those pieces are in place, vinext spins up and I can build like I would any other NextJS app, but using my Gadget API client to call my app APIs.

Letting an agent do the migration

Rather than doing everything by hand, I let an agent (Codex) handle most of the conversion.

I started with a fresh NextJS app and used the cloudflare/vinext agent skill to convert it to vinext. If you have an existing app, you could convert it instead. The skill one-shot the transformation. I tested locally and used the converted project as a reference for migrating my Gadget app.

From there, it was mostly a matter of copying the frontend structure into the Gadget project and adapting it so it used the Gadget API client and authentication hooks. The agent took care of all of it for me. There was a bit of back and forth around using the Vite Tailwind plugin (my reference app used PostCSS instead). And the only other hangup was the auth passthrough.

Look ma, no Next!

Once everything is wired together, you can build a NextJS-style app on Gadget. But, you know, without actually using NextJS.

You still get access to Gadget’s infra-less setup: define data models, Gadget generates the API client, and call those APIs directly from vinext pages. Authentication flows through Gadget. Multi-tenancy still works. The hosted development environment’s backend and database are unchanged.

The frontend just happens to speak the Next API.

vinext is still very early, but the idea behind it is compelling. If the Next programming model can run comfortably on top of Vite, it opens up a lot of flexibility in how those applications can be built and hosted.

Gadget happens to fit that model well. Because the platform already exposes backend services through a generated client, the frontend runtime becomes relatively interchangeable (so long as it uses Vite!). vinext just restructures how you build the UI layer.

This experiment worked better than expected. But let's be real: I migrated a fresh NextJS app and a fresh Gadget app. I didn’t have a bunch of existing, complex logic. I wasn’t exactly pushing the boundaries of vinext here.

But as a proof of concept, running vinext inside a Gadget project is very possible.

Want to chat more or have questions? I’m usually hanging out in our dev Discord.

Top comments (0)