Skip to content

SvelteKit

Cloudflare.Website.SvelteKit deploys a SvelteKit app as a Cloudflare Worker. It builds the app with SvelteKit’s own Vite pipeline and a wrangler-free in-memory Cloudflare adapter, then re-bundles the server output for workerd. Client assets and prerendered pages deploy as Worker static assets; dynamic routes are served by the generated Worker. There is no svelte.config.js to write, no @sveltejs/adapter-cloudflare to install, and no Wrangler file.

The build integration is not bundled with alchemy — the resource dynamically imports @distilled.cloud/sveltekit from your project at deploy time, so it must be installed alongside your framework. It is only used at build time, so a dev dependency is enough:

Terminal window
bun add -d @distilled.cloud/sveltekit

There is no framework config to write. The resource drives SvelteKit’s Vite pipeline programmatically and injects its own Cloudflare adapter, so a fresh SvelteKit project deploys as-is. Options that would normally live in svelte.config.js are passed via the kit prop instead — see Kit and adapter options below.

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.SvelteKit("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 SvelteKit 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(
"MySvelteKitSite",
{
providers: Cloudflare.providers(),
state: Cloudflare.state(),
},
Effect.gen(function* () {
const site = yield* Website;
return { url: site.url };
}),
);

SvelteKit’s server code is built for Node, so the nodejs_compat compatibility flag is added automatically — you don’t need to pass it.

See examples/cloudflare-website-sveltekit 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 Cache = Cloudflare.KV.Namespace("Cache");
export const Website = Cloudflare.Website.SvelteKit("Website", {
env: {
CACHE: Cache,
API_KEY: Config.redacted("API_KEY"),
},
});

Cache is a description, not a deploy — Alchemy provisions the real namespace 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 read bindings through SvelteKit’s platform.env. Type it once by augmenting App.Platform with the inferred env type:

src/app.d.ts
import type { WebsiteEnv } from "../alchemy.run.ts";
declare global {
namespace App {
interface Platform {
env: WebsiteEnv;
}
}
}
export {};

Every server route now gets fully typed bindings:

src/routes/+page.server.ts
export const load = async ({ platform }) => {
const cached = await platform?.env?.CACHE.get("greeting");
return { greeting: cached ?? "hello" };
};

platform.env.CACHE is typed as a KV namespace and platform.env.API_KEY as a string — renaming a binding in alchemy.run.ts is a type error in your routes.

Since kit v3 the configuration lives in memory — pass it via the kit prop instead of a svelte.config.js. The generated Cloudflare adapter is configured via adapter:

export const Website = Cloudflare.Website.SvelteKit("Website", {
kit: {
alias: { $lib: "src/lib" },
},
adapter: {
notFoundHandling: "404-page",
fallback: "spa",
},
});

Don’t set kit.adapter — Alchemy injects its own wrangler-free Cloudflare adapter.

Terminal window
bun alchemy dev

alchemy dev runs SvelteKit’s own Vite dev server — Node SSR with full HMR. platform.env carries the Worker’s real Cloudflare bindings, served wrangler-free through the cloudflare-runtime platform proxy: calls like platform.env.CACHE.get(...) round-trip to a local workerd instance, so dev state is live and shared. Literal env values (strings and secrets) overlay the proxied bindings. Your server code is identical in dev and production.