Server functions
Export a named async function from a .server.ts file, then import it into a client component. renku replaces that browser import with a Cap’n Web RPC stub. Worker imports remain ordinary local calls. Try Auto, WebSocket, then Auto again to see connection reuse.
On this page
Enter 1–80 characters.
The .server.ts suffix is the boundary. Export async functions directly; type-only exports are allowed. Default exports, re-exports, runtime constants and use client are rejected. Every reachable exported function becomes remotely callable: validate arguments and authorize each operation.
TypeScript types are not runtime validation. The compiler excludes the module implementation and its dependencies from browser JavaScript and source maps.
import { RpcError, serverContext } from "renku/server";
export async function greet(name: string) {
if (typeof name !== "string" || !name.trim() || name.length > 80)
throw new RpcError("NAME_INVALID", "Enter a name between 1 and 80 characters.");
return { message: "Hello, " + name.trim() + "!", transport: serverContext().transport };
}Add this component to an existing page. Calls use same-origin cookies and HTTP batching by default. Ordinary functions share a data-only contract across both transports: plain objects, arrays and supported scalar values, including Date, bigint and Uint8Array.
Functions and capability objects require the explicit session contract below. Use an action to publish results after await; it becomes inert when the component is disposed.
"use client";
import { state, action } from "renku/client";
import { greet } from "./greeting.server.ts";
export default function Greeting() {
const name = state("Ada"), message = state("Ready");
const publish = action((text: string) => message.set(text));
async function send() {
try { publish((await greet(name.get())).message); }
catch (error) { publish(error instanceof Error ? error.message : "Call failed."); }
}
return @{<section>
<label>Name<input bindValue={name}/></label>
<button onClick={send}>Greet</button><output>{message.get()}</output>
</section>};
}createRpcSession accepts http, websocket or auto. HTTP owns one batch: issue the graph before awaiting its result and close it with using. WebSocket opens lazily on the first call and lasts until close, failure or the five-minute deadline.
Auto uses an already-open same-build WebSocket or falls back to HTTP; it freezes that choice for an explicit graph. There is no speculative upgrade or automatic retry. Direct imported calls also use Auto. Bind exposes promised arguments and fields for pipelining without await.
"use client";
import { createRpcSession } from "renku/rpc";
import { greet } from "./greeting.server.ts";
export async function greetTwice() {
using batch = createRpcSession({ transport: "http" });
const remote = batch.bind(greet);
const first = remote("Ada");
// Pass a promised field without awaiting: one HTTP request for this graph.
return await remote(first.message);
}Start the .server.ts file with use session for callbacks or RpcTarget results, and bind it to an explicit WebSocket session. Every public method on a returned RpcTarget is a capability exposed to its recipient; keep implementation state private. Ordinary direct calls and HTTP reject these exports.
Capture authorized resources in the target, recheck revocation when required, and dispose returned stubs with using. The example creates a fresh counter for each button click.
"use session";
import { RpcTarget } from "renku/server";
class Counter extends RpcTarget {
#value = 0;
async increment() { return ++this.#value; }
}
export async function openCounter() { return new Counter(); }Session construction and bind are lazy, so they are safe during component setup on the server. Start calls in browser handlers or onMount. Register onCleanup to close the session; closing also breaks outstanding capabilities.
Call resetRpc from renku/rpc before login, logout or switching accounts: it closes connections and invalidates existing session handles. WebSocket request headers represent the opening handshake, not refreshed cookies.
After a network failure, dispose the old session and let an explicit user action create a new one; an uncertain mutation may already have committed.
"use client";
import { state, action, onCleanup } from "renku/client";
import { createRpcSession } from "renku/rpc";
import { openCounter } from "./counter.server.ts";
export default function RemoteCounter() {
const session = createRpcSession({ transport: "websocket" });
const open = session.bind(openCounter); // Lazy: safe during server setup.
const value = state("Ready");
const publish = action((text: string) => value.set(text));
onCleanup(() => session.close());
async function increment() {
try {
using counter = await open();
publish(String(await counter.increment()));
} catch (error) { publish(error instanceof Error ? error.message : "Call failed."); }
}
return @{<section><button onClick={increment}>Open and increment</button><output>{value.get()}</output></section>};
}serverContext<Env>() from renku/server supplies request, env, signal and transport in Worker requests and RPC operations. Pass signal to cooperative work. Local imports execute directly without remote validation or error wrapping.
RpcError exposes a deliberate public code and message; other exceptions become RPC_INTERNAL with no server message or stack. Remote failures are Error objects whose message contains that public code, not local RpcError instances.
Authorization is app policy: the generated endpoint runs before a custom Worker's fetch handler, so authorize inside server functions rather than relying on fetch middleware.
The endpoint /_renku/rpc checks the build version and same-origin browser requests, requires JSON for HTTP and an Origin header for WebSocket, and disables HTTP caching. Stale pages must reload.
- Limits are 256 KiB per protocol message, 4 MiB per direction per batch/session, 128 HTTP messages, 32 concurrent root function calls, 30 seconds per HTTP batch and five minutes per WebSocket. Data validation additionally bounds depth, nodes and estimated payload size.
- Returned capability methods share session byte and time budgets; their workload and concurrency are app policy. Signals cancel cooperative work, not completed mutations. All builds add
nodejs_alsfor request context; RPC builds emit an inspection manifest at .cloudflare/output/v0/rpc.json. - Apps without reachable server exports do not include this runtime.
You can call authorized server functions and choose a data-only or explicit capability session.