1---
2title: Octane
3description: Deploy an OctaneJS fullstack app to Cloudflare Workers with Cloudflare.Website.Octane — native Octane and Vite configuration, no hosting adapter or Wrangler file required.
4sidebar:
5 order: 13
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Cloudflare.Website.Octane` builds and deploys an [OctaneJS](https://octanejs.dev)
13fullstack app as a Cloudflare Worker. Alchemy preserves Octane's native
14compiler and client build, builds the server for Workers, and generates
15the Worker entry. No hosting adapter, Wrangler file, or separate build
16command is required.
17
18Your `vite.config.ts` and `octane.config.ts` load without being rewritten.
19Keep the native `octane()` plugin, Tailwind, other Vite plugins, and your
20application routes and callbacks. By default, `dist/server/worker.js`
21deploys as the Worker script and `dist/client` deploys as static assets;
22custom Octane `build.outDir` values are also supported.
23
24## Install
25
26Keep `octane` and `@octanejs/vite-plugin` in your existing Octane project.
27Add `@alchemy.run/frontend-frameworks` as a dev dependency; Alchemy loads
28its Octane integration at deploy time. `@octanejs/adapter-cloudflare` is
29optional:
30
31<Tabs syncKey="pkgManager">
32 <TabItem label="bun" icon="bun">
33 <Code code={`bun add -d ${devDeps}`} lang="sh" />
34 </TabItem>
35 <TabItem label="npm" icon="npm">
36 <Code code={`npm install -D ${devDeps}`} lang="sh" />
37 </TabItem>
38 <TabItem label="pnpm" icon="pnpm">
39 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
40 </TabItem>
41 <TabItem label="yarn" icon="seti:yarn">
42 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
43 </TabItem>
44</Tabs>
45
46## Configure Octane
47
48Define application routes in `octane.config.ts`.
49`Cloudflare.Website.Octane` selects the Worker build, so this file does
50not need an `adapter` declaration:
51
52```typescript
53// octane.config.ts
54import { defineConfig, RenderRoute } from "@octanejs/vite-plugin";
55
56export default defineConfig({
57 router: {
58 routes: [new RenderRoute({ path: "/", entry: ["App", "/src/App.tsx"] })],
59 },
60});
61```
62
63## Existing Cloudflare adapter configurations
64
65Existing `adapter: cloudflare()` declarations remain supported; migration
66is optional. To remove the adapter, delete its import and declaration:
67
68```diff lang="typescript"
69// octane.config.ts
70-import { cloudflare } from "@octanejs/adapter-cloudflare";
71import { defineConfig, RenderRoute } from "@octanejs/vite-plugin";
72
73export default defineConfig({
74- adapter: cloudflare(),
75 router: {
76 routes: [new RenderRoute({ path: "/", entry: ["App", "/src/App.tsx"] })],
77 },
78});
79```
80
81You can then uninstall `@octanejs/adapter-cloudflare` if nothing else in
82your project uses it. Keep `octane()`, Tailwind, and other native plugins
83in `vite.config.ts`. An adapter for a different hosting target is rejected
84rather than silently overridden.
85
86## Declare the Website
87
88Declare the site as a module-level const (rather than inline in the
89Stack) and derive the typed shape of its bindings from it:
90
91```typescript
92// alchemy.run.ts
93import * as Cloudflare from "alchemy/Cloudflare";
94
95export const Website = Cloudflare.Website.Octane("Website");
96
97export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;
98```
99
100`WebsiteEnv` is the typed shape of the Worker's bindings, derived
101from the class — you'll import it into your Octane code in
102[Read bindings in server code](#read-bindings-in-server-code).
103
104## Add it to the Stack
105
106Yield the class from your Stack and return its URL:
107
108```typescript
109// alchemy.run.ts
110import * as Alchemy from "alchemy";
111import * as Effect from "effect/Effect";
112
113export default Alchemy.Stack(
114 "MyOctaneSite",
115 {
116 providers: Cloudflare.providers(),
117 state: Cloudflare.state(),
118 },
119 Effect.gen(function* () {
120 const site = yield* Website;
121 return { url: site.url };
122 }),
123);
124```
125
126Octane's server runtime uses synchronous SHA-256 and
127`AsyncLocalStorage`, which need the `nodejs_compat` compatibility flag —
128it is enabled by default for every Worker, so there is nothing to pass.
129
130See
131[examples/cloudflare-octane](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-octane)
132for the checked-in example.
133
134## Add bindings
135
136The resource returns a plain `Worker`, so `env` accepts the full binding
137vocabulary — KV namespaces, R2 buckets, secrets:
138
139```diff lang="typescript"
140// alchemy.run.ts
141+import * as Config from "effect/Config";
142
143+export const Cache = Cloudflare.KV.Namespace("Cache");
144
145export const Website = Cloudflare.Website.Octane("Website", {
146+ env: {
147+ CACHE: Cache,
148+ API_KEY: Config.Redacted("API_KEY"),
149+ },
150});
151```
152
153`Cache` is a description, not a deploy — Alchemy provisions the real
154namespace because the Website binds it. `Config.Redacted` reads
155`API_KEY` from your environment at deploy time and binds it as a
156Worker secret — see [Secrets & env](/cloudflare/security/secrets-env).
157
158## Read bindings in server code
159
160Octane middleware and `ServerRoute` handlers read bindings through
161`context.platform`, which the generated Worker entry supplies as the Cloudflare
162`{ env, ctx }` pair. Type it with the inferred env type — `env` and
163`ctx` are optional because Octane's dev server supplies no platform
164(see [Local dev](#local-dev)):
165
166```typescript
167// octane.config.ts
168import type { WebsiteEnv } from "./alchemy.run.ts";
169
170interface Platform {
171 readonly env?: WebsiteEnv;
172 readonly ctx?: { waitUntil(promise: Promise<unknown>): void };
173}
174
175new ServerRoute({
176 path: "/api/visits",
177 methods: ["GET"],
178 handler: async (context) => {
179 const platform = context.platform as Platform | undefined;
180 const cache = platform?.env?.CACHE;
181 if (cache === undefined) {
182 return Response.json({ visits: null, dev: true });
183 }
184 const visits = Number((await cache.get("visits")) ?? "0") + 1;
185 platform?.ctx?.waitUntil(cache.put("visits", String(visits)));
186 return Response.json({ visits });
187 },
188});
189```
190
191`platform.env.CACHE` is typed as a KV namespace and `.API_KEY` as a
192string — renaming a binding in `alchemy.run.ts` is a type error in
193your handlers.
194
195## Asset routing
196
197Octane's intended setup is asset-first with SSR on miss: exact files in
198`dist/client` serve without invoking the Worker, and every miss reaches
199Octane SSR. That is the default — leave `assets.notFoundHandling` unset.
200Both `"single-page-application"` and `"404-page"` would stop
201browser-navigation misses from reaching SSR.
202
203## Octane SPAs use `Vite` instead
204
205A client-only Octane app (no `octane.config.ts` routes) is a plain Vite
206SPA — the `octane()` compiler plugin composes with the Cloudflare Vite
207plugin, so deploy it with
208[`Cloudflare.Website.Vite`](/cloudflare/frontend/vite):
209
210```typescript
211export const OctaneApp = Cloudflare.Website.Vite("Octane", {
212 assets: {
213 notFoundHandling: "single-page-application",
214 },
215});
216```
217
218## Local dev
219
220```sh
221bun alchemy dev
222```
223
224`alchemy dev` runs Octane's own Vite dev server — the plugin's
225in-process SSR middleware serves rendering, server routes, and RPC with
226full HMR.
227
228One limitation, inherited from Octane itself: the dev middleware
229supplies no request-scoped `context.platform`, so bindings are not
230observable from dev-mode app code — write handlers to tolerate an
231`undefined` platform, or pipe the site through `Alchemy.remote()` to
232deploy the real Worker during dev. Bindings are fully live in deployed
233Workers and previews.