Skip to content

Octane

Cloudflare.Website.Octane deploys an OctaneJS fullstack app as a Cloudflare Worker. Octane wraps Vite, so the resource is deliberately thin: it runs your project’s own vite build — Octane’s plugin builds the client bundle and the SSR server bundle, and your adapter: cloudflare() 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 file, no build command to run.

The build integration is not bundled with alchemy — the resource dynamically imports @distilled.cloud/octane from your project at deploy time, so it must be installed alongside Octane’s own Cloudflare adapter. Both are only used at build time, so dev dependencies are enough:

Terminal window
bun add -d @distilled.cloud/octane @octanejs/adapter-cloudflare

Your octane.config.ts picks the deploy target, exactly as in Octane’s own deployment story:

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"] })],
},
});

A missing or foreign adapter fails the deploy with an actionable 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.Octane("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 Octane 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(
"MyOctaneSite",
{
providers: Cloudflare.providers(),
state: Cloudflare.state(),
},
Effect.gen(function* () {
const site = yield* Website;
return { url: site.url };
}),
);

Octane’s server runtime uses synchronous SHA-256 and AsyncLocalStorage, which need the nodejs_compat compatibility flag — it is enabled by default for every Worker, so there is nothing to pass.

See examples/cloudflare-octane for the checked-in example.

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

alchemy.run.ts
import * as Config from "effect/Config";
export const Cache = Cloudflare.KV.Namespace("Cache");
export const Website = Cloudflare.Website.Octane("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.

Octane middleware and ServerRoute handlers read bindings through the adapter’s runtime contract: context.platform is the Cloudflare { env, ctx } pair. Type it with the inferred env type — env and ctx are optional because Octane’s dev server supplies no platform (see Local dev):

octane.config.ts
import type { WebsiteEnv } from "./alchemy.run.ts";
interface Platform {
readonly env?: WebsiteEnv;
readonly ctx?: { waitUntil(promise: Promise<unknown>): void };
}
new ServerRoute({
path: "/api/visits",
methods: ["GET"],
handler: async (context) => {
const platform = context.platform as Platform | undefined;
const cache = platform?.env?.CACHE;
if (cache === undefined) {
return Response.json({ visits: null, dev: true });
}
const visits = Number((await cache.get("visits")) ?? "0") + 1;
platform?.ctx?.waitUntil(cache.put("visits", String(visits)));
return Response.json({ visits });
},
});

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

Octane’s intended setup is asset-first with SSR on miss: exact files in dist/client serve without invoking the Worker, and every miss reaches Octane SSR. That is the default — leave assets.notFoundHandling unset. Both "single-page-application" and "404-page" would stop browser-navigation misses from reaching SSR.

A client-only Octane app (no octane.config.ts routes) is a plain Vite SPA — the octane() compiler plugin composes with the Cloudflare Vite plugin, so deploy it with Cloudflare.Website.Vite:

export const OctaneApp = Cloudflare.Website.Vite("Octane", {
assets: {
notFoundHandling: "single-page-application",
},
});
Terminal window
bun alchemy dev

alchemy dev runs Octane’s own Vite dev server — the plugin’s in-process SSR middleware serves rendering, server routes, and RPC with full HMR.

One limitation, inherited from Octane itself: the dev middleware supplies no request-scoped context.platform, so bindings are not observable from dev-mode app code — write handlers to tolerate an undefined platform, or pipe the site through Alchemy.remote() to deploy the real Worker during dev. Bindings are fully live in deployed Workers and previews.