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

Server Actions

Import a server function from the browser. Same TypeScript types. The implementation never ships to the client.

Write the function once in *.server.ts. Import it from the browser. Oxide turns that import into Effect RPC (HTTP or WebSocket). The module body, database clients, and unmarked helpers stay on the server — they never enter the client graph.

Create an action

Name the file *.server.ts (or .tsx / .js / .jsx). Wrap every remotely callable export in action():

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], {
    message: string;
}>
greet
=
action<[name: string], Promise<{
    message: string;
}>>(fn: (name: string) => Promise<{
    message: string;
}>, opts?: ActionOptions): ServerActionHandle<[name: string], {
    message: 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 { message: stringmessage: `hello, ${name: stringname}` }; });

Call it from the client with the same import path. Types flow from the server module; the runtime call is RPC:

// @filename: client.ts
import { 
const greet: ServerActionHandle<[name: string], {
    message: string;
}>
greet
} from "./greet.server";
const { const message: stringmessage } = await
function greet(...args: [name: string] | [name: string, CallOptions]): Promise<{
    message: string;
}>
greet
("Ryuz");

By default the stub POSTs /__oxide/action as newline-delimited JSON-RPC (application/json-rpc). The method name is <file>.<export> — here greet.greet. A batch() call posts one JSON-RPC 2.0 batch array instead.

Only action() is public

Exports that are not wrapped stay server-local. Secrets and helpers are not callable over the wire:

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";
declare const const API_KEY: stringAPI_KEY: string; const const normalize: (name: string) => stringnormalize = (name: stringname: string) => name: stringname.String.trim(): string
Removes the leading and trailing white space and line terminator characters from a string.
trim
();
export const const saveName: ServerActionHandle<[name: string], string>saveName = action<[name: string], Promise<string>>(fn: (name: string) => Promise<string>, opts?: ActionOptions): ServerActionHandle<[name: string], 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) => {
// API_KEY and normalize never appear in the client stub void const API_KEY: stringAPI_KEY; return const normalize: (name: string) => stringnormalize(name: stringname); }); export const const internalLabel: "not an RPC method"internalLabel = "not an RPC method";

action() does not validate input or authorize users by itself. Treat arguments as untrusted — use withSchema and check the current user before reading or changing private data.

Call shape on the client

Unary actions return a Promise and expose helpers for UI wiring:

Call What it does
await greet(name) Run the action (always invokes RPC on the client)
greet.set(...args) Same as calling with args; also writes the atom
greet.bind(...args) / greet.with(...args) Return an event handler that invokes the action
greet.result Read the last AsyncResult from the client atom
// @filename: bind.ts
import { 
const toggle: ServerActionHandle<[id: string], {
    id: string;
    done: boolean;
}>
toggle
} from "./tasks.server";
var document: Document
**`window.document`** returns a reference to the document contained in the window. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
document
.ParentNode.querySelector<"button">(selectors: "button"): HTMLButtonElement | null (+4 overloads)
Returns the first element that is a descendant of node that matches selectors. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/querySelector)
querySelector
("button")!.GlobalEventHandlers.onclick: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/click_event)
onclick
=
const toggle: ServerActionHandle<[id: string], {
    id: string;
    done: boolean;
}>
toggle
.ServerActionHandle<[id: string], { id: string; done: boolean; }>.bind: (id: string) => (...ev: unknown[]) => voidbind("task-1");

Stream actions do not support bind / with. On the client they resolve to an async generator — await first, then for await.

Batch calls

batch() collects calls made in the same tick and sends them as one JSON-RPC 2.0 batch — a single POST, one round trip:

// @filename: batch.ts
import { function batch<const Items extends readonly BatchItem[]>(items: Items): Promise<BatchResult<Items>> (+1 overload)
Run action calls as one JSON-RPC 2.0 batch request. Items are pending calls (`batch(fetchOne(1), fetchTwo(2))`) or thunks that start one (`batch(() => fetchOne(1), fetchTwo)`); a single array argument is accepted too. Calls must be created in the same tick as `batch()`. Results resolve in call order; a rejected call rejects the returned promise. Without an HTTP transport (`transport: "ws"`) the calls run as usual — one message each — since batching is a JSON-RPC 2.0 HTTP transport feature. On the server (and SSR) the same calls run locally, in parallel.
batch
} from "oxidejs";
import { const ping: ServerActionHandle<[], string>ping,
const task: ServerActionHandle<[id: string], {
    id: string;
}>
task
} from "./tasks.server";
const [const pong: stringpong,
const first: {
    id: string;
}
first
] = await
batch<readonly [ServerActionHandle<[], string>, Promise<{
    id: string;
}>]>(items_0: ServerActionHandle<[], string>, items_1: Promise<{
    id: string;
}>): Promise<[string, {
    id: string;
}]> (+1 overload)
Run action calls as one JSON-RPC 2.0 batch request. Items are pending calls (`batch(fetchOne(1), fetchTwo(2))`) or thunks that start one (`batch(() => fetchOne(1), fetchTwo)`); a single array argument is accepted too. Calls must be created in the same tick as `batch()`. Results resolve in call order; a rejected call rejects the returned promise. Without an HTTP transport (`transport: "ws"`) the calls run as usual — one message each — since batching is a JSON-RPC 2.0 HTTP transport feature. On the server (and SSR) the same calls run locally, in parallel.
batch
(const ping: ServerActionHandle<[], string>ping,
function task(...args: [id: string] | [id: string, CallOptions]): Promise<{
    id: string;
}>
task
("a"));

Items are pending calls (task("a")), thunks that start one (() => task(id)), or a bare handle for a zero-arg action (ping). batch([ping, task("a")]) is the same call. Types flow per item, so first is { id: string }.

Detail Behavior
Wire one POST to /__oxide/action with a JSON-RPC 2.0 batch array
Result values in call order; a failing call rejects the promise
Same tick calls must be created in the batch() tick — an earlier call already went out
Transport HTTP only: transport: "ws" still sends one message per call
Server / SSR the same calls run locally, in parallel
Stream actions not batchable — iterate ticks() directly
{ signal } an aborted call is dropped from the batch; the rest still post
{ idempotencyKey } carried as a per-frame header, so useIdempotencyKey() works

JSON-RPC 2.0 answers a batch as one response set, so the slowest call in a batch sets the latency for all of them. Batch calls that belong together instead of pushing an unbounded list through one request.

Validate input with Effect Schema

Invalid payloads become JSON-RPC Invalid params (-32602) over /__oxide/action, with the Schema message when Effect surfaces it — not a silent bad row in your database:

import { import SchemaSchema } from "effect";
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
, const withSchema: <T, E, R>(schema: Schema.Codec<T, E, never, never>, handler: (payload: T) => R) => (payload: E) => R | Effect<never, SchemaDecodeError>
Decode the action's single payload with Effect Schema before the handler runs. Use inside `action()` so Oxide still recognizes the export: ```ts export const add = action( withSchema(Schema.Struct({ text: Schema.String }), async (payload) => { // payload.text is string }) ) ``` Equivalent to `action(handler, { payload: schema })` plus a local decode for direct server calls. Decode failures are `Effect.fail(SchemaDecodeError)`. Over RPC that becomes JSON-RPC invalid params (`-32602`). Schemas that need decoding services are not supported — use `never` RD.
withSchema
} from "oxidejs";
const
const AddTask: Schema.Struct<{
    readonly text: Schema.String;
}>
AddTask
= import SchemaSchema.
function Struct<{
    readonly text: Schema.String;
}>(fields: {
    readonly text: Schema.String;
}): Schema.Struct<{
    readonly text: Schema.String;
}>
Defines a struct schema from a map of field schemas. **Details** Each field value is a schema. Use {@link optionalKey } or {@link optional } to mark fields as optional, and {@link mutableKey } to mark them as mutable. The resulting schema's `Type` is a readonly object type with the fields' decoded types. The `Encoded` form mirrors the field schemas' encoded types. Declared fields may be inherited and are copied to own properties in the output. The `__proto__` field is accepted only when it is an own property. Parsing does not guarantee that output keys retain their input order. **Example** (Defining a basic struct) ```ts import.meta.vitest import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number, email: Schema.optionalKey(Schema.String) }) // { readonly name: string; readonly age: number; readonly email?: string } type Person = typeof Person.Type Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 }) // => { name: "Alice", age: 30 } ```
@categoryconstructors@since3.10.0
Struct
({
text: Schema.Stringtext: import SchemaSchema.const String: Schema.String
Type-level representation of {@link String } . Schema for `string` values. Validates that the input is `typeof` `"string"`.
@categorymodels@since4.0.0@categoryschemas@since4.0.0
String
,
}); export const
const add: ServerActionHandle<[payload: {
    readonly text: string;
}], Effect<never, SchemaDecodeError, never> | {
    id: `${string}-${string}-${string}-${string}-${string}`;
    text: string;
}>
add
=
action<[payload: {
    readonly text: string;
}], Effect<never, SchemaDecodeError, never> | Promise<{
    id: `${string}-${string}-${string}-${string}-${string}`;
    text: string;
}>>(fn: (payload: {
    readonly text: string;
}) => Effect<never, SchemaDecodeError, never> | Promise<{
    id: `${string}-${string}-${string}-${string}-${string}`;
    text: string;
}>, opts?: ActionOptions): ServerActionHandle<[payload: {
    readonly text: string;
}], Effect<never, SchemaDecodeError, never> | {
    id: `${string}-${string}-${string}-${string}-${string}`;
    text: 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
(
withSchema<{
    readonly text: string;
}, {
    readonly text: string;
}, Promise<{
    id: `${string}-${string}-${string}-${string}-${string}`;
    text: string;
}>>(schema: Schema.Codec<{
    readonly text: string;
}, {
    readonly text: string;
}, never, never>, handler: (payload: {
    readonly text: string;
}) => Promise<{
    id: `${string}-${string}-${string}-${string}-${string}`;
    text: string;
}>): (payload: {
    readonly text: string;
}) => Effect<never, SchemaDecodeError, never> | Promise<{
    id: `${string}-${string}-${string}-${string}-${string}`;
    text: string;
}>
Decode the action's single payload with Effect Schema before the handler runs. Use inside `action()` so Oxide still recognizes the export: ```ts export const add = action( withSchema(Schema.Struct({ text: Schema.String }), async (payload) => { // payload.text is string }) ) ``` Equivalent to `action(handler, { payload: schema })` plus a local decode for direct server calls. Decode failures are `Effect.fail(SchemaDecodeError)`. Over RPC that becomes JSON-RPC invalid params (`-32602`). Schemas that need decoding services are not supported — use `never` RD.
withSchema
(
const AddTask: Schema.Struct<{
    readonly text: Schema.String;
}>
AddTask
, async (
payload: {
    readonly text: string;
}
payload
) => {
// payload.text is string — Encoded was checked on the wire return { id: `${string}-${string}-${string}-${string}-${string}`id: var crypto: Crypto
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/crypto)
crypto
.Crypto.randomUUID(): `${string}-${string}-${string}-${string}-${string}`
The **`randomUUID()`** method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator. Available only in secure contexts. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID)
randomUUID
(), text: stringtext:
payload: {
    readonly text: string;
}
payload
.text: stringtext };
}) );

withSchema is sugar for action(handler, { payload }) plus a local decode for direct server calls. Prefer one Struct payload over many positional args — it matches Effect Rpc and stays easy to evolve. Schemas that need decoding services are not supported yet.

Typed Fail on the wire

Pass an error schema (Schema.TaggedError) so Fail values encode for the client instead of scrubbing to a generic Internal error:

import { import EffectEffect, import SchemaSchema } from "effect";
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";
class class NotFoundNotFound extends import SchemaSchema.
const TaggedError: <NotFound, {}>(identifier?: string) => {
    <Tag, Fields>(tag: Tag, fields: Fields, annotations?: Schema.Annotations.Declaration<NotFound, readonly [Schema.TaggedStruct<Tag, Fields>]> | undefined): Schema.Class<NotFound, Schema.TaggedStruct<Tag, Fields>, YieldableError>;
    <Tag, S>(tag: Tag, schema: S, annotations?: Schema.Annotations.Declaration<NotFound, readonly [Schema.Struct<{ [K in keyof ({
        readonly _tag: Schema.tag<Tag>;
    } & S["fields"])]: ({
        readonly _tag: Schema.tag<Tag>;
    } & S["fields"])[K]; }>]> | undefined): Schema.Class<...>;
}
Defines a schema-backed yieldable error class with an automatically populated `_tag` field. **When to use** Use to define typed errors that are schema validated, yielded in `Effect.gen`, and matched as tagged union members. **Example** (Defining a tagged error class) ```ts import.meta.vitest import { Effect, Schema } from "effect" class NotFound extends Schema.TaggedError<NotFound>()("NotFound", { id: Schema.Number }) {} const program = Effect.gen(function*() { yield* new NotFound({ id: 42 }) }) const error = await Effect.runPromise(Effect.flip(program)) error._tag // => "NotFound" error.id // => 42 ```
@categoryconstructors@since3.10.0
TaggedError
<class NotFoundNotFound>()("NotFound", {
id: Schema.Stringid: import SchemaSchema.const String: Schema.String
Type-level representation of {@link String } . Schema for `string` values. Validates that the input is `typeof` `"string"`.
@categorymodels@since4.0.0@categoryschemas@since4.0.0
String
,
}) {} export const const getTask: ServerActionHandle<[id: string], never>getTask = action<[id: string], never, NotFound, never>(fn: (id: string) => Effect.Effect<never, NotFound, never>, opts?: ActionOptions): ServerActionHandle<[id: string], never> (+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
(
(id: stringid: string) => import EffectEffect.const gen: <Effect.Effect<never, NotFound, never>, never>(f: () => Generator<Effect.Effect<never, NotFound, never>, never, never>) => Effect.Effect<never, NotFound, never> (+1 overload)
Provides a way to write effectful code using generator functions, simplifying control flow and error handling. **When to use** Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions. Generator functions work similarly to `async/await` but keep errors, requirements, and interruption in the Effect type. You can `yield*` values from effects and return the final result at the end. **Example** (Sequencing effects with generators) ```ts import.meta.vitest import { Data, Effect } from "effect" class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} const addServiceCharge = (amount: number) => amount + 1 const applyDiscount = ( total: number, discountRate: number ): Effect.Effect<number, DiscountRateError> => discountRate === 0 ? Effect.fail(new DiscountRateError()) : Effect.succeed(total - (total * discountRate) / 100) const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) const fetchDiscountRate = Effect.promise(() => Promise.resolve(5)) export const program = Effect.gen(function*() { const transactionAmount = yield* fetchTransactionAmount const discountRate = yield* fetchDiscountRate const discountedAmount = yield* applyDiscount( transactionAmount, discountRate ) const finalAmount = addServiceCharge(discountedAmount) return `Final amount to charge: ${finalAmount}` }) await Effect.runPromise(program) // => "Final amount to charge: 96" ```
@categoryconstructors@since2.0.0
gen
(function* () {
return yield* import EffectEffect.const fail: <NotFound>(error: NotFound) => Effect.Effect<never, NotFound, never>
Creates an `Effect` that represents a recoverable error. **When to use** Use to explicitly signal a recoverable error in an `Effect`. **Details** The error keeps propagating unless it is handled. You can handle tagged errors with functions like {@link catchTag } or {@link catchTags } . **Example** (Creating a failed effect) ```ts import.meta.vitest import { Data, Effect } from "effect" class OperationFailedError extends Data.TaggedError("OperationFailedError")<{}> {} // ┌─── Effect<never, OperationFailedError, never> // ▼ const failure = Effect.fail( new OperationFailedError() ) Effect.runSync(Effect.flip(failure))._tag // => "OperationFailedError" ```
@see{@link succeed} to create an effect that represents a successful value.@categoryconstructors@since2.0.0
fail
(new
constructor NotFound(props: {
    readonly id: string;
    readonly _tag?: "NotFound" | undefined;
}, options?: Schema.MakeOptions | undefined): NotFound
NotFound
({ id: stringid }));
}), { ActionOptions.error?: Schema.Codec<unknown, unknown, never, never> | undefinederror: class NotFoundNotFound } );

Clients see application error -32000 with the tag/message. Returning an Error as a value does not promote it over RPC — use Effect.fail or throw the tagged error.

Effect handlers and services

Prefer Effect handlers (Effect.gen, Schema-stamped payload / error, services). action() also accepts Promise, async function*, and (with { stream: true } or a returned Stream) Effect Stream. Yield OxideRequest / OxideCtx for the same data as useRequest() / useCtx():

import { import EffectEffect } from "effect";
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
, class OxideRequest
Effect service for the inbound `Request` (same value as `useRequest()`).
OxideRequest
} from "oxidejs";
export const const who: ServerActionHandle<[], string | null>who = action<[], string | null, never, OxideRequest>(fn: () => Effect.Effect<string | null, never, OxideRequest>, opts?: ActionOptions): ServerActionHandle<[], string | null> (+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
(() =>
import EffectEffect.const gen: <Effect.Effect<Request, never, OxideRequest>, string | null>(f: () => Generator<Effect.Effect<Request, never, OxideRequest>, string | null, never>) => Effect.Effect<string | null, never, OxideRequest> (+1 overload)
Provides a way to write effectful code using generator functions, simplifying control flow and error handling. **When to use** Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions. Generator functions work similarly to `async/await` but keep errors, requirements, and interruption in the Effect type. You can `yield*` values from effects and return the final result at the end. **Example** (Sequencing effects with generators) ```ts import.meta.vitest import { Data, Effect } from "effect" class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} const addServiceCharge = (amount: number) => amount + 1 const applyDiscount = ( total: number, discountRate: number ): Effect.Effect<number, DiscountRateError> => discountRate === 0 ? Effect.fail(new DiscountRateError()) : Effect.succeed(total - (total * discountRate) / 100) const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) const fetchDiscountRate = Effect.promise(() => Promise.resolve(5)) export const program = Effect.gen(function*() { const transactionAmount = yield* fetchTransactionAmount const discountRate = yield* fetchDiscountRate const discountedAmount = yield* applyDiscount( transactionAmount, discountRate ) const finalAmount = addServiceCharge(discountedAmount) return `Final amount to charge: ${finalAmount}` }) await Effect.runPromise(program) // => "Final amount to charge: 96" ```
@categoryconstructors@since2.0.0
gen
(function* () {
const const req: Requestreq = yield* class OxideRequest
Effect service for the inbound `Request` (same value as `useRequest()`).
OxideRequest
;
return const req: Requestreq.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.get(name: string): string | null
The **`get()`** method of the Headers interface returns a byte string of all the values of a header within a Headers object with a given name. If the requested header doesn't exist in the Headers object, it returns null. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get)
get
("x-user");
}) );

Handlers run under an oxidejs.action span annotated with rpc.method. Provide oxideRuntimeLayer() (or your own Logger / Tracer Layer) at the host if you want spans collected.

Authorization without leaking user to the client

Keep auth on the server. The client calls profile() with no user argument — your wrapper loads the session and passes user only into the handler:

// @filename: profile.server.ts
import { import EffectEffect, import SchemaSchema } from "effect";
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
, class OxideRequest
Effect service for the inbound `Request` (same value as `useRequest()`).
OxideRequest
} from "oxidejs";
import {
const db: {
    users: {
        findByToken(token: string): Promise<User | undefined>;
    };
    profiles: {
        find(userId: string): Promise<{
            name: string;
        } | undefined>;
        update(userId: string, input: {
            name: string;
        }): Promise<void>;
    };
}
db
, type
type User = {
    id: string;
    role: "user" | "admin";
}
User
} from "./db";
class class UnauthorizedErrorUnauthorizedError extends import SchemaSchema.
const TaggedError: <UnauthorizedError, {}>(identifier?: string) => {
    <Tag, Fields>(tag: Tag, fields: Fields, annotations?: Schema.Annotations.Declaration<UnauthorizedError, readonly [Schema.TaggedStruct<Tag, Fields>]> | undefined): Schema.Class<UnauthorizedError, Schema.TaggedStruct<Tag, Fields>, YieldableError>;
    <Tag, S>(tag: Tag, schema: S, annotations?: Schema.Annotations.Declaration<UnauthorizedError, readonly [...]> | undefined): Schema.Class<...>;
}
Defines a schema-backed yieldable error class with an automatically populated `_tag` field. **When to use** Use to define typed errors that are schema validated, yielded in `Effect.gen`, and matched as tagged union members. **Example** (Defining a tagged error class) ```ts import.meta.vitest import { Effect, Schema } from "effect" class NotFound extends Schema.TaggedError<NotFound>()("NotFound", { id: Schema.Number }) {} const program = Effect.gen(function*() { yield* new NotFound({ id: 42 }) }) const error = await Effect.runPromise(Effect.flip(program)) error._tag // => "NotFound" error.id // => 42 ```
@categoryconstructors@since3.10.0
TaggedError
<class UnauthorizedErrorUnauthorizedError>()(
"UnauthorizedError", { message: Schema.Stringmessage: import SchemaSchema.const String: Schema.String
Type-level representation of {@link String } . Schema for `string` values. Validates that the input is `typeof` `"string"`.
@categorymodels@since4.0.0@categoryschemas@since4.0.0
String
,
} ) {} const const requireUser: Effect.Effect<User, UnauthorizedError, OxideRequest>requireUser = import EffectEffect.const gen: <Effect.Effect<Request, never, OxideRequest> | Effect.Effect<never, UnauthorizedError, never> | Effect.Effect<User | undefined, never, never>, User>(f: () => Generator<Effect.Effect<Request, never, OxideRequest> | Effect.Effect<never, UnauthorizedError, never> | Effect.Effect<User | undefined, never, never>, User, never>) => Effect.Effect<User, UnauthorizedError, OxideRequest> (+1 overload)
Provides a way to write effectful code using generator functions, simplifying control flow and error handling. **When to use** Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions. Generator functions work similarly to `async/await` but keep errors, requirements, and interruption in the Effect type. You can `yield*` values from effects and return the final result at the end. **Example** (Sequencing effects with generators) ```ts import.meta.vitest import { Data, Effect } from "effect" class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} const addServiceCharge = (amount: number) => amount + 1 const applyDiscount = ( total: number, discountRate: number ): Effect.Effect<number, DiscountRateError> => discountRate === 0 ? Effect.fail(new DiscountRateError()) : Effect.succeed(total - (total * discountRate) / 100) const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) const fetchDiscountRate = Effect.promise(() => Promise.resolve(5)) export const program = Effect.gen(function*() { const transactionAmount = yield* fetchTransactionAmount const discountRate = yield* fetchDiscountRate const discountedAmount = yield* applyDiscount( transactionAmount, discountRate ) const finalAmount = addServiceCharge(discountedAmount) return `Final amount to charge: ${finalAmount}` }) await Effect.runPromise(program) // => "Final amount to charge: 96" ```
@categoryconstructors@since2.0.0
gen
(function* () {
const const request: Requestrequest = yield* class OxideRequest
Effect service for the inbound `Request` (same value as `useRequest()`).
OxideRequest
;
const const authorization: string | nullauthorization = const 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.get(name: string): string | null
The **`get()`** method of the Headers interface returns a byte string of all the values of a header within a Headers object with a given name. If the requested header doesn't exist in the Headers object, it returns null. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get)
get
("authorization");
if (!const authorization: string | nullauthorization) { return yield* import EffectEffect.const fail: <UnauthorizedError>(error: UnauthorizedError) => Effect.Effect<never, UnauthorizedError, never>
Creates an `Effect` that represents a recoverable error. **When to use** Use to explicitly signal a recoverable error in an `Effect`. **Details** The error keeps propagating unless it is handled. You can handle tagged errors with functions like {@link catchTag } or {@link catchTags } . **Example** (Creating a failed effect) ```ts import.meta.vitest import { Data, Effect } from "effect" class OperationFailedError extends Data.TaggedError("OperationFailedError")<{}> {} // ┌─── Effect<never, OperationFailedError, never> // ▼ const failure = Effect.fail( new OperationFailedError() ) Effect.runSync(Effect.flip(failure))._tag // => "OperationFailedError" ```
@see{@link succeed} to create an effect that represents a successful value.@categoryconstructors@since2.0.0
fail
(
new
constructor UnauthorizedError(props: {
    readonly message: string;
    readonly _tag?: "UnauthorizedError" | undefined;
}, options?: Schema.MakeOptions | undefined): UnauthorizedError
UnauthorizedError
({ message: stringmessage: "Unauthorized" })
); } const const user: User | undefineduser = yield* import EffectEffect.const promise: <User | undefined>(evaluate: (signal: AbortSignal) => PromiseLike<User | undefined>) => Effect.Effect<User | undefined, never, never>
Creates an `Effect` that represents an asynchronous computation guaranteed to succeed. **When to use** Use to convert a `Promise` into an `Effect` when the async operation is guaranteed to succeed and will not reject. **Details** An optional `AbortSignal` can be provided to allow for interruption of the wrapped `Promise` API. **Gotchas** The `Promise` must not reject. If it rejects, the rejection is treated as a defect, not as a typed failure. Use `tryPromise` when rejection is expected. Interruption aborts the provided `AbortSignal`, but the underlying asynchronous operation only stops if it observes that signal. **Example** (Wrapping a non-rejecting Promise) ```ts import.meta.vitest import { Effect } from "effect" const succeedAsync = (message: string) => Effect.promise<string>(() => Promise.resolve(message)) // ┌─── Effect<string, never, never> // ▼ const program = succeedAsync("Async operation completed successfully!") await Effect.runPromise(program) // => "Async operation completed successfully!" ```
@see{@link tryPromise} for a version that can handle failures.@categoryconstructors@since2.0.0
promise
(() =>
const db: {
    users: {
        findByToken(token: string): Promise<User | undefined>;
    };
    profiles: {
        find(userId: string): Promise<{
            name: string;
        } | undefined>;
        update(userId: string, input: {
            name: string;
        }): Promise<void>;
    };
}
db
.
users: {
    findByToken(token: string): Promise<User | undefined>;
}
users
.function findByToken(token: string): Promise<User | undefined>findByToken(const authorization: stringauthorization)
); if (!const user: User | undefineduser) { return yield* import EffectEffect.const fail: <UnauthorizedError>(error: UnauthorizedError) => Effect.Effect<never, UnauthorizedError, never>
Creates an `Effect` that represents a recoverable error. **When to use** Use to explicitly signal a recoverable error in an `Effect`. **Details** The error keeps propagating unless it is handled. You can handle tagged errors with functions like {@link catchTag } or {@link catchTags } . **Example** (Creating a failed effect) ```ts import.meta.vitest import { Data, Effect } from "effect" class OperationFailedError extends Data.TaggedError("OperationFailedError")<{}> {} // ┌─── Effect<never, OperationFailedError, never> // ▼ const failure = Effect.fail( new OperationFailedError() ) Effect.runSync(Effect.flip(failure))._tag // => "OperationFailedError" ```
@see{@link succeed} to create an effect that represents a successful value.@categoryconstructors@since2.0.0
fail
(
new
constructor UnauthorizedError(props: {
    readonly message: string;
    readonly _tag?: "UnauthorizedError" | undefined;
}, options?: Schema.MakeOptions | undefined): UnauthorizedError
UnauthorizedError
({ message: stringmessage: "Unauthorized" })
); } return const user: Useruser; }); function function withUser<Args extends unknown[], A, E>(handler: (user: User, ...args: Args) => Effect.Effect<A, E>): (...args: Args) => Effect.Effect<A, UnauthorizedError | E, OxideRequest>withUser<function (type parameter) Args in withUser<Args extends unknown[], A, E>(handler: (user: User, ...args: Args) => Effect.Effect<A, E>): (...args: Args) => Effect.Effect<A, UnauthorizedError | E, OxideRequest>Args extends unknown[], function (type parameter) A in withUser<Args extends unknown[], A, E>(handler: (user: User, ...args: Args) => Effect.Effect<A, E>): (...args: Args) => Effect.Effect<A, UnauthorizedError | E, OxideRequest>A, function (type parameter) E in withUser<Args extends unknown[], A, E>(handler: (user: User, ...args: Args) => Effect.Effect<A, E>): (...args: Args) => Effect.Effect<A, UnauthorizedError | E, OxideRequest>E>( handler: (user: User, ...args: Args) => Effect.Effect<A, E>handler: (user: Useruser:
type User = {
    id: string;
    role: "user" | "admin";
}
User
, ...args: Args extends unknown[]args: function (type parameter) Args in withUser<Args extends unknown[], A, E>(handler: (user: User, ...args: Args) => Effect.Effect<A, E>): (...args: Args) => Effect.Effect<A, UnauthorizedError | E, OxideRequest>Args) => import EffectEffect.interface Effect<out A, out E = never, out R = never>
The `Effect` interface defines a value that lazily describes a workflow or job. The workflow requires some context `R`, and may fail with an error of type `E`, or succeed with a value of type `A`. **When to use** Use when you need to represent a lazy, composable workflow that can require services, fail with a typed error, or succeed with a typed value. **Details** `Effect` values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability. To run an `Effect` value, you need a `Runtime`, which is a type that is capable of executing `Effect` values.
@categorymodels@since2.0.0
Effect
<function (type parameter) A in withUser<Args extends unknown[], A, E>(handler: (user: User, ...args: Args) => Effect.Effect<A, E>): (...args: Args) => Effect.Effect<A, UnauthorizedError | E, OxideRequest>A, function (type parameter) E in withUser<Args extends unknown[], A, E>(handler: (user: User, ...args: Args) => Effect.Effect<A, E>): (...args: Args) => Effect.Effect<A, UnauthorizedError | E, OxideRequest>E>
) { return (...args: Args extends unknown[]args: function (type parameter) Args in withUser<Args extends unknown[], A, E>(handler: (user: User, ...args: Args) => Effect.Effect<A, E>): (...args: Args) => Effect.Effect<A, UnauthorizedError | E, OxideRequest>Args) => import EffectEffect.const gen: <Effect.Effect<User, UnauthorizedError, OxideRequest> | Effect.Effect<A, E, never>, A>(f: () => Generator<Effect.Effect<User, UnauthorizedError, OxideRequest> | Effect.Effect<A, E, never>, A, never>) => Effect.Effect<A, UnauthorizedError | E, OxideRequest> (+1 overload)
Provides a way to write effectful code using generator functions, simplifying control flow and error handling. **When to use** Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions. Generator functions work similarly to `async/await` but keep errors, requirements, and interruption in the Effect type. You can `yield*` values from effects and return the final result at the end. **Example** (Sequencing effects with generators) ```ts import.meta.vitest import { Data, Effect } from "effect" class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} const addServiceCharge = (amount: number) => amount + 1 const applyDiscount = ( total: number, discountRate: number ): Effect.Effect<number, DiscountRateError> => discountRate === 0 ? Effect.fail(new DiscountRateError()) : Effect.succeed(total - (total * discountRate) / 100) const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) const fetchDiscountRate = Effect.promise(() => Promise.resolve(5)) export const program = Effect.gen(function*() { const transactionAmount = yield* fetchTransactionAmount const discountRate = yield* fetchDiscountRate const discountedAmount = yield* applyDiscount( transactionAmount, discountRate ) const finalAmount = addServiceCharge(discountedAmount) return `Final amount to charge: ${finalAmount}` }) await Effect.runPromise(program) // => "Final amount to charge: 96" ```
@categoryconstructors@since2.0.0
gen
(function* () {
const const user: Useruser = yield* const requireUser: Effect.Effect<User, UnauthorizedError, OxideRequest>requireUser; return yield* handler: (user: User, ...args: Args) => Effect.Effect<A, E>handler(const user: Useruser, ...args: Args extends unknown[]args); }); } export const
const profile: ServerActionHandle<[], {
    name: string;
} | undefined>
profile
=
action<[], {
    name: string;
} | undefined, UnauthorizedError, OxideRequest>(fn: () => Effect.Effect<{
    name: string;
} | undefined, UnauthorizedError, OxideRequest>, opts?: ActionOptions): ServerActionHandle<[], {
    name: string;
} | undefined> (+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
(
function withUser<[], {
    name: string;
} | undefined, never>(handler: (user: User) => Effect.Effect<{
    name: string;
} | undefined, never, never>): (...args: []) => Effect.Effect<{
    name: string;
} | undefined, UnauthorizedError, OxideRequest>
withUser
((user: Useruser) => import EffectEffect.
const promise: <{
    name: string;
} | undefined>(evaluate: (signal: AbortSignal) => PromiseLike<{
    name: string;
} | undefined>) => Effect.Effect<{
    name: string;
} | undefined, never, never>
Creates an `Effect` that represents an asynchronous computation guaranteed to succeed. **When to use** Use to convert a `Promise` into an `Effect` when the async operation is guaranteed to succeed and will not reject. **Details** An optional `AbortSignal` can be provided to allow for interruption of the wrapped `Promise` API. **Gotchas** The `Promise` must not reject. If it rejects, the rejection is treated as a defect, not as a typed failure. Use `tryPromise` when rejection is expected. Interruption aborts the provided `AbortSignal`, but the underlying asynchronous operation only stops if it observes that signal. **Example** (Wrapping a non-rejecting Promise) ```ts import.meta.vitest import { Effect } from "effect" const succeedAsync = (message: string) => Effect.promise<string>(() => Promise.resolve(message)) // ┌─── Effect<string, never, never> // ▼ const program = succeedAsync("Async operation completed successfully!") await Effect.runPromise(program) // => "Async operation completed successfully!" ```
@see{@link tryPromise} for a version that can handle failures.@categoryconstructors@since2.0.0
promise
(() =>
const db: {
    users: {
        findByToken(token: string): Promise<User | undefined>;
    };
    profiles: {
        find(userId: string): Promise<{
            name: string;
        } | undefined>;
        update(userId: string, input: {
            name: string;
        }): Promise<void>;
    };
}
db
.
profiles: {
    find(userId: string): Promise<{
        name: string;
    } | undefined>;
    update(userId: string, input: {
        name: string;
    }): Promise<void>;
}
profiles
.
function find(userId: string): Promise<{
    name: string;
} | undefined>
find
(user: Useruser.id: stringid))),
{ ActionOptions.error?: Schema.Codec<unknown, unknown, never, never> | undefinederror: class UnauthorizedErrorUnauthorizedError } ); export const const update: ServerActionHandle<[name: string], void>update = action<[name: string], void, UnauthorizedError, OxideRequest>(fn: (name: string) => Effect.Effect<void, UnauthorizedError, OxideRequest>, opts?: ActionOptions): ServerActionHandle<[name: string], void> (+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
(
function withUser<[name: string], void, never>(handler: (user: User, name: string) => Effect.Effect<void, never, never>): (...args: [name: string]) => Effect.Effect<void, UnauthorizedError, OxideRequest>withUser((user: Useruser, name: stringname: string) => import EffectEffect.const promise: <void>(evaluate: (signal: AbortSignal) => PromiseLike<void>) => Effect.Effect<void, never, never>
Creates an `Effect` that represents an asynchronous computation guaranteed to succeed. **When to use** Use to convert a `Promise` into an `Effect` when the async operation is guaranteed to succeed and will not reject. **Details** An optional `AbortSignal` can be provided to allow for interruption of the wrapped `Promise` API. **Gotchas** The `Promise` must not reject. If it rejects, the rejection is treated as a defect, not as a typed failure. Use `tryPromise` when rejection is expected. Interruption aborts the provided `AbortSignal`, but the underlying asynchronous operation only stops if it observes that signal. **Example** (Wrapping a non-rejecting Promise) ```ts import.meta.vitest import { Effect } from "effect" const succeedAsync = (message: string) => Effect.promise<string>(() => Promise.resolve(message)) // ┌─── Effect<string, never, never> // ▼ const program = succeedAsync("Async operation completed successfully!") await Effect.runPromise(program) // => "Async operation completed successfully!" ```
@see{@link tryPromise} for a version that can handle failures.@categoryconstructors@since2.0.0
promise
(() =>
const db: {
    users: {
        findByToken(token: string): Promise<User | undefined>;
    };
    profiles: {
        find(userId: string): Promise<{
            name: string;
        } | undefined>;
        update(userId: string, input: {
            name: string;
        }): Promise<void>;
    };
}
db
.
profiles: {
    find(userId: string): Promise<{
        name: string;
    } | undefined>;
    update(userId: string, input: {
        name: string;
    }): Promise<void>;
}
profiles
.
function update(userId: string, input: {
    name: string;
}): Promise<void>
update
(user: Useruser.id: stringid, { name: stringname }))
), { ActionOptions.error?: Schema.Codec<unknown, unknown, never, never> | undefinederror: class UnauthorizedErrorUnauthorizedError } );

Client input stays typed (name: string on update). Session and DB access stay off the wire. Do not return tokens or credentials from an action.

Stream values

Wrap an async function* in action() to stream over Effect RPC as newline-delimited JSON-RPC (not SSE). Frames scrub as they flush — clients see early chunks without waiting for the generator to finish:

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 ticks: StreamActionHandle<[count: number], number, void>ticks = action<[count: number], number, void>(fn: (count: number) => AsyncGenerator<number, void, unknown>, opts?: ActionOptions): StreamActionHandle<[count: number], number, void> (+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 function* (count: numbercount: number) {
for (let let i: numberi = 0; let i: numberi < count: numbercount; let i: numberi++) yield let i: numberi; });
// @filename: client-stream.ts
import { const ticks: StreamActionHandle<[count: number], number, void>ticks } from "./ticks.server";

for await (const const value: numbervalue of await function ticks(...args: [count: number] | [count: number, CallOptions]): AsyncGenerator<number, void, undefined>ticks(3)) {
  var console: Consoleconsole.Console.log(...data: any[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(const value: numbervalue);
}

On the server, ticks(3) is an async generator. On the client, await the call first, then iterate. Breaking the loop (or return()) cleans up the server generator. Over HTTP, Oxide does not reconnect a dropped stream. With transport: "ws", transient closes retry; abort via { signal } does not.

Live queries

Push successive snapshots for one topic: subscribe is the query, mutate/publish is the write. The hub is isolate-local Effect PubSub — your database remains the source of truth:

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
, const liveQuery: <T>(options: LiveQueryOptions) => LiveQuery<T>
Isolate-local sliding hub: query = subscription, mutation = publish.
liveQuery
} from "oxidejs";
type
type Task = {
    id: string;
    text: string;
}
Task
= { id: stringid: string; text: stringtext: string };
const const tasks: LiveQuery<Task[]>tasks = liveQuery<Task[]>(options: LiveQueryOptions): LiveQuery<Task[]>
Isolate-local sliding hub: query = subscription, mutation = publish.
liveQuery
<
type Task = {
    id: string;
    text: string;
}
Task
[]>({ LiveQueryOptions.topic: string
Isolate-local topic key (`"tasks"`, `"room:42"`).
topic
: "tasks" });
export const const list: StreamActionHandle<[], Task[], void>list = action<[], Task[], void>(fn: () => AsyncGenerator<Task[], void, unknown>, opts?: ActionOptions): StreamActionHandle<[], Task[], void> (+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
(
const tasks: LiveQuery<Task[]>tasks.LiveQuery<Task[]>.subscribe: (seed?: () => Promise<void>) => () => AsyncGenerator<Task[], void, unknown>
Returns an async generator factory for `action()`. Optional `seed` runs once before subscribing (capture `useEnv()` first).
subscribe
(async () => {
// Capture request-scoped handles before the first await on Workers. await const tasks: LiveQuery<Task[]>tasks.LiveQuery<Task[]>.mutate: (fn: () => Promise<Task[]>) => Promise<Task[]>
Serialize Promise work that produces a snapshot, then publish it.
mutate
(async () => [] as
type Task = {
    id: string;
    text: string;
}
Task
[]);
}) ); export const const add: ServerActionHandle<[text: string], void>add = action<[text: string], Promise<void>>(fn: (text: string) => Promise<void>, opts?: ActionOptions): ServerActionHandle<[text: string], void> (+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 (text: stringtext: string) => {
await const tasks: LiveQuery<Task[]>tasks.LiveQuery<Task[]>.mutate: (fn: () => Promise<Task[]>) => Promise<Task[]>
Serialize Promise work that produces a snapshot, then publish it.
mutate
(async () => [{ id: stringid: var crypto: Crypto
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/crypto)
crypto
.Crypto.randomUUID(): `${string}-${string}-${string}-${string}-${string}`
The **`randomUUID()`** method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator. Available only in secure contexts. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID)
randomUUID
(), text: stringtext }]);
});

Under SSR, *.server.ts keeps the real generator (no WebSocket) so you can take the first snapshot for first paint. On the client, the same import resumes over actions: "ws". Transient socket errors retry inside the stream client.

Cancel a call

Pass { signal } and optional { idempotencyKey } as the final CallOptions argument. Inside the action, read the non-optional signal from useRequest().signal:

// @filename: abort.ts
import { const ping: ServerActionHandle<[], "pong">ping, const ticks: StreamActionHandle<[count: number], number, void>ticks } from "./test.server";

const const controller: AbortControllercontroller = new var AbortController: new () => AbortController
The **`AbortController`** interface represents a controller object that allows you to abort one or more Web requests as and when desired. [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController)
AbortController
();
const const stream: AsyncGenerator<number, void, undefined>stream = await function ticks(...args: [count: number] | [count: number, CallOptions]): AsyncGenerator<number, void, undefined>ticks(10, { CallOptions.signal?: AbortSignal | undefinedsignal: const controller: AbortControllercontroller.AbortController.signal: AbortSignal
The **`signal`** read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired. [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/signal)
signal
});
await function ping(...args: [] | [CallOptions]): Promise<"pong">ping({ CallOptions.signal?: AbortSignal | undefinedsignal: const controller: AbortControllercontroller.AbortController.signal: AbortSignal
The **`signal`** read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired. [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/signal)
signal
});
const controller: AbortControllercontroller.AbortController.abort(reason?: any): void
The **`abort()`** method of the AbortController interface aborts an asynchronous operation before it has completed. This is able to abort fetch requests, the consumption of any response bodies, or streams. [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/abort)
abort
();
void const stream: AsyncGenerator<number, void, undefined>stream;

idempotencyKey is sent as x-oxide-idempotency-key and available via useIdempotencyKey(). Pair with durable dedupe (for example paranorm once()) when the mutation queue replays.

Mutation queue (client)

Optional write queue for transport: "ws". Transient socket failures enqueue; flush (and online) drain FIFO. The same action() types — no second client API:

// @filename: queue.ts
import { const createMutationQueue: (options?: CreateMutationQueueOptions) => MutationQueue
Client-only write queue for `actions: "ws"`. Enqueues on transient socket / network failures; drains FIFO on `flush` with Effect Schedule backoff. Optimistic UI is app-owned — listen via `subscribe`.
createMutationQueue
} from "oxidejs/mutation-queue";
import {
const add: ServerActionHandle<[text: string], {
    id: string;
    text: string;
}>
add
} from "./tasks.server";
const const queue: MutationQueuequeue = function createMutationQueue(options?: CreateMutationQueueOptions): MutationQueue
Client-only write queue for `actions: "ws"`. Enqueues on transient socket / network failures; drains FIFO on `flush` with Effect Schedule backoff. Optimistic UI is app-owned — listen via `subscribe`.
createMutationQueue
();
const
const addQueued: (text: string) => Promise<{
    id: string;
    text: string;
}>
addQueued
= const queue: MutationQueuequeue.
MutationQueue.wrap: <[text: string], {
    id: string;
    text: string;
}>(fn: (text: string) => Promise<{
    id: string;
    text: string;
}>, options?: WrapMutationOptions<[text: string]> | undefined) => (text: string) => Promise<{
    id: string;
    text: string;
}>
wrap
(
const add: ServerActionHandle<[text: string], {
    id: string;
    text: string;
}>
add
, {
WrapMutationOptions<[text: string]>.idempotencyKey?: ((text: string) => string) | undefined
Stable id per logical write. Default: new UUID per call.
idempotencyKey
: (text: stringtext) => `add:${text: stringtext}`,
}); await
const addQueued: (text: string) => Promise<{
    id: string;
    text: string;
}>
addQueued
("Buy milk");

Optimistic UI is app-owned — listen with queue.subscribe. HTTP apps can ignore this module.

Request context

useRequest()

The inbound Request in actions, SSR, and frame renders:

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
, const useRequest: () => Request
Current server `Request`. Available in actions, SSR, and frame renders.
useRequest
} from "oxidejs";
export const const who: ServerActionHandle<[], string | null>who = action<[], Promise<string | null>>(fn: () => Promise<string | null>, opts?: ActionOptions): ServerActionHandle<[], string | null> (+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 () => {
return function useRequest(): Request
Current server `Request`. Available in actions, SSR, and frame renders.
useRequest
().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.get(name: string): string | null
The **`get()`** method of the Headers interface returns a byte string of all the values of a header within a Headers object with a given name. If the requested header doesn't exist in the Headers object, it returns null. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get)
get
("x-user");
});

useCtx()

{ req } plus fields your middleware stamped:

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
, const useCtx: <C extends ActionContext = ActionContext>() => C
Current RPC or host request context. Throws outside request handling.
useCtx
} from "oxidejs";
export const const me: ServerActionHandle<[], string | undefined>me = action<[], Promise<string | undefined>>(fn: () => Promise<string | undefined>, opts?: ActionOptions): ServerActionHandle<[], string | undefined> (+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 () => {
return
useCtx<{
    req: Request;
    user?: string;
}>(): {
    req: Request;
    user?: string;
}
Current RPC or host request context. Throws outside request handling.
useCtx
<{ req: Requestreq: Request; user?: string | undefineduser?: string }>().user?: string | undefineduser;
});

Stamp host bindings (D1, KV, …) before actions and WebSocket upgrade:

import { const stampRequestContext: (request: Request, extra: Partial<ActionContext>) => void
Merge fields into the request's fetch stamp (middleware, before actions / WS).
stampRequestContext
} from "oxidejs";
declare class class D1DatabaseD1Database {} export default function
function db(request: Request, { env }: {
    env?: {
        DB?: D1Database;
    };
}): void
db
(
request: Requestrequest: Request, {
env: {
    DB?: D1Database;
} | undefined
env
}: {
env?: {
    DB?: D1Database;
} | undefined
env
?: { DB?: D1Database | undefinedDB?: class D1DatabaseD1Database } }
) { if (
env: {
    DB?: D1Database;
} | undefined
env
?.DB?: D1Database | undefinedDB) {
function stampRequestContext(request: Request, extra: Partial<ActionContext>): void
Merge fields into the request's fetch stamp (middleware, before actions / WS).
stampRequestContext
(request: Requestrequest, { db: D1Databasedb:
env: {
    DB?: D1Database;
}
env
.DB?: D1DatabaseDB });
} }

Read them with useCtx().db (or a small useDb() helper).

useEnv() and useFetchCtx()

On preset: "worker", these are the Worker env and ctx from fetch(request, env, ctx). On Node they are undefined unless you pass env:

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
,
const useEnv: <E = {
    [key: string]: OxidejsJson;
}>() => E | undefined
Worker `env` from `fetch(request, env, ctx)`. `undefined` on Node.
useEnv
, const useFetchCtx: () => ExecutionContext | undefined
Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). `undefined` on Node.
useFetchCtx
} from "oxidejs";
type
type Env = {
    API_URL: string;
}
Env
= { type API_URL: stringAPI_URL: string };
export const const hasApi: ServerActionHandle<[], boolean>hasApi = action<[], Promise<boolean>>(fn: () => Promise<boolean>, opts?: ActionOptions): ServerActionHandle<[], boolean> (+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 () => {
function useFetchCtx(): ExecutionContext | undefined
Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). `undefined` on Node.
useFetchCtx
()?.ExecutionContext.waitUntil?: ((promise: PromiseLike<OxidejsJson | object | null | undefined>) => void) | undefinedwaitUntil?.(var Promise: PromiseConstructor
Represents the completion of an asynchronous operation
Promise
.PromiseConstructor.resolve<null>(value: null): Promise<null> (+2 overloads)
Creates a new resolved promise for the provided value.
@paramvalue A promise.@returnsA promise whose internal state matches the provided promise.
resolve
(null));
return
var Boolean: BooleanConstructor
<string>(value?: string | undefined) => boolean
Boolean
(useEnv<Env>(): Env | undefined
Worker `env` from `fetch(request, env, ctx)`. `undefined` on Node.
useEnv
<
type Env = {
    API_URL: string;
}
Env
>()?.type API_URL: string | undefinedAPI_URL);
});

Anything an action returns is sent to the caller — do not return secrets.

Transport and path

HTTP is the default. Endpoint /__oxide/action, same-origin protection on:

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
: {
path?: string | undefined
Endpoint path for actions. Default: `/__oxide/action`.
path
: "/rpc",
sameOrigin?: boolean | undefined
Reject cross-origin action requests (CSRF defense). Default: true.
sameOrigin
: true,
}, });

Use transport: "ws" for live queries and the mutation queue. Node/fetch needs crossws; preset: "worker" uses WebSocketPair:

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: "ws", path?: string | undefined
Endpoint path for actions. Default: `/__oxide/action`.
path
: "/rpc" },
});

Set actions.openrpc: true to serve GET /__oxide/openrpc — an OpenRPC 1.3 document for action() handlers only (Effect Schema → JSON Schema; workflows, queues, and schedules are omitted). OpenRPC is HTTP-only and stays off when transport is "ws". See actions.

Set sameOrigin: false only when you accept another origin and own CORS, CSRF, and authentication. For static client headers, see actionHeaders — never put a private secret there.

Was this page helpful?