Skip to content

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 ServerContext as 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:

  • serverFunctions is optional overall.
  • If present, you must provide module or endpoint.
  • exports must be non-empty in runtime manifest.
  • Default publish scripts auto-refresh exports from src/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-id
    • x-cdn-loader-feature-id
    • x-cdn-loader-function
    • x-cdn-loader-context (base64url JSON)
    • optionally forwarded auth headers (authorization, x-auth-request-access-token, cookie) unless forwardAuthHeaders: 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

StatusErrorTypical cause
401UnauthorizedMissing/invalid auth in active auth mode
403Function "...\" is not declared...Missing from manifest.serverFunctions.exports
403Function "...\" is not exported...Export missing from server module
404Feature "...\" not foundFeature not loaded/mount mismatch
503Server functions unavailable...No module loaded and no endpoint configured
503RPC execution timed out... / RPC upstream timed outTimeout in module or endpoint mode

Execution modes

Server functions can run in two modes:

ModeConfigBehavior
ModuleserverFunctions.moduleHost imports and executes the module in-process. Timeout protected.
Endpoint proxyserverFunctions.endpointHost 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

  1. Check /_admin and /_health/{featureId} to confirm feature/version loaded.
  2. Confirm serverFunctions.exports contains your function in published manifest.
  3. Confirm function exists in dist-server/server-functions.js.
  4. For endpoint mode, verify upstream route {endpoint}/{fnName} and timeout budget.
  5. 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: [] };
}