Quickstart
Install the plugin, write a server (and an action), then run a production build.
Install
npm install -D oxidejspnpm add -D oxidejsyarn add -D oxidejsbun add -D oxidejsnub add -D oxidejsaube add -D oxidejsExtend the TypeScript config:
{ "extends": "oxidejs/tsconfig" }
Add the plugin
Use Vite or Rsbuild. Same plugin, same output tree.
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()],
});Add scripts to package.json:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}import { import defineConfigdefineConfig } from "@rsbuild/core";
import const oxide: (options?: OxidejsOptions | undefined) => anyoxide from "oxidejs/rsbuild";
export default import defineConfigdefineConfig({
plugins: any[]plugins: [function oxide(options?: OxidejsOptions | undefined): anyoxide()],
});Add scripts to package.json:
{
"scripts": {
"dev": "rsbuild dev",
"build": "rsbuild build",
"preview": "rsbuild preview"
}
}Server entry
Handle routes you care about. Return nothing to fall through to static files (and index.html for navigations):
import type { type FetchHandler<Env extends object = { [key: string]: OxidejsJson; }> = (request: Request, env: Env, ctx: ExecutionContext) => FetchResult | Promise<FetchResult>`src/server.ts` fetch handler. The generated wrapper always calls
`fetch(request, env, ctx)` — `env` may be `{}` on Node without the `env` option.
Return `undefined` (or bare `return`) to fall through to assets.FetchHandler } from "oxidejs";
export const const fetch: (request: Request) => Response | undefinedfetch = ((request: Requestrequest) => {
if (new var URL: new (url: string | URL, base?: string | URL) => URLThe **`URL`** interface is used to parse, construct, normalize, and encode URLs. It works by providing properties which allow you to easily read and modify the components of a URL.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/URL)URL(request: Requestrequest.Request.url: stringThe **`url`** read-only property of the Request interface contains the URL of the request.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/url)url).URL.pathname: stringThe **`pathname`** property of the URL interface represents a location in a hierarchical structure. It is a string constructed from a list of path segments, each of which is prefixed by a / character.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname)pathname === "/api/ok") {
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("ok");
}
return;
}) satisfies type FetchHandler<Env extends object = { [key: string]: OxidejsJson; }> = (request: Request, env: Env, ctx: ExecutionContext) => FetchResult | Promise<FetchResult>`src/server.ts` fetch handler. The generated wrapper always calls
`fetch(request, env, ctx)` — `env` may be `{}` on Node without the `env` option.
Return `undefined` (or bare `return`) to fall through to assets.FetchHandler;
A typed action
Put server-only code in *.server.ts. Wrap what the browser may call in action(). Import it from the client with the same module path — Oxide replaces the import with Effect RPC. The implementation never ships to the browser:
import { function action<Args extends unknown[], Y, R = void>(fn: (...args: Args) => AsyncGenerator<Y, R, unknown>, opts?: ActionOptions): StreamActionHandle<Args, Y, R> (+3 overloads)Marks a `*.server.ts` export as a remote RPC action. On the server the
underlying function runs locally; on the client the build replaces the module
with an `Atom.fn`-shaped RPC handle (`set`, `bind`, `result`).
Optional schemas stamp Effect Rpc payload / success / error codecs for the
generated actions module. `withSchema` is sugar for `{ payload }`.action } from "oxidejs";
export const const greet: ServerActionHandle<[name: string], `hello, ${string}`>greet = action<[name: string], Promise<`hello, ${string}`>>(fn: (name: string) => Promise<`hello, ${string}`>, opts?: ActionOptions): ServerActionHandle<[name: string], `hello, ${string}`> (+3 overloads)Marks a `*.server.ts` export as a remote RPC action. On the server the
underlying function runs locally; on the client the build replaces the module
with an `Atom.fn`-shaped RPC handle (`set`, `bind`, `result`).
Optional schemas stamp Effect Rpc payload / success / error codecs for the
generated actions module. `withSchema` is sugar for `{ payload }`.action(async (name: stringname: string) => {
return `hello, ${name: stringname}` as type const = `hello, ${string}`const;
});
// @filename: client.ts
import { const greet: ServerActionHandle<[name: string], `hello, ${string}`>greet } from "./hello.server";
const const message: `hello, ${string}`message = await function greet(...args: [name: string] | [name: string, CallOptions]): Promise<`hello, ${string}`>greet("Ryuz");
Build and run
vite build
node dist/server.js
No index.html → only dist/server.js. With index.html → client assets in dist/client/. Files in public/ land next to those assets.
Common workflows
Cloudflare Workers
Add a root wrangler.jsonc (preset defaults to "worker") and @cloudflare/vite-plugin. Wrap Cloudflare options with withOxide so durable bindings merge.
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: [function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide(), 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())],
});
Production builds that emit dist/ssr/wrangler.json also write a celld-ready dist/wrangler.json. See preset and Worker.
Server only
Leave out index.html. The plugin emits dist/server.js and does not build a client.
Next steps
- Server entry —
src/server.tsand fallthrough - Server actions — Schema, streams, live queries, WebSocket
- Worker — workflows, queues, schedules
- Configuration —
preset, paths, actions, celld