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(): stringRemoves 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 }
```Struct({
text: Schema.Stringtext: import SchemaSchema.const String: Schema.StringType-level representation of
{@link
String
}
.
Schema for `string` values. Validates that the input is `typeof` `"string"`.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
```TaggedError<class NotFoundNotFound>()("NotFound", {
id: Schema.Stringid: import SchemaSchema.const String: Schema.StringType-level representation of
{@link
String
}
.
Schema for `string` values. Validates that the input is `typeof` `"string"`.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"
```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"
```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 OxideRequestEffect 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"
```gen(function* () {
const const req: Requestreq = yield* class OxideRequestEffect service for the inbound `Request` (same value as `useRequest()`).OxideRequest;
return const req: Requestreq.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.get(name: string): string | nullThe **`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 OxideRequestEffect 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
```TaggedError<class UnauthorizedErrorUnauthorizedError>()(
"UnauthorizedError",
{
message: Schema.Stringmessage: import SchemaSchema.const String: Schema.StringType-level representation of
{@link
String
}
.
Schema for `string` values. Validates that the input is `typeof` `"string"`.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"
```gen(function* () {
const const request: Requestrequest = yield* class OxideRequestEffect service for the inbound `Request` (same value as `useRequest()`).OxideRequest;
const const authorization: string | nullauthorization = const 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.get(name: string): string | nullThe **`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"
```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!"
```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"
```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.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"
```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!"
```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!"
```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[]): voidThe **`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: stringIsolate-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 () => AbortControllerThe **`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: AbortSignalThe **`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: AbortSignalThe **`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): voidThe **`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) => MutationQueueClient-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): MutationQueueClient-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) | undefinedStable 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: () => RequestCurrent 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(): RequestCurrent server `Request`. Available in actions, SSR, and frame renders.useRequest().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.get(name: string): string | nullThe **`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>() => CCurrent 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>) => voidMerge 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>): voidMerge 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 | undefinedWorker `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 | undefinedWorker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). `undefined` on Node.useFetchCtx()?.ExecutionContext.waitUntil?: ((promise: PromiseLike<OxidejsJson | object | null | undefined>) => void) | undefinedwaitUntil?.(var Promise: PromiseConstructorRepresents the completion of an asynchronous operationPromise.PromiseConstructor.resolve<null>(value: null): Promise<null> (+2 overloads)Creates a new resolved promise for the provided value.resolve(null));
return var Boolean: BooleanConstructor
<string>(value?: string | undefined) => boolean
Boolean(useEnv<Env>(): Env | undefinedWorker `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 | undefinedTransport and path for `*.server.ts` stubs. Default: `"http"` at `/__oxide/action`.actions: {
path?: string | undefinedEndpoint path for actions. Default: `/__oxide/action`.path: "/rpc",
sameOrigin?: boolean | undefinedReject 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 | undefinedTransport and path for `*.server.ts` stubs. Default: `"http"` at `/__oxide/action`.actions: { transport?: OxidejsActionTransport | undefinedtransport: "ws", path?: string | undefinedEndpoint 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.