Appearance
Server Functions
Server functions let features expose backend behavior through a uniform host RPC route. The host exposes them at POST /rpc/{featureId}/{functionName}.
RPC at a glance
- Browser always calls host RPC endpoint:
POST /rpc/{featureId}/{fnName} - Host injects
ServerContextas first argument - Execution happens in one of two modes:
- Module mode: host executes feature module in-process
- Endpoint proxy mode: host forwards call to feature-owned backend
- Authorization logic is team-owned inside feature/backend logic
Declaring server functions
js
// src/actions.server.js
/**
* Every server function receives a ServerContext as the first argument.
* Client-provided arguments follow.
*/
export async function getData(ctx) {
console.log(`Request ${ctx.requestId} from ${ctx.featureId}`);
console.log(`Claims subject: ${ctx.claims?.sub ?? 'anonymous'}`);
return { items: ['a', 'b', 'c'] };
}
export async function saveData(ctx, id, payload) {
if (!ctx.claims?.sub) {
throw new Error('Forbidden');
}
return { saved: true, id };
}Manifest requirements for RPC
json
{
"serverFunctions": {
"module": "server-functions.js",
"exports": ["getData", "saveData"]
}
}Rules:
serverFunctionsis optional overall.- If present, you must provide
moduleorendpoint. exportsmust be non-empty in runtime manifest.- Default publish scripts auto-refresh
exportsfromsrc/actions.server.js. - For remote CDN module mode, active release entry must include
serverFunctionsSha256.
ServerContext
Every server function receives this context as its first argument:
ts
interface ServerContext {
requestId: string; // Unique request trace ID
featureId: string; // Your feature ID
userRoles: string[]; // Merged from all auth sources
claims: object | null; // Raw JWT claims (null if unauthenticated)
visibility: {
featureToggles: string[];
tenantId: string | null;
};
}Calling from the client
Preferred pattern: import .server.js directly
When using @favn/vite-plugin-server-fns, client builds rewrite .server.js imports into RPC proxies automatically:
js
// src/pages/Reports.jsx
import { getData, saveData } from '../actions.server.js';
const rows = await getData();
await saveData('abc-123', { title: 'Updated' });No manual fetch boilerplate is needed.
Explicit fetch pattern
Use this when you want full control over request/response handling:
js
async function callServerFn(featureId, fnName, ...args) {
const res = await fetch(`/rpc/${featureId}/${fnName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ args }),
});
const data = await res.json();
if (!res.ok) {
const message = typeof data?.error === 'string' ? data.error : `RPC failed: ${res.status}`;
throw new Error(message);
}
return data;
}
// Usage
const stats = await callServerFn('feature-my-feature', 'getData');Host request/response contract
Request to host RPC endpoint
- Method:
POST - Path:
/rpc/{featureId}/{fnName} - Body:
{ "args": [...] } - Content-Type:
application/json
Module mode response
- Success: host returns JSON body produced by the function
- Failure: host returns
{ "error": "...", "requestId": "..." }with status
Endpoint proxy mode forwarding contract
When serverFunctions.endpoint is configured, host forwards to:
- URL:
{endpoint}/{fnName} - Method:
POST - Body:
{ "args": [...], "context": { ...ServerContext } } - Headers:
x-request-idx-cdn-loader-feature-idx-cdn-loader-functionx-cdn-loader-context(base64url JSON)- optionally forwarded auth headers (
authorization,x-auth-request-access-token,cookie) unlessforwardAuthHeaders: false
Example backend handler (Hono):
ts
app.post('/rpc/getData', async (c) => {
const body = await c.req.json();
const args = Array.isArray(body?.args) ? body.args : [];
const context = body?.context ?? null;
return c.json({ items: ['a', 'b'], requestId: context?.requestId, argCount: args.length });
});Authorization
Authorization is owned by each feature's server code. The host authenticates and injects identity context; authorization decisions happen in your code:
js
export async function saveData(ctx, payload) {
const allowed = Boolean(ctx.claims?.sub);
if (!allowed) {
const err = new Error('Forbidden');
err.statusCode = 403;
throw err;
}
return { saved: true };
}The framework authenticates and transports identity context; your feature code or backend decides policy.
Common RPC errors
| Status | Error | Typical cause |
|---|---|---|
401 | Unauthorized | Missing/invalid auth in active auth mode |
403 | Function "...\" is not declared... | Missing from manifest.serverFunctions.exports |
403 | Function "...\" is not exported... | Export missing from server module |
404 | Feature "...\" not found | Feature not loaded/mount mismatch |
503 | Server functions unavailable... | No module loaded and no endpoint configured |
503 | RPC execution timed out... / RPC upstream timed out | Timeout in module or endpoint mode |
Execution modes
Server functions can run in two modes:
| Mode | Config | Behavior |
|---|---|---|
| Module | serverFunctions.module | Host imports and executes the module in-process. Timeout protected. |
| Endpoint proxy | serverFunctions.endpoint | Host forwards the RPC call to your backend endpoint. |
For remote CDN features, module mode requires a serverFunctionsSha256 digest match from the signed releases.json.
Debugging checklist
- Check
/_adminand/_health/{featureId}to confirm feature/version loaded. - Confirm
serverFunctions.exportscontains your function in published manifest. - Confirm function exists in
dist-server/server-functions.js. - For endpoint mode, verify upstream route
{endpoint}/{fnName}and timeout budget. - Reproduce with a direct call:
bash
curl -i -X POST http://localhost:3000/rpc/feature-my-feature/getData \
-H 'content-type: application/json' \
--data '{"args":[]}'TypeScript support
ts
import type { ServerContext } from '@favn/feature-sdk';
export async function getData(ctx: ServerContext): Promise<{ items: string[] }> {
return { items: [] };
}