Skip to content

Nuxt

Cloudflare.Website.Nuxt deploys a Nuxt app as a Cloudflare Worker. It builds the app through your project’s own @nuxt/kit with nitro’s cloudflare_module preset: the nitro server bundle deploys as the Worker script, and client assets plus prerendered pages deploy as Worker static assets. There is no nitro.preset to edit, no Wrangler file, and no build command to run.

The build integration is not bundled with alchemy. Install @alchemy.run/cloudflare-frameworks; the resource loads its /nuxt export from your project at deploy time. It is only used at build time, so a dev dependency is enough:

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

Your nuxt.config.ts loads natively — modules, layers, and all — so configure Nuxt exactly as you would outside Alchemy:

nuxt.config.ts
export default defineNuxtConfig({
modules: ["@nuxtjs/tailwindcss"],
routeRules: {
"/about": { prerender: true },
},
});

Deploy-specific overrides are merged over it via the nuxt prop (the override wins) — see Prerendering below for an example.

Don’t set nitro.preset — the Cloudflare deploy target owns the preset, and a foreign preset is a hard error.

Declare the site as a module-level const (rather than inline in the Stack) and derive the typed shape of its bindings from it:

alchemy.run.ts
import * as Cloudflare from "alchemy/Cloudflare";
export const Website = Cloudflare.Website.Nuxt("Website");
export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;

WebsiteEnv is the typed shape of the Worker’s bindings, derived from the class — you’ll import it into your Nuxt code in Read bindings in server code.

Yield the class from your Stack and return its URL:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"MyNuxtSite",
{
providers: Cloudflare.providers(),
state: Cloudflare.state(),
},
Effect.gen(function* () {
const site = yield* Website;
return { url: site.url };
}),
);

The server build uses nitro’s hybrid workerd Node compatibility, so the nodejs_compat compatibility flag is added automatically — you don’t need to pass it.

See examples/cloudflare-website-nuxt for the checked-in example.

The resource returns a plain Worker, so env accepts the full binding vocabulary — KV namespaces, R2 buckets, Durable Objects, secrets:

alchemy.run.ts
import * as Config from "effect/Config";
export const Uploads = Cloudflare.R2.Bucket("Uploads");
export const Website = Cloudflare.Website.Nuxt("Website", {
env: {
UPLOADS: Uploads,
API_KEY: Config.redacted("API_KEY"),
},
});

Uploads is a description, not a deploy — Alchemy provisions the real bucket because the Website binds it. Config.redacted reads API_KEY from your environment at deploy time and binds it as a Worker secret — see Secrets & env.

Server routes and SSR read bindings through nitro’s Cloudflare runtime contract, event.context.cloudflare.env. Type it once by augmenting h3’s event context with the inferred env type:

server/types.d.ts
import type { WebsiteEnv } from "../alchemy.run.ts";
declare module "h3" {
interface H3EventContext {
cloudflare: {
env: WebsiteEnv;
};
}
}

Every server route now gets fully typed bindings:

server/api/upload.ts
export default defineEventHandler(async (event) => {
const uploads = event.context.cloudflare.env.UPLOADS;
await uploads.put("hello.txt", "Hello!");
return { ok: true };
});

event.context.cloudflare.env.UPLOADS is typed as an R2 bucket and .API_KEY as a string — renaming a binding in alchemy.run.ts is a type error in your routes. event.context.cf and event.context.cloudflare.context.waitUntil are available the same way.

Routes marked for prerendering in routeRules (or via nitro.prerender) render at build time into .output/public and are served as static assets, with no Worker invocation:

export const Website = Cloudflare.Website.Nuxt("Website", {
nuxt: {
routeRules: {
"/about": { prerender: true },
},
},
});

Nitro’s isr route rule 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.

Nitro’s entry module is the Worker’s exports seam. Point main at your own module that re-exports nitro’s runtime handler and adds extra exports — Durable Object classes must live on the deployed Worker for their namespace bindings to resolve:

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

Every framework route keeps working through the re-exported handler.

Terminal window
bun alchemy dev

alchemy dev runs Nuxt’s own dev server (nitro dev, full HMR) with the Worker’s real bindings served on event.context.cloudflare — wrangler-free, through the cloudflare-runtime platform proxy. Resource bindings (KV, R2, D1) round-trip to a local workerd instance, so dev state is live and shared; literal env values overlay the proxied bindings. Your server code is identical in dev and production.

One limitation: Durable Object classes declared via a custom main entry only exist in the production build, so they are not servable in dev yet.