Octane
Fly.Website.Octane deploys an OctaneJS
fullstack app to Fly. 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 self-contained SSR server
bundle, and the Node deploy target’s finishing pass wraps its fetch
handler as a Node HTTP program. The server runs on a Machine
(Fly.Service on a Fly.App, plus a shared IPv4 so
https://{app}.fly.dev answers); client assets are baked into the
image. No Dockerfile, no build command to run.
Install
Section titled “Install”The build integration is not bundled with alchemy. Install
@alchemy.run/frontend-frameworks; the resource loads the package’s
/octane and /octane/node exports from your project at deploy
time. It is only used at build time, so a dev dependency is enough:
bun add -d @alchemy.run/frontend-frameworksnpm install -D @alchemy.run/frontend-frameworkspnpm add -D @alchemy.run/frontend-frameworksyarn add -D @alchemy.run/frontend-frameworksConfigure Octane
Section titled “Configure Octane”Your octane.config.ts picks the deploy target, exactly as in
Octane’s own deployment story — select the Node marker adapter, not
aws() or cloudflare():
import { node } from "@alchemy.run/frontend-frameworks/octane/node-adapter";import { defineConfig, RenderRoute } from "@octanejs/vite-plugin";
export default defineConfig({ adapter: node(), router: { routes: [new RenderRoute({ path: "/", entry: ["App", "/src/App.tsx"] })], },});A missing or foreign adapter fails the deploy with an actionable error.
Vite plugins like Tailwind go in vite.config.ts next to it,
alongside the Octane plugin:
import { octane } from "@octanejs/vite-plugin";import tailwindcss from "@tailwindcss/vite";import { defineConfig } from "vite";
export default defineConfig({ plugins: [octane(), tailwindcss()],});Declare the Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the Stack):
import * as Fly from "alchemy/Fly";
export const Website = Fly.Website.Octane("Website");Omit app and Alchemy creates a Fly.App under the site’s
namespace. Pass an existing App to put the site on it:
export const Site = Fly.App("Site");
export const Website = Fly.Website.Octane("Website", { app: Site,});Add it to the Stack
Section titled “Add it to the Stack”Yield the site from your Stack and return its URL:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "MyOctaneSite", { providers: Fly.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const site = yield* Website; return { url: site.url }; }),);On deploy, url is https://{app}.fly.dev. The resolved site also
exposes app, service, ip, and certificate (all undefined
under alchemy dev).
Add environment variables
Section titled “Add environment variables”Process environment is configured under env — plain strings, or
Redacted values. They are copied onto process.env before the
build and onto the Machine at deploy time:
export const Website = Fly.Website.Octane("Website", { env: { GREETING: "Hello from alchemy", API_BASE: "https://api.example.com", },});The same values are injected into the dev server’s process
environment under alchemy dev, so server code reads them in both
modes.
Read the environment in server code
Section titled “Read the environment in server code”Octane’s server runs in Node on the Machine, so middleware and
ServerRoute handlers read the environment from process.env:
new ServerRoute({ path: "/api/greeting", methods: ["GET"], handler: async () => { return Response.json({ greeting: process.env.GREETING ?? "hello" }); },});Asset routing
Section titled “Asset routing”Octane’s intended setup is asset-first with SSR on miss: exact files
in dist/client are served by the Node process without invoking the
Octane handler, and every miss streams from Octane SSR. That is the
default — there is nothing to configure. GET /health answers for
the platform health check.
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. site.url is the local address and no Fly resources
are created (app, service, ip, and certificate are
undefined). Wrap the site in Alchemy.remote() to deploy the real
infrastructure even during dev.
Custom domain
Section titled “Custom domain”const site = yield* Fly.Website.Octane("Web", { domain: "app.example.com",});Alchemy requests ACME (Fly.Certificate) on the App and url
becomes https://{domain}. Point existing DNS at the App — the
Website does not create DNS records. See
IPs & certificates.