Vite plugin for running Payload CMS with vinext (Cloudflare's Vite-based re-implementation of Next.js).
Experimental. Both vinext and this plugin are experimental. Tested with Payload 3.80.0, vinext 0.0.33, and Vite 8 (Rolldown).
If you have an existing Payload CMS project on Next.js:
npm install -D vinext vite # Install vinext
npx vinext init # Convert Next.js → vinext
npm install -D vite-plugin-vinext-payload
npx vite-plugin-vinext-payload init # Apply Payload-specific fixes
npm run devNote:
vinext initrunsnpm installinternally. If you hit peer dependency conflicts (common with@vitejs/plugin-react), runnpm install -D vinext vite --legacy-peer-depsbeforenpx vinext init.
The plugin's init command is idempotent — safe to run multiple times. It:
- Adds
payloadPlugin()to yourvite.config.ts - Extracts the inline server function from
layout.tsxinto a separate'use server'module (required for Vite's RSC transform) - Adds
normalizeParamsto the admin page
Use --dry-run to preview changes without writing files.
For Cloudflare D1 projects, see Cloudflare D1 guide for additional configuration.
If you've already run init, or are setting up manually:
// vite.config.ts
import { defineConfig } from "vite";
import vinext from "vinext";
import { payloadPlugin } from "vite-plugin-vinext-payload";
export default defineConfig({
plugins: [vinext(), payloadPlugin()],
});For Cloudflare Workers with RSC:
import { cloudflare } from "@cloudflare/vite-plugin";
import vinext from "vinext";
import { defineConfig } from "vite";
import { payloadPlugin } from "vite-plugin-vinext-payload";
export default defineConfig({
plugins: [
cloudflare({
viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
}),
vinext(),
payloadPlugin({
ssrExternal: ["cloudflare:workers"],
}),
],
});payloadPlugin({
// Additional packages to externalize from SSR bundling
ssrExternal: ["some-native-package"],
// Additional packages to exclude from optimizeDeps
excludeFromOptimize: ["some-broken-package"],
// Additional CJS packages needing default export interop
cjsInteropDeps: ["some-cjs-dep"],
});payloadPlugin() composes these sub-plugins:
| Plugin | Owner bug | What it does |
|---|---|---|
payloadUseClientBarrel |
Payload | Auto-detects @payloadcms/* barrel files that re-export from 'use client' modules and excludes them from RSC pre-bundling (pre-bundling strips the directive, breaking client references) |
payloadConfigAlias |
workerd | Configures SSR externals — only build tools and native addons are externalized (workerd can't resolve externals at runtime) |
payloadOptimizeDeps |
vinext | Per-environment optimizeDeps: excludes problematic packages, force-includes CJS transitive deps for the client. Auto-discovers all next/* alias specifiers from the resolved config so the optimizer doesn't discover them at runtime (which causes a full page reload) |
payloadCjsTransform |
Vite | Fixes this → globalThis in UMD/CJS wrappers and wraps module.exports with ESM scaffolding (skips React/ReactDOM which Vite 8 handles natively) |
payloadCliStubs |
Payload | Stubs packages not needed at web runtime (console-table-printer, json-schema-to-typescript, esbuild-register, ws) |
payloadNavHydrationFix |
Payload | Patches DefaultNavClient and DocumentTabLink to not switch element types (<a> vs <div>) based on usePathname()/useParams() — prevents React 19 tree-destroying hydration mismatches (AST-based via ast-grep) |
payloadNavigationHydrationFix |
vinext | Patches vinext's next/navigation shim on disk so usePathname/useParams/useSearchParams use client snapshots during hydration instead of the server context (which is null on the client) |
payloadRedirectFix |
vinext | Catches NEXT_REDIRECT errors that leak through the RSC stream during async rendering and converts them to client-side location.replace() redirects |
payloadRscExportFix |
@vitejs/plugin-rsc | Fixes @vitejs/plugin-rsc's CSS export transform dropping exports after sourcemap comments |
payloadRscStubs |
vinext / workerd / pnpm | Stubs file-type and drizzle-kit/api for RSC/workerd, polyfills workerd's broken console.createTask, patches RSC serializer to silently drop non-serializable values (functions, RegExps) at the server/client boundary (matching Next.js prod behavior) |
payloadServerActionFix |
vinext | Moves getReactRoot().render() after the returnValue check in vinext's browser entry so data-returning server actions (like getFormState) don't trigger a re-render that resets Payload's form state. Also rewrites the browser entry's relative shim import to use the pre-bundled alias (AST-based via ast-grep) |
cjsInterop |
Vite | Fixes CJS default export interop for packages like bson-objectid (via vite-plugin-cjs-interop) |
Import individual plugins if you need fine-grained control:
import {
payloadUseClientBarrel,
payloadConfigAlias,
payloadOptimizeDeps,
payloadCjsTransform,
payloadCliStubs,
payloadNavHydrationFix,
payloadNavigationHydrationFix,
payloadRedirectFix,
payloadRscExportFix,
payloadRscStubs,
payloadServerActionFix,
} from "vite-plugin-vinext-payload";- Node.js >= 24
- Vite 8 (may work on 6/7 but untested)
- vinext 0.0.33+
- Payload CMS 3.x
These all work fine on Next.js — they exist because vinext reimplements Next.js's framework layer on Vite. See docs/upstream-bugs.md for details on what Next.js does differently.
| Issue | Owner | Our workaround |
|---|---|---|
Barrel exports missing 'use client' directive |
Payload | Auto-exclude affected packages from RSC optimizeDeps |
| RSC export transform drops exports after sourcemap comments | @vitejs/plugin-rsc | Post-transform newline insertion |
console.createTask throws "not implemented" |
workerd | Try/catch polyfill |
file-type / drizzle-kit/api unresolvable in workerd |
pnpm + Vite | Stub modules for RSC |
Navigation shim getServerSnapshot returns wrong values during hydration |
vinext | Patch on disk to use client snapshots |
| Browser entry imports shims via relative paths → optimizer reload + duplicate React | vinext | Rewrite import to aliased specifier; auto-include all next/* aliases in optimizeDeps |
render() called before returnValue check → form state reset |
vinext | AST transform to reorder render after returnValue |
| Components switch element types based on pathname/params → tree-destroying hydration mismatch | Payload | AST transform to force consistent element types |
| Non-serializable values (functions, RegExps) not silently dropped at RSC boundary | vinext | Patch serializer throws to return undefined |
NEXT_REDIRECT errors leak through RSC stream during async rendering |
vinext | Client-side redirect interception |
MIT