1---
2title: SvelteKit
3description: Deploy a SvelteKit app to Cloudflare Workers with Cloudflare.Website.SvelteKit — a wrangler-free in-memory adapter, real bindings on platform.env, and full-HMR local dev.
4sidebar:
5 order: 11
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Cloudflare.Website.SvelteKit` deploys a
13[SvelteKit](https://svelte.dev/docs/kit) app as a Cloudflare Worker.
14It builds the app with SvelteKit's own Vite pipeline and a
15wrangler-free in-memory Cloudflare adapter, then re-bundles the
16server output for workerd. Client assets and prerendered pages deploy
17as Worker static assets; dynamic routes are served by the generated
18Worker. Your `vite.config.ts` loads natively; there is no
19`svelte.config.js` to write (kit v3 dropped it), no
20`@sveltejs/adapter-cloudflare` to install, and no Wrangler file.
21
22## Install
23
24The build integration is not bundled with alchemy. Install
25`@alchemy.run/frontend-frameworks`; the resource loads its
26`/sveltekit` export from your project at deploy time. It is only used at
27build time, so a dev dependency is enough:
28
29<Tabs syncKey="pkgManager">
30 <TabItem label="bun" icon="bun">
31 <Code code={`bun add -d ${devDeps}`} lang="sh" />
32 </TabItem>
33 <TabItem label="npm" icon="npm">
34 <Code code={`npm install -D ${devDeps}`} lang="sh" />
35 </TabItem>
36 <TabItem label="pnpm" icon="pnpm">
37 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
38 </TabItem>
39 <TabItem label="yarn" icon="seti:yarn">
40 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
41 </TabItem>
42</Tabs>
43
44## Configure SvelteKit
45
46Your project's `vite.config.ts` loads natively — your Vite plugins
47and the kit options in your `sveltekit(...)` call all apply as usual.
48Alchemy injects its own wrangler-free Cloudflare adapter (replacing
49any adapter you declare, with a warning), so a fresh SvelteKit
50project deploys as-is. Deploy-specific kit overrides can also be
51passed via the `kit` prop, which merges over your `sveltekit(...)`
52options — see [Kit options and 404 handling](#kit-options-and-404-handling)
53below. A project without a `vite.config.*` works too: the resource
54falls back to a fully programmatic build.
55
56## Declare the Website
57
58Declare the site as a module-level const (rather than inline in the
59Stack) and derive the typed shape of its bindings from it:
60
61```typescript
62// alchemy.run.ts
63import * as Cloudflare from "alchemy/Cloudflare";
64
65export const Website = Cloudflare.Website.SvelteKit("Website");
66
67export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;
68```
69
70`WebsiteEnv` is the typed shape of the Worker's bindings, derived
71from the class — you'll import it into your SvelteKit code in
72[Read bindings in server code](#read-bindings-in-server-code).
73
74## Add it to the Stack
75
76Yield the class from your Stack and return its URL:
77
78```typescript
79// alchemy.run.ts
80import * as Alchemy from "alchemy";
81import * as Effect from "effect/Effect";
82
83export default Alchemy.Stack(
84 "MySvelteKitSite",
85 {
86 providers: Cloudflare.providers(),
87 state: Cloudflare.state(),
88 },
89 Effect.gen(function* () {
90 const site = yield* Website;
91 return { url: site.url };
92 }),
93);
94```
95
96SvelteKit's server code is built for Node, so the `nodejs_compat`
97compatibility flag is added automatically — you don't need to pass
98it.
99
100See
101[examples/cloudflare-website-sveltekit](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-website-sveltekit)
102for the checked-in example.
103
104## Add bindings
105
106The resource returns a plain `Worker`, so `env` accepts the full
107binding vocabulary — KV namespaces, R2 buckets, Durable Objects,
108secrets:
109
110```diff lang="typescript"
111// alchemy.run.ts
112+import * as Config from "effect/Config";
113
114+export const Cache = Cloudflare.KV.Namespace("Cache");
115
116export const Website = Cloudflare.Website.SvelteKit("Website", {
117+ env: {
118+ CACHE: Cache,
119+ API_KEY: Config.Redacted("API_KEY"),
120+ },
121});
122```
123
124`Cache` is a description, not a deploy — Alchemy provisions the real
125namespace because the Website binds it. `Config.Redacted` reads
126`API_KEY` from your environment at deploy time and binds it as a
127Worker secret — see [Secrets & env](/cloudflare/security/secrets-env).
128
129## Read bindings in server code
130
131Server routes read bindings through SvelteKit's `platform.env`. Type
132it once by augmenting `App.Platform` with the inferred env type:
133
134```typescript
135// src/app.d.ts
136import type { WebsiteEnv } from "../alchemy.run.ts";
137
138declare global {
139 namespace App {
140 interface Platform {
141 env: WebsiteEnv;
142 }
143 }
144}
145
146export {};
147```
148
149Every server route now gets fully typed bindings:
150
151```typescript
152// src/routes/+page.server.ts
153export const load = async ({ platform }) => {
154 const cached = await platform?.env?.CACHE.get("greeting");
155 return { greeting: cached ?? "hello" };
156};
157```
158
159`platform.env.CACHE` is typed as a KV namespace and
160`platform.env.API_KEY` as a string — renaming a binding in
161`alchemy.run.ts` is a type error in your routes.
162
163## Kit options and 404 handling
164
165Since kit v3 there is no `svelte.config.js` — kit options live in the
166`sveltekit(...)` call in your `vite.config.ts`, which loads natively:
167
168```typescript
169// vite.config.ts
170import { sveltekit } from "@sveltejs/kit/vite";
171import tailwindcss from "@tailwindcss/vite";
172import { defineConfig } from "vite";
173
174export default defineConfig({
175 plugins: [
176 tailwindcss(),
177 sveltekit({
178 alias: { $lib: "src/lib" },
179 }),
180 ],
181});
182```
183
184The `kit` prop is the deploy-time override bag: JSON-serializable kit
185config merged over your own `sveltekit(...)` options (the prop wins).
186Use it for per-stage values or alchemy `Output`s your config file
187can't compute:
188
189```typescript
190// alchemy.run.ts
191export const Website = Cloudflare.Website.SvelteKit("Website", {
192 kit: {
193 paths: { base: "/docs" },
194 },
195});
196```
197
198No functions or plugins in the bag — `preprocess`, `vitePlugin`, and
199the other construction-time options belong in your `sveltekit(...)`
200call. Don't set an `adapter` in either place: Alchemy always injects
201its own wrangler-free Cloudflare adapter (a declared adapter is
202replaced with a warning).
203
204404 behavior is driven by `assets.notFoundHandling` — the one knob
205configures both the build-time fallback-page generation and the
206assets layer that serves it. The generated 404 page renders the app
207shell, so kit's own error page shows:
208
209```typescript
210// alchemy.run.ts
211export const Website = Cloudflare.Website.SvelteKit("Website", {
212 assets: {
213 notFoundHandling: "404-page",
214 },
215});
216```
217
218## Local dev
219
220```sh
221bun alchemy dev
222```
223
224`alchemy dev` runs SvelteKit's own Vite dev server — Node SSR with
225full HMR. `platform.env` carries the Worker's real Cloudflare
226bindings, served wrangler-free through the cloudflare-runtime
227platform proxy: calls like `platform.env.CACHE.get(...)` round-trip
228to a local workerd instance, so dev state is live and shared. Literal
229`env` values (strings and secrets) overlay the proxied bindings. Your
230server code is identical in dev and production.