Alchemy owns the Cloudflare Vite plugin, so your app shouldn't
By Warya Wayne ·
If you scaffold a TanStack Start app targeting Cloudflare, your vite.config.ts gets the Cloudflare Vite plugin:
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [cloudflare({ viteEnvironment: { name: 'ssr' } }), tanstackStart(), viteReact()],
})That is correct, documented, and what every guide tells you to do. It is also the thing that will break your deploy the moment you use Alchemy.
The failure
[prerender] Prerendering pages...
[prerender] Crawling: /
Error: Server is not running.
at disposeMiniflare (@cloudflare/vite-plugin/dist/index.mjs)
at prerender (@tanstack/start-plugin-core/dist/esm/prerender.js)Baffling on first read, because vite build locally works perfectly. Only the deploy fails, and it fails inside prerendering — a step that has nothing obvious to do with deployment.
Why
Alchemy's Website.Vite resource doesn't shell out to vite build. It builds in-process, and it constructs the builder with its own plugin list:
const builder = await vite.createBuilder({
plugins: [cloudflare(pluginOptions), outputPlugin.plugin],
})Alchemy adds cloudflare() itself, because it needs control over the Worker environment it's about to deploy. If your config also registers it, the build ends up with two instances of the Cloudflare plugin, and therefore two miniflare servers.
TanStack Start's prerenderer boots a server and requests every page through it. One of the two plugin instances finishes its own lifecycle and disposes its miniflare while the prerenderer is still rendering through the other. The server the prerenderer was talking to goes away mid-crawl, and you get Server is not running.
The fix
Remove the Cloudflare plugin from your app config and let Alchemy provide it:
export default defineConfig({
plugins: [tanstackStart({ prerender: { enabled: true } }), viteReact()],
})Two consequences worth knowing before you do it.
Your local vite build changes shape. Without the plugin, prerendering runs in plain Node instead of workerd. If anything imported during prerender expects the Worker environment — bindings, .dev.vars secrets — it will now fail locally while continuing to work under Alchemy. In my case better-auth started throwing on a missing BETTER_AUTH_SECRET, because miniflare had been quietly supplying it from .dev.vars the whole time.
Nothing hooked to your npm script runs on deploy. Alchemy calls Vite's API directly, so "build": "node scripts/generate-feed.mjs && vite build" will not generate your feed when Alchemy builds. Anything that must happen on every build belongs in vite.config.ts as a plugin, not in package.json.
The general rule
When a deploy tool builds your app for you, it owns the build pipeline. Plugins that shape the output target — the Cloudflare plugin here — belong to whoever is doing the deploying. Plugins that shape your application stay in your config.
The confusing part is that neither set of docs is wrong. TanStack tells you to add the plugin because you need it. Alchemy doesn't tell you to remove it because from its side it simply provides one. Only when you combine them does the ownership question come up, and nothing surfaces it until a miniflare disposes underneath your prerenderer.