Skip to content

Octane

Source: src/Cloudflare/Website/Octane.ts

A Cloudflare Worker deployed from an OctaneJS fullstack project.

Octane wraps Vite, so Octane is deliberately thin: it drives the project’s own vite build@octanejs/vite-plugin (from the app’s vite.config.ts) builds the client bundle and the SSR server bundle, and the app’s adapter: cloudflare() (from @octanejs/adapter-cloudflare, selected in octane.config.ts) emits the module Worker entry at dist/server/worker.js. That entry deploys as the Worker script and dist/client deploys as static assets — no Wrangler configuration and no build command required.

Requires the @distilled.cloud/octane package to be installed in your project (alongside octane, @octanejs/vite-plugin, and @octanejs/adapter-cloudflare). 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. The app’s own octane.config.ts must select the Cloudflare adapter:

octane.config.ts

import { cloudflare } from "@octanejs/adapter-cloudflare";
import { defineConfig, RenderRoute } from "@octanejs/vite-plugin";
export default defineConfig({
adapter: cloudflare(),
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 the adapter’s runtime contract: context.platform is 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;