Skip to content

Cloudflare.Website reference

Source: src/Cloudflare/Website/Astro.ts

A Cloudflare Worker deployed from an Astro project.

Astro runs Astro’s programmatic build with a wrangler-free Cloudflare adapter (@alchemy.run/frontend-frameworks/astro): server-rendered pages execute in the Worker, prerendered pages and client assets deploy as static assets. Your astro.config.* loads natively — no adapter setup or Wrangler configuration required.

Input files are content-hashed (respecting .gitignore by default) so unchanged projects skip the build and deploy entirely.

The @alchemy.run/frontend-frameworks package must be installed in your project; its /astro export is loaded dynamically at deploy time:

Terminal window
bun add -d @alchemy.run/frontend-frameworks

A single call builds the project and deploys the server bundle plus static assets. Pages are server-rendered by default; pages that export const prerender = true are served as static assets. Astro’s server runtime is built against Node APIs, so nodejs_compat is always included in the Worker’s compatibility flags.

const site = yield* Cloudflare.Website.Astro("Website");

With astro: { output: "static" } every page is prerendered at build time and the deploy is assets-only: no server bundle is uploaded — Cloudflare’s asset layer answers every request (serve the built 404.html via assets: { notFoundHandling: "404-page" }). Session provisioning is skipped for declared-static sites since no Worker code runs at request time.

const site = yield* Cloudflare.Website.Astro("Docs", {
astro: { output: "static" },
assets: {
notFoundHandling: "404-page",
},
});

Bind resources through env like any other Worker. Astro code reads them via import { env } from "cloudflare:workers" (or Astro.locals.runtime.env).

const kv = yield* Cloudflare.KV.Namespace("Cache");
const bucket = yield* Cloudflare.R2.Bucket("Uploads");
const site = yield* Cloudflare.Website.Astro("Website", {
env: {
CACHE: kv,
UPLOADS: bucket,
},
});

Astro’s session API is backed by a KV namespace. One is provisioned and bound under the session binding name (SESSION by default) automatically, so Astro.session works with zero configuration. Bind your own namespace under that name to use it instead, or set sessionKVBindingName: false to opt out of session provisioning.

Bringing your own session namespace

const sessions = yield* Cloudflare.KV.Namespace("Sessions");
const site = yield* Cloudflare.Website.Astro("Website", {
env: {
SESSION: sessions,
},
});

Opting out of session provisioning

const site = yield* Cloudflare.Website.Astro("Website", {
sessionKVBindingName: false,
});

By default, every non-gitignored file is hashed to decide whether a rebuild is needed. Use memo to narrow the scope when your project has large directories that don’t affect the build output.

const site = yield* Cloudflare.Website.Astro("Docs", {
memo: {
include: ["src/**", "public/**", "package.json"],
},
});

Your astro.config.* is the home for Astro configuration (integrations, Vite plugins, site, base, …) and loads natively. The Cloudflare adapter is injected for you — declaring an adapter in the config file fails the build. The astro prop is a deploy-time override bag merged OVER the file (values here win) for settings that vary per stage or derive from other resources’ Outputs, which a config file cannot consume. output defaults to "server" — astro’s zero-config "static" default would prerender every page inside workerd, where the Worker’s bindings don’t exist. Use config to point at an alternate config file (relative to rootDir).

const site = yield* Cloudflare.Website.Astro("Blog", {
astro: { site: "https://blog.example.com" },
});

Calling Astro with no arguments returns a constructor you can extend to declare the Worker as a named class. The class is both an Effect you can yield* to deploy and a type you can reference elsewhere — useful when other resources need to bind to this Worker.

class Website extends Cloudflare.Website.Astro<Website>()("Website") {}
const site = yield* Website;

Source: src/Cloudflare/Website/Foldkit.ts

A Cloudflare Worker deployed from a Foldkit app.

Foldkit apps are client-only Vite projects, so Foldkit drives the project’s own vite build — the Foldkit Vite plugin in the app’s vite.config.ts composes with the injected Cloudflare plugin — and deploys the client output as static assets. No Wrangler configuration, build command, or output directory required.

Input files are content-hashed (respecting .gitignore by default) so unchanged projects skip the build and deploy entirely.

Foldkit apps route on the client, so assets.notFoundHandling defaults to "single-page-application" — deep links serve index.html and the Foldkit router takes over.

A single call builds the project and deploys the client output as static assets — no configuration required.

Foldkit app

const site = yield* Cloudflare.Website.Foldkit("Website");

Foldkit project in a subdirectory

const site = yield* Cloudflare.Website.Foldkit("Website", {
rootDir: "applications/web",
});

Unmatched paths serve index.html by default so deep links boot the app and the Foldkit router resolves the route. A site that ships real 404 content overrides the default with notFoundHandling: "404-page".

const site = yield* Cloudflare.Website.Foldkit("Website", {
assets: {
notFoundHandling: "404-page",
},
});

By default the deployment is assets-only. When code must run at the edge — API routes, error reporting, Durable Object classes — point main at your own module that serves the client build through the ASSETS binding (see FoldkitProps.main). Bindings passed in env are reachable from the entry (and from cron handlers), not from browser code — a Foldkit app runs on the client, so anything it needs must come from a route the Worker serves.

const ticker = yield* Cloudflare.KV.Namespace("Ticker");
const site = yield* Cloudflare.Website.Foldkit("Platform", {
main: "src/worker.ts",
env: {
TICKER: ticker,
},
});

By default, every non-gitignored file is hashed to decide whether a rebuild is needed. Use memo to narrow the scope when your project has large directories that don’t affect the build output.

const site = yield* Cloudflare.Website.Foldkit("Website", {
memo: {
include: ["src/**", "public/**", "package.json"],
},
});

Calling Foldkit with no arguments returns a constructor you can extend to declare the Worker as a named class. The class is both an Effect you can yield* to deploy and a type you can reference elsewhere — useful when other resources need to bind to this Worker.

class Website extends Cloudflare.Website.Foldkit<Website>()("Website") {}
const site = yield* Website;

Source: src/Cloudflare/Website/Nextjs.ts

A Cloudflare Worker deployed from a Next.js project.

Nextjs builds the app with the wrangler-free OpenNext pipeline from @alchemy.run/frontend-frameworks/nextjs: next build runs through @opennextjs/cloudflare, the resulting worker is bundled into a self-contained ES module set, and the static assets (including prerendered pages and the read-only incremental cache) deploy as Workers static assets. Input files are content-hashed so unchanged projects skip the build and deploy entirely.

Both @alchemy.run/frontend-frameworks and its peer @opennextjs/cloudflare must be installed in the deploying project. The source provider is loaded from the package’s /nextjs export with a dynamic import().

Local dev (alchemy dev) defaults to preview parity — the built worker served under workerd. Set dev: { mode: "hmr" } for the real next dev (Turbopack HMR) with the Worker’s bindings proxied onto getCloudflareContext().

If open-next.config.ts exists in the project root, Alchemy loads it through OpenNext’s native compiler without rewriting it. Otherwise, Alchemy generates temporary defaults: a read-only static-assets cache, or KV adapters when isr is set. Static-assets revalidation writes are a no-op. Native Next.js and Tailwind configuration files are left unchanged.

Known limitations (upstream @opennextjs/cloudflare):

  • Edge-runtime routes/pages (export const runtime = "edge") are not supported — the build fails with the offending route list; remove the directive (the node runtime runs on Workers). Middleware is fine.
  • next/image optimization requires a zone with Cloudflare Images; on workers.dev, use unoptimized (images serve as raw assets).
  • Partial Prerendering / "use cache" (cacheComponents) and Pages-Router i18n config are untested/out of scope for now. App Router i18n via middleware works (middleware is fully supported).

A single call builds the app and deploys the Worker plus static assets. No open-next.config.ts, Wrangler config, or Alchemy plugin is required.

Basic Next.js site

const site = yield* Cloudflare.Website.Nextjs("Site");

Explicit project root

const site = yield* Cloudflare.Website.Nextjs("Site", {
rootDir: "./apps/web",
});

Nextjs: Optional Native OpenNext Configuration

Section titled “Nextjs: Optional Native OpenNext Configuration”

The same Cloudflare.Website.Nextjs("Site") call works with or without a config file. To customize OpenNext, add open-next.config.ts under rootDir (the working directory by default). Imports and callbacks are preserved; the native OpenNextConfig is passed to OpenNext’s compiler, subject to Cloudflare’s adapter constraints rather than AWS runtime feature parity.

open-next.config.ts
import { defineCloudflareConfig, type OpenNextConfig } from "@opennextjs/cloudflare";
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
export default {
...defineCloudflareConfig({ incrementalCache: staticAssetsIncrementalCache }),
buildCommand: "pnpm exec next build",
} satisfies OpenNextConfig;

Resources passed via env become Worker bindings, readable in route handlers and server components through OpenNext’s getCloudflareContext().

const bucket = yield* Cloudflare.R2.Bucket("Uploads");
const site = yield* Cloudflare.Website.Nextjs("Site", {
env: {
UPLOADS: bucket,
},
});
app/api/upload/route.ts
import { getCloudflareContext } from "@opennextjs/cloudflare";
export async function PUT(request: Request) {
const { env } = getCloudflareContext();
await env.UPLOADS.put("key", await request.text());
return Response.json({ ok: true });
}

Supply the cache namespaces on the resource. Alchemy binds them and adds the Durable Object queue for background regeneration. Without a native config, it also selects the matching adapters. With a native config, select kv-incremental-cache, kv-next-tag-cache, and do-queue there; isr does not override the file’s adapter choices.

const incCache = yield* Cloudflare.KV.Namespace("NextIncCache");
const tagCache = yield* Cloudflare.KV.Namespace("NextTagCache");
const site = yield* Cloudflare.Website.Nextjs("Site", {
isr: { incrementalCache: incCache, tagCache },
});

By default, every project file outside build outputs is hashed to decide whether a rebuild is needed. Use memo to narrow the scope when the project has large directories that don’t affect the build output. open-next.config.ts itself is always hashed. Include any imported config helpers in a narrowed memo.include too (for example, config/**).

const site = yield* Cloudflare.Website.Nextjs("Site", {
memo: {
include: ["app/**", "public/**", "package.json", "next.config.mjs", "config/**"],
},
});

An explicit openNext.buildCommand overrides the native config’s command. openNext.minify and openNext.debug remain resource-level build controls.

const site = yield* Cloudflare.Website.Nextjs("Site", {
openNext: { buildCommand: "pnpm exec next build", minify: true },
});

Calling Nextjs with no arguments returns a constructor you can extend to declare the Worker as a named class. The class is both an Effect you can yield* to deploy and a type you can reference elsewhere — useful when other resources need to bind to this Worker.

class Site extends Cloudflare.Website.Nextjs<Site>()("Site", {
rootDir: "./apps/web",
}) {}
const site = yield* Site;

Source: src/Cloudflare/Website/Nuxt.ts

A Cloudflare Worker deployed from a Nuxt project.

Nuxt builds the app programmatically through the project’s own @nuxt/kit with nitro’s cloudflare_module preset — the project’s nuxt.config.ts loads natively, no nitro.preset edits, no Wrangler configuration, and no build command required. The nitro server bundle deploys as the Worker script; client assets and prerendered pages (.output/public) deploy as Worker static assets.

Requires the @alchemy.run/frontend-frameworks package to be installed in your project; the integration is loaded from its /nuxt export. Input files are content-hashed (respecting .gitignore by default) so unchanged projects skip the build and deploy entirely.

The server build uses nitro’s hybrid workerd Node compatibility (cloudflare.nodeCompat), which relies on workerd’s native node:* modules — the nodejs_compat compatibility flag is always included in the Worker’s compatibility flags to match.

On local dev: alchemy dev runs Nuxt’s own dev server (nitro dev, SSR in a Node worker thread, full HMR) with the Worker’s bindings served on event.context.cloudflare through cloudflare-runtime’s platform proxy — wrangler-free. Literal env values overlay the proxied bindings; resource bindings (KV, R2, D1, …) round-trip to the proxy’s local workerd instance, so dev state is live and shared. Durable Objects declared via a custom main entry only exist in the production build and are not servable in dev yet.

A single call builds and deploys the app — server-rendered pages, API routes, prerendered pages, and client assets included.

Basic Nuxt site

const site = yield* Cloudflare.Website.Nuxt("Website");

Nuxt project in a subdirectory

const site = yield* Cloudflare.Website.Nuxt("Website", {
rootDir: "apps/web",
});

Values passed via env are exposed to server routes and SSR through nitro’s cloudflare_module runtime contract: event.context.cloudflare.env (plus event.context.cf and event.context.cloudflare.context.waitUntil).

Reading env from an API route

const site = yield* Cloudflare.Website.Nuxt("Website", {
env: {
API_KEY: Config.Redacted("API_KEY"),
},
});
// server/api/hello.ts
// export default defineEventHandler((event) => ({
// hasKey: event.context.cloudflare?.env?.API_KEY !== undefined,
// }));

Binding an R2 bucket

const bucket = yield* Cloudflare.R2.Bucket("Uploads");
const site = yield* Cloudflare.Website.Nuxt("Website", {
env: {
UPLOADS: bucket,
},
});

Routes marked for prerendering in routeRules (or via nitro.prerender) render at build time into .output/public and are served as static assets — no Worker invocation. Configure them in your nuxt.config.ts, which loads natively:

nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
"/about": { prerender: true },
},
});

nuxt.config.ts is the primary home for Nuxt configuration — it loads natively. The nuxt prop layers deploy-time overrides on top (the highest-priority c12 layer) for values the file can’t express, like per-stage settings. The bag must be JSON-serializable — no functions, plugins, or modules — and nitro.preset stays owned by the deploy target.

const site = yield* Cloudflare.Website.Nuxt("Website", {
nuxt: {
app: { baseURL: "/docs/" },
runtimeConfig: {
public: { apiBase: "https://api.example.com" },
},
},
});

Nuxt: Custom Worker Exports (Durable Objects)

Section titled “Nuxt: Custom Worker Exports (Durable Objects)”

Nitro’s entry module is the Worker’s exports seam. Point main at your own module that re-exports nitro’s runtime handler (imported from nitropack/presets/cloudflare/runtime/cloudflare-module) and adds extra exports — Durable Object classes must live on the deployed Worker for their namespace bindings to resolve. Every framework route keeps working through the re-exported handler.

worker-entry.ts
// import nitroHandler from "nitropack/presets/cloudflare/runtime/cloudflare-module";
// export class Counter extends DurableObject { ... }
// export default nitroHandler;
const site = yield* Cloudflare.Website.Nuxt("Website", {
main: "worker-entry.ts",
env: {
COUNTER: Cloudflare.DurableObject("Counter", {
className: "Counter",
}),
},
});

alchemy dev runs Nuxt’s own dev server (nitro dev, full HMR) with event.context.cloudflare served wrangler-free through cloudflare-runtime’s platform proxy: resource bindings resolve against a local workerd instance, and literal env values overlay them.

// server/api/greeting.ts — identical code in dev and deployed
// export default defineEventHandler((event) => ({
// greeting: event.context.cloudflare?.env?.GREETING,
// }));
const site = yield* Cloudflare.Website.Nuxt("Website", {
env: { GREETING: "hello" },
});

By default, every non-gitignored file is hashed to decide whether a rebuild is needed. Use memo to narrow the scope when the project lives in a large repository.

const site = yield* Cloudflare.Website.Nuxt("Website", {
memo: {
include: ["app/**", "server/**", "public/**", "nuxt.config.ts", "package.json"],
},
});

Nitro’s isr route rule (incremental static regeneration) is implemented only by the Vercel and Netlify presets — on Cloudflare it is silently ignored at build time, and the route renders on demand in the Worker like any other SSR route. Use prerender for build-time static routes, or cache route rules for runtime caching.

Calling Nuxt with no arguments returns a constructor you can extend to declare the Worker as a named class. The class is both an Effect you can yield* to deploy and a type you can reference elsewhere — useful when other resources need to bind to this Worker.

class Website extends Cloudflare.Website.Nuxt<Website>()(
"Website",
) {}
const site = yield* Website;

Source: src/Cloudflare/Website/Octane.ts

A Cloudflare Worker deployed from an OctaneJS fullstack project.

Alchemy preserves Octane’s native compiler and client build, builds the server for Workers, and generates the Worker entry. By default, dist/server/worker.js deploys as the Worker script and dist/client deploys as static assets; custom Octane build.outDir values are also supported. No hosting adapter, Wrangler configuration, or separate build command is required.

Requires @alchemy.run/frontend-frameworks alongside octane and @octanejs/vite-plugin in your project. Keep the native octane() plugin, Tailwind, and other Vite plugins in vite.config.ts, and application routes and callbacks in octane.config.ts. These files load without being rewritten. Existing adapter: cloudflare() declarations from @octanejs/adapter-cloudflare remain supported but are optional; adapters for other hosting targets are rejected.

Input files are content-hashed (respecting .gitignore by default) so unchanged projects skip the build and deploy entirely.

Octane’s server runtime needs synchronous SHA-256 and AsyncLocalStorage, so the nodejs_compat compatibility flag (enabled by default for every Worker) is required.

A client-only Octane SPA (no octane.config.ts routes) is a plain Vite project — deploy it with Cloudflare.Website.Vite instead, where the octane() compiler plugin composes with the injected Cloudflare Vite plugin.

A single call builds and deploys the app — server-rendered routes, server (API) routes, and client assets included. Define the app’s routes in octane.config.ts; Cloudflare.Website.Octane selects the Worker build:

octane.config.ts

import { defineConfig, RenderRoute } from "@octanejs/vite-plugin";
export default defineConfig({
router: {
routes: [new RenderRoute({ path: "/", entry: ["App", "/src/App.tsx"] })],
},
});

alchemy.run.ts

const site = yield* Cloudflare.Website.Octane("Website");

Octane project in a subdirectory

const site = yield* Cloudflare.Website.Octane("Website", {
rootDir: "apps/web",
});

Values passed via env reach Octane middleware and ServerRoute handlers through context.platform. The generated Worker entry supplies the Cloudflare { env, ctx } pair, so platform.env.MY_KV is the live binding and platform.ctx.waitUntil schedules background work.

Reading a binding from a ServerRoute

// octane.config.ts route
// new ServerRoute({
// path: "/api/hello",
// methods: ["GET"],
// handler: (context) => {
// const platform = context.platform as { env: { API_KEY: string } };
// return Response.json({ hasKey: platform.env.API_KEY !== undefined });
// },
// })
const site = yield* Cloudflare.Website.Octane("Website", {
env: {
API_KEY: Config.Redacted("API_KEY"),
},
});

Binding a KV namespace

const cache = yield* Cloudflare.KV.Namespace("Cache");
const site = yield* Cloudflare.Website.Octane("Website", {
env: {
CACHE: cache,
},
});

alchemy dev runs Octane’s own Vite dev server (the plugin’s in-process SSR middleware — rendering, server routes, and RPC with full HMR). NOTE: Octane’s dev middleware does not supply request-scoped platform bindings (context.platform is undefined in dev — an upstream limitation), so code touching platform.env must tolerate undefined during dev; bindings are live in deployed Workers.

By default, every non-gitignored file is hashed to decide whether a rebuild is needed. Use memo to narrow the scope when the project lives in a large repository.

const site = yield* Cloudflare.Website.Octane("Website", {
memo: {
include: ["src/**", "public/**", "octane.config.ts", "vite.config.ts", "package.json"],
},
});

Calling Octane with no arguments returns a constructor you can extend to declare the Worker as a named class. The class is both an Effect you can yield* to deploy and a type you can reference elsewhere — useful when other resources need to bind to this Worker.

class Website extends Cloudflare.Website.Octane<Website>()(
"Website",
) {}
const site = yield* Website;

Source: src/Cloudflare/Website/StaticSite.ts

A Cloudflare Worker that serves static assets built by a shell command.

StaticSite runs a build command (e.g. npm run build), content-hashes the output directory, and deploys the result as a Cloudflare Worker with static assets. Use this when your site has its own build step that produces a directory of files — Hugo, Zola, Eleventy, or any custom pipeline.

For Vite-based projects, prefer Cloudflare.Website.Vite which handles building automatically.

Point command at your build script and outdir at where it writes output. Alchemy runs the command, hashes the output, and deploys it as an assets-only Worker — no Worker code is uploaded, and Cloudflare’s asset layer serves every request itself.

Deploying a Hugo site

const site = yield* Cloudflare.Website.StaticSite("Blog", {
command: "hugo --minify",
outdir: "public",
});

Provide main to put your own Worker in front of the assets instead. The Worker receives an ASSETS binding it can delegate to:

src/worker.ts
export default {
fetch: (request: Request, env: { ASSETS: Fetcher }) =>
env.ASSETS.fetch(request),
};

Custom Worker in front of the assets

const site = yield* Cloudflare.Website.StaticSite("Blog", {
command: "hugo --minify",
outdir: "public",
main: "./src/worker.ts",
});

Use assets to control how Cloudflare handles routing for your static files — HTML handling, not-found behavior, etc.

const site = yield* Cloudflare.Website.StaticSite("App", {
command: "npm run build",
outdir: "dist",
main: "./src/worker.ts",
assets: {
htmlHandling: "auto-trailing-slash",
notFoundHandling: "single-page-application",
},
});

Set cwd to run the build command in a subdirectory (e.g. a monorepo package). outdir is resolved relative to cwd.

const site = yield* Cloudflare.Website.StaticSite("Web", {
cwd: "apps/web",
command: "npm run build",
outdir: "dist",
main: "apps/web/worker.ts",
});

By default, all non-gitignored files are hashed to decide whether the build should re-run. Use memo to narrow the scope.

Narrowing the memo scope

const site = yield* Cloudflare.Website.StaticSite("Docs", {
command: "npm run build",
outdir: "dist",
main: "./src/worker.ts",
memo: {
include: ["content/**", "templates/**", "config.toml"],
},
});

Rebuilding when a sibling workspace package changes

The default scope only hashes files under cwd (plus the nearest lockfile), so edits to a sibling workspace package the app imports do not retrigger the build on their own. Add the sibling’s sources with a ../ include glob — and keep lockfile: true, since providing include otherwise drops the lockfile from the hash:

const site = yield* Cloudflare.Website.StaticSite("Web", {
cwd: "apps/web",
command: "npm run build",
outdir: "dist",
main: "./src/worker.ts",
memo: {
include: ["**\/*", "../../packages/env/src/**"],
lockfile: true,
},
});

Calling StaticSite with no arguments returns a constructor you can extend to declare the Worker as a named class. The class is both an Effect you can yield* to deploy and a type you can reference elsewhere — useful when other resources need to bind to this Worker.

class Blog extends Cloudflare.Website.StaticSite<Blog>()("Blog", {
command: "hugo --minify",
outdir: "public",
main: "./src/worker.ts",
}) {}
const site = yield* Blog;

Source: src/Cloudflare/Website/SvelteKit.ts

A Cloudflare Worker deployed from a SvelteKit project.

SvelteKit builds the app with SvelteKit’s own Vite pipeline and a wrangler-free in-memory Cloudflare adapter, then re-bundles the Node-flavored server output for workerd. A project-owned vite.config.* loads natively (its sveltekit(...) options apply) — no svelte.config.js (kit v3 dropped it), no @sveltejs/adapter-cloudflare, no Wrangler configuration required. Client assets and prerendered pages are deployed as Worker static assets; dynamic routes are served by the generated Worker.

The @alchemy.run/frontend-frameworks package must be installed in your project — its /sveltekit export is loaded dynamically at deploy time.

Input files are content-hashed (respecting .gitignore by default) so unchanged projects skip the build and deploy entirely.

SvelteKit’s server code runs under nodejs_compat (the server graph is built for Node), so the flag is always included in the Worker’s compatibility flags.

Note on local dev: alchemy dev runs SvelteKit’s own Vite dev server (Node SSR with full HMR). platform.env carries the Worker’s real Cloudflare bindings (KV, R2, D1, …) served by the cloudflare-runtime platform proxy, with literal env values (strings and secrets) overlaid.

A single call builds and deploys the app — server-rendered routes, prerendered pages, and client assets included.

const site = yield* Cloudflare.Website.SvelteKit("Website");

Values passed via env are exposed to server routes through SvelteKit’s platform.env.

const site = yield* Cloudflare.Website.SvelteKit("Website", {
env: {
API_KEY: Config.Redacted("API_KEY"),
},
});
// src/routes/+page.server.ts
// export const load = ({ platform }) => ({
// hasKey: platform?.env?.API_KEY !== undefined,
// });

Kit options live in the sveltekit(...) call in your vite.config.ts, which loads natively. Fallback-page behavior is driven by the platform-native assets.notFoundHandling knob — the build generates the matching fallback page (rendering the app shell, so kit’s own error page shows).

App-shell 404 fallback

const site = yield* Cloudflare.Website.SvelteKit("Website", {
assets: {
notFoundHandling: "404-page",
},
});

The kit prop is a deploy-time override bag merged over your own sveltekit(...) options (the prop wins) — useful for per-stage values the config file can’t compute. JSON-serializable values only.

Deploy-time kit overrides

const site = yield* Cloudflare.Website.SvelteKit("Website", {
kit: {
paths: { base: "/docs" },
},
});

By default, every non-gitignored file is hashed to decide whether a rebuild is needed. Use memo to narrow the scope when the project lives in a large repository.

const site = yield* Cloudflare.Website.SvelteKit("Website", {
memo: {
include: ["src/**", "static/**", "package.json"],
},
});

Calling SvelteKit with no arguments returns a constructor you can extend to declare the Worker as a named class. The class is both an Effect you can yield* to deploy and a type you can reference elsewhere — useful when other resources need to bind to this Worker.

class Website extends Cloudflare.Website.SvelteKit<Website>()(
"Website",
) {}
const site = yield* Website;

Source: src/Cloudflare/Website/Vinext.ts

A Cloudflare Worker deployed from a vinext app.

vinext reimplements the Next.js API surface on Vite. Vinext is the Alchemy-shaped deploy path: it runs the project’s own vite.config.ts (which must register vinext()) through Alchemy’s wrangler-free Cloudflare Vite plugin, then deploys the RSC Worker plus client assets. There is no wrangler.jsonc and no @vinext/cloudflare deploy.

This is not Cloudflare.Website.Nextjs. That resource runs next build through OpenNext. vinext never consumes next build output — mixing the two stacks will fail.

Install vinext, @vitejs/plugin-rsc, react-server-dom-webpack, and @alchemy.run/frontend-frameworks in the deploying project. Do not also register @cloudflare/vite-plugin in vite.config.ts — Alchemy injects its own Cloudflare plugin (vite-plugin-cloudflare:alchemy; vinext matches the vite-plugin-cloudflare: prefix) and no-ops an official plugin if one is still present. The Worker source is @alchemy.run/frontend-frameworks/vinext/source (loaded with a dynamic import(), like the other Website framework resources).

Bindings are declared on this resource (env) and read from import { env } from "cloudflare:workers" in server components, route handlers, and server actions.

The resource injects Alchemy’s KV data-cache adapter into the Vite build and local server. No Alchemy plugin or cache configuration is needed in vite.config.ts (the same ISR codec is used by Redis / S3). Website.Vinext provisions VINEXT_KV_CACHE (do not bind it in env) and alchemy deploy seeds prerender pairs into it. Workers Cache is enabled (cache.enabled) and CF_VERSION_METADATA is bound. There is no @vinext/cloudflare data-cache adapter.

Basic vinext site

const site = yield* Cloudflare.Website.Vinext("Site");

Bindings on the Worker

const site = yield* Cloudflare.Website.Vinext("Site", {
env: {
GREETING: "Hello from vinext on Cloudflare!",
},
});

In a server component:

import { env } from "cloudflare:workers";
export default function Page() {
return <h1>{env.GREETING}</h1>;
}

Custom Worker entry

const site = yield* Cloudflare.Website.Vinext("Site", {
main: "worker/index.ts",
});
class Site extends Cloudflare.Website.Vinext<Site>()("Site") {}
const site = yield* Site;

Source: src/Cloudflare/Website/Vite.ts

A Cloudflare Worker deployed from a Vite project.

Vite uses the Cloudflare Vite plugin to build both the server bundle and client assets in a single vite build invocation — no manual main entrypoint, build command, output directory, or Wrangler configuration required.

Input files are content-hashed (respecting .gitignore by default) so unchanged projects skip the build and deploy entirely.

For a pure static site (no SSR), a single call is all you need. Vite builds the project and Alchemy deploys the output as a Cloudflare Worker with static assets.

const site = yield* Cloudflare.Website.Vite("Website");

SSR frameworks like TanStack Start or SolidStart work with a single call — the nodejs_compat compatibility flag is enabled by default so the server bundle can use Node.js APIs.

TanStack Start

const app = yield* Cloudflare.Website.Vite("TanStackStart");

SolidStart with worker-first routing

const app = yield* Cloudflare.Website.Vite("SolidStart", {
assets: { runWorkerFirst: true },
});

React Router

React Router’s server build (virtual:react-router/server-build) is a build manifest with no default export, so it cannot be deployed as the Worker entry directly. Point main at a module that wraps it with createRequestHandler (React Router’s Cloudflare template ships this as workers/app.ts):

const app = yield* Cloudflare.Website.Vite("ReactRouter", {
main: "workers/app.ts",
});

Frameworks that emit more than one server environment (e.g. React Server Components, which split into an rsc environment and an ssr environment) need viteEnvironments to declare which environment produces the deployed Worker entry and which additional server environments to bundle alongside it. The client environment is always deployed as static assets.

const app = yield* Cloudflare.Website.Vite("ReactRouterRSC", {
viteEnvironments: {
entry: "rsc",
children: ["ssr"],
},
});

By default the deployed Worker entry is the server bundle the framework produces. When the Worker must export more than the framework’s fetch handler — Durable Object classes, additional handlers — point main at your own module that wraps the framework handler and re-exports the extras. main takes precedence over any entry configured in the Vite config.

const app = yield* Cloudflare.Website.Vite("App", {
main: "worker/index.ts",
viteEnvironments: {
entry: "rsc",
children: ["ssr"],
},
});

For SPAs (React, Vue, etc.), configure asset handling so unmatched routes fall back to index.html and the client router takes over.

Vue SPA

const app = yield* Cloudflare.Website.Vite("Vue", {
assets: {
notFoundHandling: "single-page-application",
},
});

Foldkit

Foldkit apps are client-only Vite projects, so a single call deploys them — the Foldkit Vite plugin in the app’s own vite.config.ts composes with the injected Cloudflare plugin. Enable single-page-application not-found handling so deep links boot the app:

const app = yield* Cloudflare.Website.Vite("Foldkit", {
assets: {
notFoundHandling: "single-page-application",
},
});

Cloudflare.Website.Foldkit is the same thing with that default already applied.

Octane SPA

A client-only OctaneJS app (no octane.config.ts routes) is a plain Vite SPA — the octane() compiler plugin in the app’s own vite.config.ts composes with the injected Cloudflare plugin:

const app = yield* Cloudflare.Website.Vite("Octane", {
assets: {
notFoundHandling: "single-page-application",
},
});

Fullstack Octane apps (routes + SSR in octane.config.ts) run their own two-pass build through Octane’s Cloudflare adapter — deploy those with Cloudflare.Website.Octane instead.

Vite: Serving on a Zone Route with a Path Prefix

Section titled “Vite: Serving on a Zone Route with a Path Prefix”

Cloudflare matches static assets against the full request pathname, so a site attached to a route like example.com/docs* only serves assets whose uploaded paths carry the /docs prefix. Set Vite’s base in your vite.config.ts — the emitted HTML references its assets under the prefix, and Alchemy keys the uploaded asset manifest with the same resolved base so the two always agree.

vite.config.ts

import { defineConfig } from "vite";
export default defineConfig({
base: "/docs/",
});

alchemy.run.ts

const docs = yield* Cloudflare.Website.Vite("Docs", {
routes: [{ pattern: "example.com/docs*", zoneName: "example.com" }],
});

By default, every non-gitignored file is hashed to decide whether a rebuild is needed. Use memo to narrow the scope when your project has large directories that don’t affect the build output.

Narrowing the memo scope

const site = yield* Cloudflare.Website.Vite("Docs", {
memo: {
include: ["src/**", "content/**", "package.json"],
},
});

Rebuilding when a sibling workspace package changes

The default scope only hashes files under the project root (plus the nearest lockfile), so edits to a sibling workspace package the app imports do not retrigger the build on their own. Add the sibling’s sources with a ../ include glob — and keep lockfile: true, since providing include otherwise drops the lockfile from the hash:

const site = yield* Cloudflare.Website.Vite("Web", {
rootDir: "apps/web",
memo: {
include: ["**\/*", "../../packages/env/src/**"],
lockfile: true,
},
});

Calling Vite with no arguments returns a constructor you can extend to declare the Worker as a named class. The class is both an Effect you can yield* to deploy and a type you can reference elsewhere — useful when other resources need to bind to this Worker.

class Website extends Cloudflare.Website.Vite<Website>()("Website") {}
const site = yield* Website;

Source: src/Cloudflare/Website/Vocs.ts

A Cloudflare Worker deployed from a Vocs documentation project.

Vocs’ vocs.config.* loads natively. Alchemy runs Vocs’ Waku/RSC build, deploys its server environments as a Worker, and publishes the client and prerendered output as static assets. No Vite or Wrangler config is required.

Requires @alchemy.run/frontend-frameworks, vocs, and Vocs’ Waku peer dependencies in the project.

Input files are content-hashed (respecting .gitignore by default), so an unchanged project skips its build and deployment. Vocs’ server runtime uses Node APIs, enabled by the Worker’s compatibility date flags automatically.

A single resource builds the documentation project and deploys its server runtime, prerendered pages, generated files, and public assets.

const docs = yield* Cloudflare.Website.Vocs("Docs", {
rootDir: "./docs",
});

Pass Cloudflare resources through env like any other Worker. Server-side Vocs and MDX code can access them from cloudflare:workers.

const searchCache = yield* Cloudflare.KV.Namespace("SearchCache");
const docs = yield* Cloudflare.Website.Vocs("Docs", {
rootDir: "./docs",
env: {
SEARCH_CACHE: searchCache,
},
});

Vocs configuration continues to own the output directory. When vocs.config.* changes outDir, mirror that value on the resource so the generated directory is excluded from the rebuild hash and read correctly.

// vocs.config.ts: defineConfig({ outDir: "build" })
const docs = yield* Cloudflare.Website.Vocs("Docs", {
rootDir: "./docs",
outDir: "build",
});

Use memo to narrow the files that trigger a rebuild in large projects.

const docs = yield* Cloudflare.Website.Vocs("Docs", {
rootDir: "./docs",
memo: {
include: ["src/**", "public/**", "vocs.config.ts", "package.json"],
},
});

Calling Vocs without arguments returns a constructor for declaring the deployed Worker as a named class.

class Docs extends Cloudflare.Website.Vocs<Docs>()("Docs", {
rootDir: "./docs",
}) {}
const docs = yield* Docs;

Source: src/Cloudflare/Website/Waku.ts

A Cloudflare Worker deployed from a Waku project.

Waku builds the project programmatically — no waku.config.ts edits, no Wrangler configuration, and no build command required. A project’s waku.config.* loads natively (same as waku’s CLI) and is where all Waku configuration (srcDir, distDir, basePath, …) lives; the waku prop overrides it per key at deploy time. unstable_adapter is owned by Alchemy and must not be set. Note that a standalone vite.config.ts is NOT loaded (same as waku’s CLI) — Vite config belongs in waku.config.*’s vite field. The RSC server bundle deploys as the Worker script and the client output (including SSG-prerendered pages) deploys as static assets.

Requires the @alchemy.run/frontend-frameworks package to be installed in your project; the integration is loaded from its /waku export. Input files are content-hashed (respecting .gitignore by default) so unchanged projects skip the build and deploy entirely.

Waku’s server runtime uses AsyncLocalStorage, so the nodejs_als compatibility flag is enabled automatically when your compatibility flags include neither nodejs_als nor nodejs_compat. SSG pages are served at their extensionless URLs (/about) via the default drop-trailing-slash asset handling.

A single call builds the project and deploys the RSC server bundle plus the client assets — no configuration required.

Waku site

const site = yield* Cloudflare.Website.Waku("Site");

Waku project in a subdirectory

const site = yield* Cloudflare.Website.Waku("Site", {
rootDir: "apps/web",
});

Waku configuration lives in your waku.config.*. The waku prop overrides it per key at deploy time — for values that vary by stage or come from other resources.

const site = yield* Cloudflare.Website.Waku("Site", {
waku: {
basePath: "/docs/",
},
});

Pass resources through env like any other Worker. Server components and API routes read them from the cloudflare:workers env at request time. Prefer a guarded dynamic import in page modules — Waku’s SSG step renders static pages in Node, where a top-level import { env } from "cloudflare:workers" cannot resolve.

const bucket = yield* Cloudflare.R2.Bucket("Uploads");
const site = yield* Cloudflare.Website.Waku("Site", {
env: {
UPLOADS: bucket,
},
});

By default the deployed Worker entry is Waku’s own RSC server entry. When the Worker must export more than Waku’s fetch handler — Durable Object classes, additional handlers — point main at your own module that wraps Waku’s handler (imported from virtual:waku/server-entry) and re-exports the extras.

src/worker-entry.ts
// import wakuHandler from "virtual:waku/server-entry";
// export class Counter extends DurableObject { ... }
// export default { fetch: (req, env, ctx) => wakuHandler.fetch(req, env, ctx) };
const site = yield* Cloudflare.Website.Waku("Site", {
main: "src/worker-entry.ts",
env: {
COUNTER: Cloudflare.DurableObject("Counter", {
className: "Counter",
}),
},
});

By default, every non-gitignored file is hashed to decide whether a rebuild is needed. Use memo to narrow the scope when your project has large directories that don’t affect the build output.

const site = yield* Cloudflare.Website.Waku("Site", {
memo: {
include: ["src/**", "public/**", "package.json"],
},
});

Calling Waku with no arguments returns a constructor you can extend to declare the Worker as a named class. The class is both an Effect you can yield* to deploy and a type you can reference elsewhere — useful when other resources need to bind to this Worker.

class Site extends Cloudflare.Website.Waku<Site>()("Site") {}
const site = yield* Site;