﻿# Server Functions

Server Functions are async functions that can be called from the client-side. You don't have to implement API endpoints for these functions to be callable as the client can call these functions like a direct reference to the function itself. Server Functions are usable on forms, buttons, submit inputs, and as props to client components. React and the runtime will manage calling these functions on the server-side.

You can expose any `"use server";` marked function as a Server Function. Server Functions can be called from the client using the `action` prop on a `` element, `formAction` on a `` or `` element, or from a client component passing down the Server Function as a prop to the client component.

> If you use TypeScript, all of your Server Functions are type-safe and you will get type errors if you pass the wrong parameters to a Server Function or if you try to access a property on the response object that doesn't exist.

## Inline Server Functions

You can define Server Functions inline in your components as any other event handler in a React component.

```jsx
export default function App() {
  async function action() {
    "use server";
    console.log("Server Function called!");
  }

  return (
    <form action={action}>
      <button type="submit">Submit</button>
    </form>
  );
}
```

> **Progressive enhancement:** if you have JavaScript enabled, the Server Function will be called using the `fetch` API and the response will be used to update the DOM in a React transition. If you don't have JavaScript enabled, the Server Function will be called using a regular HTTP request.

You can even define your Server Functions inline your JSX.

```jsx
export default function App() {
  return (
    <form
      action={async () => {
        "use server";
        console.log("Server Function called!");
      }}
    >
      <button type="submit">Submit</button>
    </form>
  );
}
```

> Server Functions are able to access all variables in scope, including references to props and any variables available in the scope of the Server Function from the last render of the component as Server Functions are mapped on each render of the server component.

## Server Function modules

If you want to keep your Server Functions in separate modules, you can do so by using the `"use server";` pragma at the top of your module. All exported functions from a Server Function module will be usable as Server Functions.

```js
"use server";

export async function action() {
  console.log("Server Function called!");
}
```

## Server Function parameters

You will get all form data as an object as the first parameter to your Server Function.

```jsx
export default function App() {
  async function action(formData) {
    "use server";
    console.log(`Server Function called by ${formData.get("name")}!`);
  }

  return (
    <form action={action}>
      Your name: <input name="name" />
      <button type="submit">Submit</button>
    </form>
  );
}
```

## Action state

```jsx
import { useActionState } from "@lazarv/react-server/router";

export default function App() {
  async function action(formData) {
    "use server";
    console.log(`Server Function called by ${formData.get("name")}!`);
  }

  const { error } = useActionState(action);

  return (
    <form action={action}>
      Your name: <input name="name" />
      <button type="submit">Submit</button>
      {error && <p>{error.message}</p>}
    </form>
  );
}
```

To access the action state, you can use the `useActionState` hook. The `useActionState` hook takes the Server Function as the first parameter and returns an object with the following properties:

- `formData`: the form data object
- `data`: the data object returned by the Server Function
- `error`: the error object if the action failed
- `actionId`: the action ID of the current action

## Server Functions with client components

You can also pass Server Function references to client components as props and call them from the client component as any other async function.

```jsx
"use client";

export default function MyClientComponent({ action }) {
  const handleClick = () => {
    action({ name: "John" });
  };

  return <button onClick={handleClick}>Click me!</button>;
}
```

```jsx
import MyClientComponent from "./MyClientComponent";

export default function App() {
  async function action({ name }) {
    "use server";
    console.log(`Server Function called by ${name}!`);
  }

  return (
    <div>
      <MyClientComponent action={action} />
    </div>
  );
}
```

## Server Function response to client component calls

Server Functions called from client components can return data to the client component directly. This can be useful if you want to display a message to the user after the Server Function has been completed or if you want to display an error message if the Server Function failed.

```jsx
"use client";

export default function MyClientComponent({ action }) {
  const [response, setResponse] = useState(null);

  const handleClick = async () => {
    const response = await action({ name: "John" });
    setResponse(response);
  };

  return (
    <>
      <button onClick={handleClick}>Click me!</button>
      {response && <p>{response.message}</p>}
    </>
  );
}
```

```jsx
import MyClientComponent from "./MyClientComponent";

export default function App() {
  async function action({ name }) {
    "use server";
    console.log(`Server Function called by ${name}!`);
    return { message: `Hello ${name}!` };
  }

  return (
    <div>
      <MyClientComponent action={action} />
    </div>
  );
}
```

## Inline Server Functions with inline client components

You can combine inline `"use server"` functions and inline `"use client"` components in the same file. This is useful when a Server Function and the client UI that consumes it are closely related.

```jsx
import { useState, useTransition } from "react";

const greeting = "Hello";

async function greet(name) {
  "use server";
  return `${greeting}, ${name}!`;
}

function Greeter() {
  "use client";

  const [message, setMessage] = useState("");
  const [, startTransition] = useTransition();

  return (
    <div>
      <button
        onClick={() =>
          startTransition(async () => {
            setMessage(await greet("World"));
          })
        }
      >
        Greet
      </button>
      {message && <p>{message}</p>}
    </div>
  );
}

export default function App() {
  return <Greeter />;
}
```

Both the Server Function and the client component are extracted into separate modules automatically. The Server Function can capture variables from the module scope, and the client component can call the Server Function directly.

## Returning client components from Server Functions

A Server Function can define an inline `"use client"` component and return it as rendered JSX. The client component will be serialized through the RSC protocol and hydrated on the client, making it fully interactive.

```jsx
import { useState, useTransition } from "react";

async function createCounter(initialCount) {
  "use server";

  function Counter({ start }) {
    "use client";
    const [count, setCount] = useState(start);
    return (
      <div>
        <p>Count: {count}</p>
        <button onClick={() => setCount(count + 1)}>Increment</button>
      </div>
    );
  }

  return <Counter start={initialCount} />;
}

function Shell() {
  "use client";

  const [content, setContent] = useState(null);
  const [, startTransition] = useTransition();

  return (
    <div>
      <button onClick={() =>
        startTransition(async () => setContent(await createCounter(0)))
      }>
        Create Counter
      </button>
      {content}
    </div>
  );
}

export default function App() {
  return <Shell />;
}
```

The `createCounter` Server Function defines a `Counter` client component, renders it with the given props, and returns the element. The framework extracts the nested directives into separate modules through a chain of virtual modules, so everything works from a single file.

## Inline Server Functions in "use client" files

You can define inline `"use server"` functions inside a file that already has a top-level `"use client"` directive. The Server Functions are automatically extracted from the client module, so you can keep related server logic and client UI together in a single file.

```jsx
"use client";

import { useState, useTransition } from "react";

export default function TodoApp() {
  const [items, setItems] = useState([]);
  const [, startTransition] = useTransition();

  async function addItem(text) {
    "use server";
    return { id: Date.now(), text };
  }

  return (
    <div>
      <button
        onClick={() =>
          startTransition(async () => {
            const item = await addItem(`item-${items.length}`);
            setItems((prev) => [...prev, item]);
          })
        }
      >
        Add
      </button>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  );
}
```

The file is treated as a client component because of the top-level `"use client"` directive, but the `addItem` function is extracted into a separate server module. This works the same way as defining the Server Function in a standalone `"use server"` file — the runtime handles the extraction automatically.

## Validation

Server Functions accept arguments from the client. Any payload that flows from a browser into a server handler is, by definition, untrusted. The runtime offers an opt-in API — `createFunction` — that lets you declare a parse/validate contract for each runtime argument slot. The contract is enforced at the protocol layer: the decoder runs your `parse` and `validate` slot-by-slot during the args walk, and rejects the request on the first failure with HTTP 400 — *before* the handler runs, before any business logic touches the value, before any allocation that the handler might assume is bounded.

Bare `"use server"` functions without `createFunction` keep working unchanged. The validation contract is opt-in and additive.

### `createFunction`

Wrap an action handler with `createFunction(spec)(handler)`. Pass the per-slot validate specs as an array — the most common shape:

```jsx
import {
  createFunction,
  formData,
  file,
} from "@lazarv/react-server/function";
import { z } from "zod";

export const updateProfile = createFunction([
  z.string().email(),
  z.string().min(2).max(80),
])(async function updateProfile(email, name) {
  "use server";
  await db.users.update({ email, name });
});
```

Index `i` in the array describes the **runtime arg slot `i`** — what the client puts on the wire at position `i`. It is *not* the handler's signature parameter `i`. Bound captures from `.bind(...)` or inline closures are not part of this contract — they're hidden values, integrity-protected by the AEAD action token, and never validated as user input.

The runtime accepts any [Standard Schema](https://standardschema.dev/) — Zod, Valibot, ArkType — duck-typed via `safeParse` / `assert` / `parse`. You don't import a particular library from `@lazarv/react-server`; bring whichever schema library your project already uses.

### Parse, then validate

When you also need pre-validate parsing, switch to the object form: `createFunction({ validate, parse })`. Both fields are arrays indexed by the same slot. `parse[i]` runs after the value tree is materialized but before `validate[i]` — the right place for type coercion that schemas can't easily express:

```js
export const setLimit = createFunction({
  parse: [(v) => Number(v)],
  validate: [z.number().int().min(1).max(1000)],
})(async function setLimit(limit) {
  "use server";
  await db.config.set({ limit });
});
```

A throwing `parse` surfaces as `DecodeValidationError(reason: "parse_failed")`; a failing `validate` surfaces as `reason: "validate_failed"`. The decoder aborts on the first failure — the next slot is never touched, the handler is never called.

### Skipping slots with `noop`

When only some slots need validation or parsing, use the `noop` export as a placeholder rather than reaching for sparse-array syntax (`[, , schema]`) or explicit `undefined`:

```js
import { createFunction, noop } from "@lazarv/react-server/function";

export const update = createFunction([noop, noop, z.number().int()])(
  async function update(a, b, count) {
    // a: unknown, b: unknown, count: number — slots 0/1 are accepted
    // unvalidated; slot 2 is the only one constrained.
    "use server";
  }
);
```

Same idea in the object form when `parse` and `validate` need different gap patterns:

```js
createFunction({
  parse:    [noop, noop, (v) => Number(v)],
  validate: [z.string(), noop, z.number()],
})(handler);
// handler: (a: string, b: unknown, c: number)
```

`noop` is the identity transform — at runtime it's the same as omitting validation/parsing for that slot. Slots typed `noop` resolve to `unknown` in the inferred handler signature.

### `formData` — file uploads and structured forms

`FormData` arguments are common when uploading files or submitting progressive-enhancement forms. Schema libraries can describe object shapes, but they describe *materialized* values — by the time a schema runs, the file is already buffered. `formData` is wire-aware: the decoder enforces declared keys and per-entry constraints *during* the FormData walk, so size and MIME limits are checked synchronously against `Blob.size` / `Blob.type` before the entry is added to the result.

```js
import {
  createFunction,
  formData,
  file,
} from "@lazarv/react-server/function";

export const upload = createFunction([
  formData({
    title: z.string().min(1).max(120),
    photo: file({
      maxBytes: 5 * 1024 * 1024,
      mime: ["image/png", "image/jpeg"],
    }),
  }),
])(async function upload(form) {
  "use server";
  const title = form.get("title");
  const photo = form.get("photo");
  // photo is already size- and MIME-checked.
});
```

`formData(shape, options?)` takes the entry shape as its first argument and an optional config bag as its second. The shape is required; the only option today is `unknown`, but the slot is reserved for future per-form constraints.

The decoder looks up declared entries by **exact key**. There is no prefix scan, so an attacker-injected entry like `5_role=admin` cannot land in the FormData your handler reads.

The `unknown` policy — passed as `formData(shape, { unknown: "drop" })` — controls what happens to entries that are *not* declared:

- `"reject"` (default, recommended): an unknown entry fails the decode with `DecodeValidationError(reason: "unknown_entry")`. Defends against attacker-injected fields.
- `"drop"`: silently skip undeclared entries. Useful when the form includes React-managed hidden fields the schema doesn't enumerate.
- `"allow"`: copy undeclared entries through unvalidated. Escape hatch — documented as unsafe.

`file({...})` and `blob({...})` accept `maxBytes` (per-entry size cap), `mime` (allowlist of acceptable MIME types), an optional sync `validate(value)` callback for custom checks (e.g. magic-byte detection), and `optional: true` to allow a missing entry. `File.type` is browser-supplied and trivially spoofable — combine MIME with magic-byte detection in `validate` for hard guarantees.

### Other wire types

The Flight protocol carries more than just primitives, objects, and `FormData`. For each remaining wire type where a Standard Schema isn't enough on its own — usually because validation needs to bound resource consumption, type-check against a platform constructor, or wrap an async source — there's a dedicated wire-aware helper:

```js
import {
  createFunction,
  arrayBuffer,
  typedArray,
  map,
  set,
  stream,
  asyncIterable,
  iterable,
  promise,
} from "@lazarv/react-server/function";
```

| Helper | Wire tag | Handler-side type | Why it's wire-aware |
|---|---|---|---|
| `arrayBuffer({ maxBytes })` | `$AB` | `ArrayBuffer` | Byte-length cap before the handler observes the buffer |
| `typedArray({ ctor, maxBytes })` | `$AT` | `InstanceType` (e.g. `Float32Array`) | Constructor allowlist via `instanceof`; type narrowed in inference |
| `map({ maxSize, key, value })` | `$Q` | `Map<K, V>` | Size cap; inner key/value schemas |
| `set({ maxSize, value })` | `$W` | `Set` | Size cap; per-item schema |
| `stream({ maxChunks, maxBytes })` | `$r` / `$b` | `ReadableStream` | Bounds enforced as the handler consumes the stream |
| `asyncIterable({ maxYields, value })` | `$x` | `AsyncIterable` | Yield ceiling + per-yield validation |
| `iterable({ maxYields, value })` | `$X` | `Iterable` | Same, sync flavor |
| `promise(value)` | `$@` | `Promise` | Resolved-value validation through the schema |

A few examples in context:

```js
// Binary upload arriving as a typed array
export const upload = createFunction([
  typedArray({ ctor: Uint8Array, maxBytes: 5 * 1024 * 1024 }),
])(async function upload(bytes) {
  "use server";
  // bytes: Uint8Array, already size-checked
});

// Bounded Map argument
export const lookup = createFunction([
  map({ maxSize: 100, key: z.string(), value: z.number() }),
])(async function lookup(table) {
  "use server";
  // table: Map<string, number>, capped at 100 entries
});

// Streaming upload with a per-stream byte cap
export const ingest = createFunction([
  stream({ maxBytes: 50 * 1024 * 1024, maxChunks: 8192 }),
])(async function ingest(stream) {
  "use server";
  // stream: ReadableStream — wrapped to error if either ceiling is exceeded
  for await (const chunk of stream) { … }
});

// Bounded async iterable with per-yield validation
export const events = createFunction([
  asyncIterable({
    maxYields: 1000,
    value: z.object({ type: z.string(), payload: z.unknown() }),
  }),
])(async function events(stream) {
  "use server";
  // stream: AsyncIterable<{type: string, payload: unknown}>
});
```

Each helper rejects at decode with a distinct `reason` code:

- `wire_shape_mismatch` — slot's wire tag doesn't match the spec (e.g. expected `$AT` got a primitive)
- `max_bytes_exceeded` / `max_size_exceeded` / `max_chunks_exceeded` / `max_yields_exceeded` — resource ceiling tripped
- `validate_failed` — inner Standard Schema rejected a key, value, or yielded item

For streams and iterables, the bound is enforced **as the handler consumes them**, not at decode time. The decoder wraps the materialized stream / iterable; once the handler reads past the ceiling the wrapper errors instead of yielding more data. That matters because Flight materializes stream chunks up-front at decode — the wrap is what lets per-slot bounds tighten beyond the global `maxStreamChunks` ceiling.

`typedArray` takes the actual constructor reference (not a string name): `typedArray({ ctor: Float32Array })` infers as `(samples: Float32Array)` in the handler, and the runtime check is `value instanceof Float32Array`. Pass an array of constructors for a union: `typedArray({ ctor: [Uint8Array, Uint8ClampedArray] })`.

### Dev-time warnings

In development, every Server Function call without a `createFunction` contract logs a one-time warning to the server console:

> Server function `src/actions/upload.mjs#upload` called without validation — wrap the export with `createFunction({...})(handler)` from `@lazarv/react-server/function` (set `config.serverFunctions.strict = false` to silence) 🛡️

The action id format matches what the runtime uses internally (`#`), so you can grep straight to the source. The warning is per-action and per-process: each unwrapped action is named once, no matter how many times it's called.

This is purely a dev guardrail — built/started servers never log it. If you're migrating an existing codebase incrementally and don't want the noise, set `config.serverFunctions.strict = false`:

```js
// react-server.config.mjs
export default {
  serverFunctions: { strict: false },
};
```

### Error handling

When validation fails, the runtime returns HTTP 400 and sets the `x-react-server-action-error` header to the failure reason (`validate_failed`, `parse_failed`, `unknown_entry`, `max_bytes_exceeded`, `mime_not_allowed`, `missing_entry`, `wire_shape_mismatch`, `custom_validate_failed`, `duplicate_entry`). The client receives a generic error — schema diagnostics are not forwarded to avoid leaking expected-shape details that aid attackers. Diagnostics are written to the server log via `logger.warn` for operator visibility.

If you need user-facing error messages tied to specific fields, validate the same payload again *inside* the handler and return a structured error object: that path is yours to design and is what `useActionState` is built around.

## Security

Server Function calls travel a round-trip across the network: the runtime emits a reference to the function, the client invokes it, and the runtime resolves and runs the function on the server. Two things need to be tamper-evident across that round-trip — the *identity* of the action being called, and any *captured values* that travel with it.

### Action identity and bound captures

Every Server Function reference is encoded as a single AES-256-GCM token. The token's plaintext is the pair `(actionId, bound)`, where `bound` is the array of values that were captured by `.bind(...)` or by an inline closure at render time, or `null` for unbound actions. The ciphertext is what the client sees on the wire, and what it sends back when the action is invoked.

```jsx
function ProfilePage({ userId }) {
  return (
    <form
      action={async (formData) => {
        "use server";
        await db.users.update(userId, formData.get("name"));
      }}
    >
      …
    </form>
  );
}
```

In the example above, `userId` is captured by the inline Server Function. The runtime emits a token that bundles both the action's identity and the captured `userId` together. The client never sees `userId` in plaintext, never round-trips it as a separate value, and cannot edit it without invalidating the token's authentication tag — which causes the call to be rejected.

This applies to every form of Server Function:

- Module-scope `"use server"` functions (no captures → bound is `null`)
- Inline closures with render-time captures
- Server-side `.bind(...)` usage to partially apply a Server Function
- Bound server references passed as arguments to other Server Functions

### Configuration

There is one configuration property to set: a stable encryption secret. Without it, the runtime generates an ephemeral key per process — fine for development, but tokens won't survive a restart or be valid across multiple instances of the server.

```js
// react-server.config.mjs
export default {
  serverFunctions: {
    secret: process.env.ACTION_SECRET, // 32-byte hex, or any string (hashed to 32 bytes)
  },
};
```

The key resolves in this order:

1. `REACT_SERVER_FUNCTIONS_SECRET` environment variable
2. `REACT_SERVER_FUNCTIONS_SECRET_FILE` environment variable (path to a file)
3. `serverFunctions.secret` in the runtime config
4. `serverFunctions.secretFile` in the runtime config (path to a file)
5. A random ephemeral key (development fallback only)

### Key rotation

To rotate without invalidating in-flight tokens, list the prior keys under `serverFunctions.previousSecrets` (or `serverFunctions.previousSecretFiles`). Incoming tokens are tried against the primary key first and then each previous key in turn:

```js
export default {
  serverFunctions: {
    secret: process.env.ACTION_SECRET,
    previousSecrets: [process.env.ACTION_SECRET_PREVIOUS],
  },
};
```

The same rotation applies to both action identity and bound captures — they share one key.

### Client-side `.bind()`

When a bound Server Function is exposed to a client component and the client calls `.bind(...)` to add more arguments, those additional arguments are treated as *runtime arguments*, not as new captures. They travel as ordinary call args (alongside whatever the user passes at invocation), and they are not included inside the encrypted token. This is intentional: only the original server-emitted bound is integrity-protected. Client-added arguments are effectively just the arguments the client chooses to send at call time.

### Disabling Server Functions entirely

If your application has no Server Functions at all, set `serverFunctions: false` in the runtime config. The runtime then refuses to decode any action request: incoming `POST`/`PUT`/`PATCH`/`DELETE` traffic is not parsed for action calls, the manifest is not queried, and the request flows through to normal page rendering as if the action endpoints didn't exist. This eliminates the action-dispatch surface as an attack target and removes a small amount of per-request overhead.

```js
// react-server.config.mjs
export default {
  serverFunctions: false,
};
```

In production builds with no `"use server"` modules and no inline Server Functions, the runtime auto-detects an empty manifest and applies the same gate without explicit configuration. The explicit `false` is for cases where you want the gate even before a build (development), or as a deliberate posture choice regardless of what the build sees.

Remote Components rendering can be force-disabled in the same defense-in-depth spirit via `remoteComponents: false` — see [Disabling Remote Components rendering](../features/micro-frontends#disabling-remote-components) on the Micro-frontends page.

### Limitations

The token covers the values in the bound array. If a captured value is a `File` or `Blob`, the token covers the slot reference that points to the binary content, but not the binary content itself. Servers that bind server-constructed binary data into a closure should be aware that an attacker controlling the upload could substitute the binary content even with a valid token. In practice, captured `File`/`Blob` values are rare in Server Function closures.