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:
wrangler.jsonc,wrangler.toml, orwrangler.jsonpresent →"worker"- Otherwise →
"fetch" - Pass
preset: "worker"orpreset: "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.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[] | undefinedArray 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 | undefinedTransport 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.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 | undefinedPath 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 | undefinedTransport 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 | undefinedTransport and path for `*.server.ts` stubs. Default: `"http"` at `/__oxide/action`.actions: {
transport?: OxidejsActionTransport | undefinedtransport: "http",
path?: string | undefinedEndpoint path for actions. Default: `/__oxide/action`.path: "/__oxide/action",
sameOrigin?: boolean | undefinedReject cross-origin action requests (CSRF defense). Default: true.sameOrigin: true,
openrpc?: boolean | undefinedServe `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,
},
});
transportis"http"by default."ws"uses WebSocket (crosswson Node,WebSocketPairwithpreset: "worker").pathdefaults to/__oxide/action. It must start with/and cannot contain a query string.sameOrigindefaults totruefor HTTP and WebSocket. This blocks browser calls from another site, including sibling subdomains.openrpcdefaults tofalse. Whentrue, Oxide servesGET /__oxide/openrpcwith an OpenRPC 1.3 document foraction()handlers only (payload / success / error fromACTION_META; workflows, queues, and schedules are omitted). Ignored whentransportis"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 | undefinedExtra 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: HeadersThe **`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): booleanThe **`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) => ResponseThe **`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[] | undefinedArray 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[] | undefinedArray 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[] | undefinedArray of vite plugins to use.plugins: [
function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({
OxidejsOptions.plugins?: OxidePluginInput[] | undefinedBuild 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[] | undefinedArray of vite plugins to use.plugins: [function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({ OxidejsOptions.imports?: string[] | undefinedModule 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 | undefinedMax 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 | undefinedCustom 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" } });