Skip to content
Oxide
Esc
↑↓navigate↵open⌘Jpreview
On this page

Configuration

Choose the build target, output folders, and how actions reach the server.

Pass options to oxide() in your Vite or Rsbuild config. Start with oxide() — add options when the defaults are wrong for your app.

Option Default What it controls
preset auto "fetch" (Node) or "worker" (Cloudflare)
workerEntry src/server.ts Server entry file (optional when default is missing)
outDir dist Build output folder
clientDir client Client folder inside outDir
actions "http" Action transport, endpoint path, same-origin, OpenRPC
actionHeaders - Public static headers added by the browser action client
middleware [] Request middleware, in order
plugins [] Production beforeBuild / afterBuild hooks
imports [] Side-effect modules loaded at server startup
bodyLimit 1048576 Max Node request body size (413 when exceeded)
notFound - Custom HTML 404 body on Node
env - Node value passed to fetch(request, env, ctx)

preset

Defaults from the project root:

  1. wrangler.jsonc, wrangler.toml, or wrangler.json present → "worker"
  2. Otherwise → "fetch"
  3. Pass preset: "worker" or preset: "fetch" to override

"fetch" builds a Node server (dist/server.js + optional client). "worker" is companion mode for @cloudflare/vite-plugin: oxide supplies virtual:oxide/worker and durable scan; Cloudflare owns the Vite Worker environment and deploy config.

import { function cloudflare(pluginConfig?: PluginConfig): Plugin[]
Vite plugin that enables a full-featured integration between Vite and the Cloudflare Workers runtime. See the [README](https://github.com/cloudflare/workers-sdk/tree/main/packages/vite-plugin-cloudflare#readme) for more details.
@parampluginConfig An optional {@link PluginConfig} object.
cloudflare
} from "@cloudflare/vite-plugin";
import {
const withOxide: <T extends object>(options?: T & {
    root?: string;
}) => Omit<T, "config" | "root" | "viteEnvironment"> & {
    config: CloudflareConfigCustomizer;
    viteEnvironment: ViteEnvironmentOptions;
}
Higher-order options for `@cloudflare/vite-plugin`'s `cloudflare()` — defaults `viteEnvironment.name` to `"ssr"` and runs `mergeDurableBindings` before any user `config` object / function. Pass `root` when the Vite project root is not `process.cwd()` (stripped before options reach Cloudflare). With a root `cloudflare.config.ts` and no wrangler file (or `configPath`), the config `oxide()` converted from it is assigned first. List `oxide()` in Vite `plugins` so it loads before Cloudflare resolves its config. ```ts cloudflare(withOxide()) cloudflare(withOxide({ root: import.meta.dirname })) cloudflare(withOxide({ config: (c) => { … } })) ```
withOxide
} from "oxidejs/wrangler";
import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite"; import { function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
} from "vite";
export default function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
({
UserConfig.plugins?: PluginOption[] | undefined
Array of vite plugins to use.
plugins
: [
// wrangler.jsonc in the project → preset defaults to "worker" function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({ OxidejsOptions.actions?: OxidejsActions | undefined
Transport and path for `*.server.ts` stubs. Default: `"http"` at `/__oxide/action`.
actions
: "ws" }),
function cloudflare(pluginConfig?: PluginConfig): Plugin[]
Vite plugin that enables a full-featured integration between Vite and the Cloudflare Workers runtime. See the [README](https://github.com/cloudflare/workers-sdk/tree/main/packages/vite-plugin-cloudflare#readme) for more details.
@parampluginConfig An optional {@link PluginConfig} object.
cloudflare
(
withOxide<object>(options?: (object & {
    root?: string;
}) | undefined): Omit<object, "config" | "root" | "viteEnvironment"> & {
    config: CloudflareConfigCustomizer;
    viteEnvironment: ViteEnvironmentOptions;
}
Higher-order options for `@cloudflare/vite-plugin`'s `cloudflare()` — defaults `viteEnvironment.name` to `"ssr"` and runs `mergeDurableBindings` before any user `config` object / function. Pass `root` when the Vite project root is not `process.cwd()` (stripped before options reach Cloudflare). With a root `cloudflare.config.ts` and no wrangler file (or `configPath`), the config `oxide()` converted from it is assigned first. List `oxide()` in Vite `plugins` so it loads before Cloudflare resolves its config. ```ts cloudflare(withOxide()) cloudflare(withOxide({ root: import.meta.dirname })) cloudflare(withOxide({ config: (c) => { … } })) ```
withOxide
()),
], });

Point root wrangler.jsonc main at a thin entry that re-exports virtual:oxide/worker. Wrap Cloudflare options with withOxide from oxidejs/wrangler so scanned workflow() / queue() / schedule() bindings merge and the Worker uses Vite’s ssr environment (or call mergeDurableBindings yourself inside config). Durable bindings are not oxide options — they merge through withOxide.

celld

When Cloudflare Vite writes dist/ssr/wrangler.json, every production oxide build also runs prepareCelldDeploy: it writes dist/wrangler.json with celld-safe paths (main: "celld/entry.js", assets.directory: "client"), strips .assetsignore and Cloudflare-only keys (no_bundle, workers_dev, …), writes a stripped entry under dist/celld/ (outside ssr/, so Cloudflare deploy does not upload a second Worker module), and merges project .dev.vars into vars. Cloudflare wrangler deploy still uses the Vite SSR snapshot; celld deploy dist uses the rewritten root config. Call prepareCelldDeploy("dist") yourself only outside a normal oxide build. Helpers toCelldWrangler / writeCelldWrangler live on oxidejs/wrangler.

workerEntry

The default is src/server.ts. When that file is present, Oxide imports it as your server. Missing the default path is fine — actions and static assets still run. An explicit path that does not exist fails at config resolve. See Server entry.

import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite";

function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({ OxidejsOptions.workerEntry?: string | undefined
Path to server entry, relative to project root. Default: `"src/server.ts"`. When the default path is missing, the entry is skipped (actions / assets only). An explicit path that does not exist fails at build time.
workerEntry
: "src/api.ts" });

outDir

The default is dist. On "fetch", Oxide writes server.js and any client build under this folder. With "worker", Cloudflare’s plugin owns the output layout.

clientDir

The default is client, producing dist/client/ on "fetch". It must stay inside outDir; Oxide rejects paths that escape the output folder.

actions

The short form chooses a transport:

import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite";

function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({ OxidejsOptions.actions?: OxidejsActions | undefined
Transport and path for `*.server.ts` stubs. Default: `"http"` at `/__oxide/action`.
actions
: "http" }); // default

The object form also configures the URL path, same-origin protection, and OpenRPC discovery:

import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite";

function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({
  OxidejsOptions.actions?: OxidejsActions | undefined
Transport and path for `*.server.ts` stubs. Default: `"http"` at `/__oxide/action`.
actions
: {
transport?: OxidejsActionTransport | undefinedtransport: "http", path?: string | undefined
Endpoint path for actions. Default: `/__oxide/action`.
path
: "/__oxide/action",
sameOrigin?: boolean | undefined
Reject cross-origin action requests (CSRF defense). Default: true.
sameOrigin
: true,
openrpc?: boolean | undefined
Serve `GET /__oxide/openrpc` with an OpenRPC 1.3 document for `action()` handlers only. Ignored when `transport` is `"ws"` (discovery is HTTP). Default: false.
openrpc
: true,
}, });
  • transport is "http" by default. "ws" uses WebSocket (crossws on Node, WebSocketPair with preset: "worker").
  • path defaults to /__oxide/action. It must start with / and cannot contain a query string.
  • sameOrigin defaults to true for HTTP and WebSocket. This blocks browser calls from another site, including sibling subdomains.
  • openrpc defaults to false. When true, Oxide serves GET /__oxide/openrpc with an OpenRPC 1.3 document for action() handlers only (payload / success / error from ACTION_META; workflows, queues, and schedules are omitted). Ignored when transport is "ws".

With "worker", the generated Worker calls server.accept() for upgrades. Durable Object apps that need hibernation can pass a custom accept into createWsHooks (for example ctx.acceptWebSocket(ws)). The generated wrapper does not create a DO for you.

Turn sameOrigin off only when cross-origin RPC is intentional. You must then provide the appropriate authentication, CORS, and CSRF policy.

actionHeaders

These static headers are embedded in the shared HTTP action client and are visible in browser code. Use them for public values, not private server credentials.

import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite";

function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({
  OxidejsOptions.actionHeaders?: OxidejsActionHeaders | undefined
Extra headers on the shared HTTP action client. Ignored when `actions` is "ws".
actionHeaders
: { "x-client-version": "1" },
});

For login tokens, prefer cookies with same-origin protection or compute authorization in your own client/transport integration. Functions cannot be embedded in actionHeaders.

middleware

List modules that run before server actions, your server entry, and static files in the generated production handler. Each module must default-export a function. Return a Response to stop the chain, or return undefined to continue.

// src/auth.ts
export default function 
function auth(request: Request, context: {
    env: unknown;
    ctx: unknown;
}): Response | undefined
auth
(
request: Requestrequest: Request,
context: {
    env: unknown;
    ctx: unknown;
}
context
: { env: unknownenv: unknown; ctx: unknownctx: unknown }
) { if (!request: Requestrequest.Request.headers: Headers
The **`headers`** read-only property of the Request interface contains the Headers object associated with the request. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/headers)
headers
.Headers.has(name: string): boolean
The **`has()`** method of the Headers interface returns a boolean stating whether a Headers object contains a certain header. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has)
has
("authorization")) {
return new var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response
The **`Response`** interface of the Fetch API represents the response to a request. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response)
Response
("Unauthorized", { ResponseInit.status?: number | undefinedstatus: 401 });
} }
// vite.config.ts
import { function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
} from "vite";
import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite"; export default function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
({
UserConfig.plugins?: PluginOption[] | undefined
Array of vite plugins to use.
plugins
: [function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({
OxidejsOptions.middleware?: (string | {
    module: string;
    imports?: string[];
})[] | undefined
Module specifiers whose default export is `(request, ctx) => Response | undefined | Promise<Response | undefined>`. Tried in order at the top of the production fetch handler; a Response short-circuits. Dev servers use connect middleware instead.
middleware
: ["./src/auth.ts"] })],
});

Modules run in array order, before the action gate. The second argument contains the runtime’s env and fetch ctx. The same handlers are loaded through the SSR graph in development, so dev and production behave identically.

On WebSocket upgrades, a middleware Response short-circuits (auth 302 / 401 / …). Oxide ignores only @ilha/router/ssr document Responses so upgrades still reach WebSocketPair / celld. On other requests, return a Response to stop the chain or undefined to continue.

Entries can also be objects that carry their own side-effect imports — modules loaded at startup before the handler runs:

// vite.config.ts
import { function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
} from "vite";
import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite"; export default function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
({
UserConfig.plugins?: PluginOption[] | undefined
Array of vite plugins to use.
plugins
: [
function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({
OxidejsOptions.middleware?: (string | {
    module: string;
    imports?: string[];
})[] | undefined
Module specifiers whose default export is `(request, ctx) => Response | undefined | Promise<Response | undefined>`. Tried in order at the top of the production fetch handler; a Response short-circuits. Dev servers use connect middleware instead.
middleware
: [
{ module: stringmodule: "@ilha/router/ssr", imports?: string[] | undefinedimports: ["ilha:pages/server", "ilha:loaders"], }, ], }), ], });

plugins

Build plugins with beforeBuild / afterBuild hooks (once per production build). Pass a plugin object or a module specifier that default-exports one. Relative paths resolve against the project root.

// vite.config.ts
import { function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
} from "vite";
import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite"; export default function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
({
UserConfig.plugins?: PluginOption[] | undefined
Array of vite plugins to use.
plugins
: [
function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({ OxidejsOptions.plugins?: OxidePluginInput[] | undefined
Build plugins with `beforeBuild` / `afterBuild` hooks (production builds only; once per build). Pass an object or a module specifier (default export). Relative specifiers resolve against the Vite/Rsbuild project root.
plugins
: ["./build-plugin.ts"],
}), ], });

Hooks run once per production build (not during vite / vite dev). Specifiers are dynamic-imported and must default-export an OxidePlugin. Celld prepare is built-in when a Cloudflare Vite snapshot exists — see celld — not a plugin.

imports

Module specifiers imported for side effects at the top of the generated server bundle — for virtual modules or integrations that self-register handlers. Also loaded once during development.

// vite.config.ts
import { function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
} from "vite";
import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite"; export default function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
({
UserConfig.plugins?: PluginOption[] | undefined
Array of vite plugins to use.
plugins
: [function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({ OxidejsOptions.imports?: string[] | undefined
Module specifiers imported for side effects at the top of the production server bundle (e.g. virtual modules that self-register handlers).
imports
: ["./src/register-handlers.ts"] })],
});

bodyLimit

Maximum request body size in bytes on Node. Larger requests receive 413 Payload Required. Defaults to 1 MiB.

import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite";

function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({ OxidejsOptions.bodyLimit?: number | undefined
Max request body size in bytes (Node). Larger requests get 413. Default: 1048576 (1 MiB).
bodyLimit
: 4 * 1024 * 1024 });

notFound

Custom HTML served with a 404 status when no action, user fetch, route, or asset handled the request (Node). Defaults to a minimal <h1>404 Not Found</h1>.

import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite";

function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({ OxidejsOptions.notFound?: string | undefined
Custom 404 body (HTML) served when no route, asset, or user fetch handled the request (Node with client assets).
notFound
: "<h1>Page not found</h1>" });

env

Extra values passed as the second argument to fetch(request, env, ctx) on Node. Read them with useEnv().

import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite";

function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({ 
OxidejsOptions.env?: {
    [key: string]: OxidejsJson;
} | undefined
Extra env passed as the second argument to fetch(request, env, ctx) on Node — read it with useEnv().
env
: { type API_ORIGIN: stringAPI_ORIGIN: "https://api.example.com" } });

Was this page helpful?