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.
Install
Section titled “Install”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:
bun add -d @distilled.cloud/octane @octanejs/adapter-cloudflarenpm install -D @distilled.cloud/octane @octanejs/adapter-cloudflarepnpm add -D @distilled.cloud/octane @octanejs/adapter-cloudflareyarn add -D @distilled.cloud/octane @octanejs/adapter-cloudflareConfigure Octane
Section titled “Configure Octane”Your octane.config.ts picks the deploy target, exactly as in Octane’s
own deployment story:
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 Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the Stack) and derive the typed shape of its bindings from it:
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.
Add it to the Stack
Section titled “Add it to the Stack”Yield the class from your Stack and return its URL:
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.
Add bindings
Section titled “Add bindings”The resource returns a plain Worker, so env accepts the full binding
vocabulary — KV namespaces, R2 buckets, secrets:
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.
Read bindings in server code
Section titled “Read bindings in server code”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):
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.
Asset routing
Section titled “Asset routing”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.
Octane SPAs use Vite instead
Section titled “Octane SPAs use Vite instead”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", },});Local dev
Section titled “Local dev”bun alchemy devalchemy 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.