[GH-ISSUE #80] Pluggable deployment adapters: support Cloudflare, Vercel, Netlify, and custom targets #23

Open
opened 2026-05-06 12:36:35 +02:00 by BreizhHardware · 3 comments

Originally created by @southpolesteve on GitHub (Feb 25, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/80

Problem

vinext deploy is currently hard-coded to Cloudflare Workers. The entire deploy pipeline (config generation, dependency installation, build adjustments, and the deploy command itself) lives in a single deploy.ts file that only knows about wrangler, Workers, and KV.

As vinext grows, people want to deploy to other targets (Vercel, Netlify, generic Node servers). We need a way for deployment providers to plug in without forking the core.

Current state

The good news: the separation is already cleaner than it looks. Six of seven CLI commands are fully provider-agnostic today:

Command Provider-specific?
vinext init No, generates a plain vite.config.ts with just vinext()
vinext check No, pure Next.js compatibility scanner
vinext dev No, just starts Vite dev server
vinext build No, standard Vite build (with conditional codepath if CF plugin is detected)
vinext start No, pure Node.js HTTP server
vinext lint No
vinext deploy Yes, entirely Cloudflare

The Cloudflare-specific code lives in four places:

  1. deploy.ts (~780 lines): Generates wrangler.jsonc, worker entries, installs @cloudflare/vite-plugin and wrangler, builds, calls wrangler deploy
  2. vinext:cloudflare-build plugin in index.ts (lines 3058-3204): Post-build hook that injects __VINEXT_SSR_MANIFEST__ and __VINEXT_LAZY_CHUNKS__ globals into the worker entry, generates _headers file for immutable asset caching
  3. Conditional config in index.ts: A hasCloudflarePlugin boolean (detected by scanning plugin names for vite-plugin-cloudflare) that controls SSR externals, build manifest, and multi-environment behavior
  4. cloudflare/ directory: kv-cache-handler.ts (implements the already-pluggable CacheHandler interface), tpr.ts (Traffic-aware Pre-Rendering via Cloudflare analytics API)

The cache system (CacheHandler interface in shims/cache.ts) is already fully pluggable. The ISR layer (server/isr-cache.ts) is generic. The production Node server (server/prod-server.ts) has zero Cloudflare code.

Proposal: deployment adapters

Following the same pattern used by SvelteKit (@sveltejs/adapter-cloudflare, @sveltejs/adapter-vercel, etc.) and Astro (@astrojs/cloudflare, @astrojs/vercel, etc.), we introduce a deployment adapter interface.

Adapter interface (rough sketch)

interface DeployAdapter {
  /** Human-readable name, e.g. "Cloudflare Workers" */
  name: string;

  /**
   * Vite config adjustments for this target.
   * Called during vinext's config hook.
   *
   * Examples:
   *   - Cloudflare: disable SSR externals, enable build manifest
   *   - Vercel: set output directory for serverless functions
   *   - Node: no changes needed
   */
  configureVite?: (ctx: AdapterContext) => ViteConfigOverrides;

  /**
   * Additional Vite plugins required by this adapter.
   * Returned plugins are merged into the plugin array.
   *
   * Examples:
   *   - Cloudflare: returns @cloudflare/vite-plugin
   *   - Vercel: returns vercel's vite plugin or preset
   */
  vitePlugins?: (ctx: AdapterContext) => Plugin[];

  /**
   * Post-build hook. Runs after all Vite environments are built.
   * Use this for injecting globals, rewriting output, generating
   * platform-specific files, etc.
   *
   * Examples:
   *   - Cloudflare: inject __VINEXT_SSR_MANIFEST__, generate _headers
   *   - Vercel: generate .vercel/output config
   */
  postBuild?: (ctx: BuildContext) => Promise<void>;

  /**
   * Generate platform-specific config files if they don't exist.
   * Called during `vinext deploy` before the build step.
   *
   * Examples:
   *   - Cloudflare: generate wrangler.jsonc, worker/index.ts
   *   - Vercel: generate vercel.json
   *   - Netlify: generate netlify.toml
   */
  generateConfig?: (ctx: AdapterContext) => Promise<GeneratedFile[]>;

  /**
   * Generate the server entry point for this target.
   *
   * Examples:
   *   - Cloudflare: Workers fetch() handler
   *   - Vercel: serverless function handler
   *   - Node: already covered by vinext start, may be a no-op
   */
  generateServerEntry?: (ctx: AdapterContext) => Promise<GeneratedFile | null>;

  /**
   * npm packages this adapter needs installed.
   */
  dependencies?: () => { name: string; dev?: boolean }[];

  /**
   * Run the actual deployment.
   * Called during `vinext deploy` after the build completes.
   *
   * Examples:
   *   - Cloudflare: exec `wrangler deploy`
   *   - Vercel: exec `vercel deploy` or `vercel --prod`
   *   - Netlify: exec `netlify deploy --prod`
   */
  deploy: (ctx: DeployContext) => Promise<void>;
}

Package structure

Separate packages in this monorepo:

packages/
  vinext/              # Core (unchanged package name)
  adapter-cloudflare/  # @vinext/adapter-cloudflare
  adapter-vercel/      # @vinext/adapter-vercel (future)
  adapter-netlify/     # @vinext/adapter-netlify (future)
  adapter-node/        # @vinext/adapter-node (future, wraps vinext start)

User-facing config

The adapter is specified in vite.config.ts, similar to how SvelteKit does it in svelte.config.js:

import vinext from "vinext";
import cloudflare from "@vinext/adapter-cloudflare";

export default defineConfig({
  plugins: [vinext({ adapter: cloudflare() })],
});

Then vinext deploy reads the adapter from the resolved config. No CLI argument needed in the common case. If no adapter is configured, vinext deploy prints an error telling the user to pick one.

What changes in core

  1. Define the DeployAdapter interface in the vinext core package
  2. Replace hasCloudflarePlugin detection in index.ts with adapter-driven config: the adapter's configureVite() hook provides the overrides instead of vinext detecting plugins by name
  3. Extract vinext:cloudflare-build into the Cloudflare adapter's postBuild() hook
  4. Extract deploy.ts into @vinext/adapter-cloudflare (the current deploy.ts essentially becomes the adapter)
  5. Update cli.ts to load the adapter from vite config and call its deploy() method
  6. vinext build stays generic: the adapter hooks run at config time and post-build time, so vinext build doesn't need to know which adapter is in use

What stays the same

  • vinext init, vinext check, vinext dev, vinext start, vinext lint are unchanged
  • The CacheHandler interface is already pluggable (unrelated to this work)
  • server/prod-server.ts remains a pure Node server
  • server/isr-cache.ts remains generic

Open questions

How should adapters interact with existing Vite platform plugins?

Cloudflare, Vercel, and Netlify all have their own Vite plugins. Should the adapter:

  • (a) Wrap and re-export the platform's Vite plugin (adapter owns the full pipeline)
  • (b) Coexist alongside the platform's Vite plugin (adapter only handles deploy, platform plugin handles build)
  • (c) Both, with the adapter detecting whether the platform plugin is already configured

Option (c) is probably the most pragmatic. The adapter can provide the platform plugin via vitePlugins() if the user hasn't already added it manually, but also work alongside a manually configured plugin.

Should vinext start be documented as the "deploy anywhere" solution?

For targets that don't have a dedicated deploy platform (Docker, fly.io, Railway, any VPS), vinext start already gives you a Node HTTP server. Rather than building adapters for every hosting provider, we could document vinext start as the universal escape hatch and provide guidance on running it in Docker, systemd, etc.

Adapter-specific CLI flags

The current vinext deploy has Cloudflare-specific flags (--experimental-tpr, --tpr-coverage, etc.). Should adapter-specific flags be:

  • (a) Namespaced: vinext deploy --cloudflare-tpr
  • (b) Passed through: vinext deploy -- --experimental-tpr (everything after -- goes to the adapter)
  • (c) Configured in vite.config.ts instead of CLI flags: cloudflare({ tpr: { coverage: 90 } })

Option (c) feels cleanest since the adapter is already configured in vite.config.ts.

Implementation plan

  1. Define the DeployAdapter interface in packages/vinext/src/adapter.ts
  2. Create packages/adapter-cloudflare/ and extract current Cloudflare code into it
  3. Update index.ts to call adapter hooks instead of checking hasCloudflarePlugin
  4. Update cli.ts deploy command to load adapter from config
  5. Ensure all existing tests and examples still work (should be a refactor, not a behavior change)
  6. Document the adapter interface for community contributors
Originally created by @southpolesteve on GitHub (Feb 25, 2026). Original GitHub issue: https://github.com/cloudflare/vinext/issues/80 ## Problem `vinext deploy` is currently hard-coded to Cloudflare Workers. The entire deploy pipeline (config generation, dependency installation, build adjustments, and the deploy command itself) lives in a single `deploy.ts` file that only knows about wrangler, Workers, and KV. As vinext grows, people want to deploy to other targets (Vercel, Netlify, generic Node servers). We need a way for deployment providers to plug in without forking the core. ## Current state The good news: the separation is already cleaner than it looks. Six of seven CLI commands are fully provider-agnostic today: | Command | Provider-specific? | |---------|-------------------| | `vinext init` | No, generates a plain vite.config.ts with just `vinext()` | | `vinext check` | No, pure Next.js compatibility scanner | | `vinext dev` | No, just starts Vite dev server | | `vinext build` | No, standard Vite build (with conditional codepath if CF plugin is detected) | | `vinext start` | No, pure Node.js HTTP server | | `vinext lint` | No | | `vinext deploy` | **Yes, entirely Cloudflare** | The Cloudflare-specific code lives in four places: 1. **`deploy.ts`** (~780 lines): Generates wrangler.jsonc, worker entries, installs `@cloudflare/vite-plugin` and `wrangler`, builds, calls `wrangler deploy` 2. **`vinext:cloudflare-build`** plugin in `index.ts` (lines 3058-3204): Post-build hook that injects `__VINEXT_SSR_MANIFEST__` and `__VINEXT_LAZY_CHUNKS__` globals into the worker entry, generates `_headers` file for immutable asset caching 3. **Conditional config in `index.ts`**: A `hasCloudflarePlugin` boolean (detected by scanning plugin names for `vite-plugin-cloudflare`) that controls SSR externals, build manifest, and multi-environment behavior 4. **`cloudflare/` directory**: `kv-cache-handler.ts` (implements the already-pluggable `CacheHandler` interface), `tpr.ts` (Traffic-aware Pre-Rendering via Cloudflare analytics API) The cache system (`CacheHandler` interface in `shims/cache.ts`) is already fully pluggable. The ISR layer (`server/isr-cache.ts`) is generic. The production Node server (`server/prod-server.ts`) has zero Cloudflare code. ## Proposal: deployment adapters Following the same pattern used by SvelteKit (`@sveltejs/adapter-cloudflare`, `@sveltejs/adapter-vercel`, etc.) and Astro (`@astrojs/cloudflare`, `@astrojs/vercel`, etc.), we introduce a deployment adapter interface. ### Adapter interface (rough sketch) ```ts interface DeployAdapter { /** Human-readable name, e.g. "Cloudflare Workers" */ name: string; /** * Vite config adjustments for this target. * Called during vinext's config hook. * * Examples: * - Cloudflare: disable SSR externals, enable build manifest * - Vercel: set output directory for serverless functions * - Node: no changes needed */ configureVite?: (ctx: AdapterContext) => ViteConfigOverrides; /** * Additional Vite plugins required by this adapter. * Returned plugins are merged into the plugin array. * * Examples: * - Cloudflare: returns @cloudflare/vite-plugin * - Vercel: returns vercel's vite plugin or preset */ vitePlugins?: (ctx: AdapterContext) => Plugin[]; /** * Post-build hook. Runs after all Vite environments are built. * Use this for injecting globals, rewriting output, generating * platform-specific files, etc. * * Examples: * - Cloudflare: inject __VINEXT_SSR_MANIFEST__, generate _headers * - Vercel: generate .vercel/output config */ postBuild?: (ctx: BuildContext) => Promise<void>; /** * Generate platform-specific config files if they don't exist. * Called during `vinext deploy` before the build step. * * Examples: * - Cloudflare: generate wrangler.jsonc, worker/index.ts * - Vercel: generate vercel.json * - Netlify: generate netlify.toml */ generateConfig?: (ctx: AdapterContext) => Promise<GeneratedFile[]>; /** * Generate the server entry point for this target. * * Examples: * - Cloudflare: Workers fetch() handler * - Vercel: serverless function handler * - Node: already covered by vinext start, may be a no-op */ generateServerEntry?: (ctx: AdapterContext) => Promise<GeneratedFile | null>; /** * npm packages this adapter needs installed. */ dependencies?: () => { name: string; dev?: boolean }[]; /** * Run the actual deployment. * Called during `vinext deploy` after the build completes. * * Examples: * - Cloudflare: exec `wrangler deploy` * - Vercel: exec `vercel deploy` or `vercel --prod` * - Netlify: exec `netlify deploy --prod` */ deploy: (ctx: DeployContext) => Promise<void>; } ``` ### Package structure Separate packages in this monorepo: ``` packages/ vinext/ # Core (unchanged package name) adapter-cloudflare/ # @vinext/adapter-cloudflare adapter-vercel/ # @vinext/adapter-vercel (future) adapter-netlify/ # @vinext/adapter-netlify (future) adapter-node/ # @vinext/adapter-node (future, wraps vinext start) ``` ### User-facing config The adapter is specified in `vite.config.ts`, similar to how SvelteKit does it in `svelte.config.js`: ```ts import vinext from "vinext"; import cloudflare from "@vinext/adapter-cloudflare"; export default defineConfig({ plugins: [vinext({ adapter: cloudflare() })], }); ``` Then `vinext deploy` reads the adapter from the resolved config. No CLI argument needed in the common case. If no adapter is configured, `vinext deploy` prints an error telling the user to pick one. ### What changes in core 1. **Define the `DeployAdapter` interface** in the vinext core package 2. **Replace `hasCloudflarePlugin` detection** in `index.ts` with adapter-driven config: the adapter's `configureVite()` hook provides the overrides instead of vinext detecting plugins by name 3. **Extract `vinext:cloudflare-build`** into the Cloudflare adapter's `postBuild()` hook 4. **Extract `deploy.ts`** into `@vinext/adapter-cloudflare` (the current deploy.ts essentially becomes the adapter) 5. **Update `cli.ts`** to load the adapter from vite config and call its `deploy()` method 6. **`vinext build` stays generic**: the adapter hooks run at config time and post-build time, so `vinext build` doesn't need to know which adapter is in use ### What stays the same - `vinext init`, `vinext check`, `vinext dev`, `vinext start`, `vinext lint` are unchanged - The `CacheHandler` interface is already pluggable (unrelated to this work) - `server/prod-server.ts` remains a pure Node server - `server/isr-cache.ts` remains generic ## Open questions ### How should adapters interact with existing Vite platform plugins? Cloudflare, Vercel, and Netlify all have their own Vite plugins. Should the adapter: - **(a)** Wrap and re-export the platform's Vite plugin (adapter owns the full pipeline) - **(b)** Coexist alongside the platform's Vite plugin (adapter only handles deploy, platform plugin handles build) - **(c)** Both, with the adapter detecting whether the platform plugin is already configured Option (c) is probably the most pragmatic. The adapter can provide the platform plugin via `vitePlugins()` if the user hasn't already added it manually, but also work alongside a manually configured plugin. ### Should `vinext start` be documented as the "deploy anywhere" solution? For targets that don't have a dedicated deploy platform (Docker, fly.io, Railway, any VPS), `vinext start` already gives you a Node HTTP server. Rather than building adapters for every hosting provider, we could document `vinext start` as the universal escape hatch and provide guidance on running it in Docker, systemd, etc. ### Adapter-specific CLI flags The current `vinext deploy` has Cloudflare-specific flags (`--experimental-tpr`, `--tpr-coverage`, etc.). Should adapter-specific flags be: - **(a)** Namespaced: `vinext deploy --cloudflare-tpr` - **(b)** Passed through: `vinext deploy -- --experimental-tpr` (everything after `--` goes to the adapter) - **(c)** Configured in `vite.config.ts` instead of CLI flags: `cloudflare({ tpr: { coverage: 90 } })` Option (c) feels cleanest since the adapter is already configured in vite.config.ts. ## Implementation plan 1. Define the `DeployAdapter` interface in `packages/vinext/src/adapter.ts` 2. Create `packages/adapter-cloudflare/` and extract current Cloudflare code into it 3. Update `index.ts` to call adapter hooks instead of checking `hasCloudflarePlugin` 4. Update `cli.ts` deploy command to load adapter from config 5. Ensure all existing tests and examples still work (should be a refactor, not a behavior change) 6. Document the adapter interface for community contributors
Author
Owner

@Nsttt commented on GitHub (Feb 25, 2026):

From the Zephyr point of view we will be very interested on making this possible.

We often try to make everything deployable everyone on the bundler level, so its compatible with almost every kind of infra without needing any change.

Worth to mention that for something related to this Nitro comes to mind.

<!-- gh-comment-id:3962751092 --> @Nsttt commented on GitHub (Feb 25, 2026): From the [Zephyr](https://zephyr-cloud.io/) point of view we will be very interested on making this possible. We often try to make everything deployable everyone on the bundler level, so its compatible with almost every kind of infra without needing any change. Worth to mention that for something related to this Nitro comes to mind.
Author
Owner

@pi0 commented on GitHub (Feb 26, 2026):

Appreciate that you’re pushing Vinext toward being platform-agnostic ❤️

I opened #82 for adding initial support for the Nitro Vite plugin.

Nitro already provides a solid abstraction layer for deployment providers (Vercel, Cloudflare, Netlify, Deno deploy, AWS, and more) and runtimes such as Node, Bun, and Deno. It natively integrates with Vite tooling (Environments API and RSC) and generates the required platform-specific output files with close to zero overhead.

Instead of building and maintaining multiple adapter systems, I believe positioning Vinext + Nitro together could be a strong move. Nitro already solves the multi-platform deployment layer in a mature way, and collaborating here would help us grow a healthier ecosystem without duplicating work.

We’re fully open to discussion from the Nitro side and happy to explore how we can align our efforts.

<!-- gh-comment-id:3963196097 --> @pi0 commented on GitHub (Feb 26, 2026): Appreciate that you’re pushing Vinext toward being platform-agnostic ❤️ I opened #82 for adding initial support for the [Nitro](https://v3.nitro.build/) Vite plugin. Nitro already provides a solid abstraction layer for deployment providers (Vercel, Cloudflare, Netlify, Deno deploy, AWS, and [more](https://v3.nitro.build/deploy)) and runtimes such as Node, Bun, and Deno. It natively integrates with Vite tooling (Environments API and RSC) and generates the required platform-specific output files with close to zero overhead. Instead of building and maintaining multiple adapter systems, I believe positioning Vinext + Nitro together could be a strong move. Nitro already solves the multi-platform deployment layer in a mature way, and collaborating here would help us grow a healthier ecosystem without duplicating work. We’re fully open to discussion from the Nitro side and happy to explore how we can align our efforts.
Author
Owner

@Ethan-Arrowood commented on GitHub (Feb 26, 2026):

Hacking around the Next.js server and build process is a pain point. Excited to see this being addressed front-and-center. Expect Harper to participate and following along closely. Thank you for the excellent work here!

<!-- gh-comment-id:3968021627 --> @Ethan-Arrowood commented on GitHub (Feb 26, 2026): Hacking around the Next.js server and build process is a pain point. Excited to see this being addressed front-and-center. Expect [Harper](https://harper.fast) to participate and following along closely. Thank you for the excellent work here!
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#23
No description provided.