Server Functions
Sygnal has no server-function runtime of its own. With Vike, use Telefunc: a function exported from a *.telefunc.js file runs on the server, and importing it on the client gives a stub that calls it over HTTP. In a Sygnal app, the call goes through a driver, so the component stays pure and the reply arrives as a reply action, like an HTTP request.
Install and set up Telefunc for Vike as its documentation describes; this page only shows the Sygnal side. The Telefunc API shown here (*.telefunc.js files, getContext(), Abort(), shield()) follows the Telefunc docs at the time of writing; check them for your version.
A server function
Section titled “A server function”// runs on the server onlyimport { getContext, Abort } from 'telefunc'
export async function onLoadQuote(id) { const { user } = getContext() // whatever your server puts in the context if (!user) throw Abort() // every telefunction is a public endpoint return db.quotes.find(id) // plain JSON data}Calling it through driverFromAsync()
Section titled “Calling it through driverFromAsync()”Register one driver for the page’s functions in a +drivers.js file (Vike loads it on the client only; drivers can’t be set inside +config.js, see Vike). args: ['call', 'args'] calls call(request.call, request.args):
import { driverFromAsync } from 'sygnal'import * as quoteFns from './Quote.telefunc.js'
const call = (name, args = []) => quoteFns[name](...args)
export default { RPC: driverFromAsync(call, { args: ['call', 'args'] }) }The component sends the call and names the reply actions:
export default function Quote({ state }) { return ( <div> <button className="load">Load</button> <p className="text">{state.status === 'loading' ? 'Loading…' : state.error || state.quote?.text}</p> </div> )}Quote.initialState = { id: 1, status: 'idle', quote: null, error: '' }Quote.intent = ({ DOM }) => ({ LOAD: DOM.click('.load') })Quote.model = { LOAD: { STATE: (state) => ({ ...state, status: 'loading', error: '' }), RPC: (state) => ({ call: 'onLoadQuote', args: [state.id], ok: 'LOADED', error: 'FAILED' }), }, LOADED: (state, quote) => ({ ...state, status: 'done', quote }), // what onLoadQuote returned FAILED: (state) => ({ ...state, status: 'error', error: 'Could not load the quote.' }), // data: { error, request }}LOADEDgets the function’s return value;FAILEDgets{ error, request }when the call throws or the network fails.- The reply reaches exactly the instance that called.
driverFromAsynchas nolatest/abort(Custom Drivers shows the request-id technique). - Tests need no server and no driver:
await t.respond('RPC', { text: 'Hi' }, 'LOADED')answers the call, andt.requests('RPC')shows{ call: 'onLoadQuote', args: [1], … }. - Data a page needs for its first render belongs in Vike’s
+data(it is merged intoinitialState), not in a call onBOOTSTRAP.
Security
Section titled “Security”A server function is an HTTP endpoint that anyone can call with any arguments, whatever the client code does. Whether you use Telefunc or expose functions some other way:
- Authorize inside every function. Read the user from an explicit request context (
getContext()in Telefunc) and reject what they may not do. A button the UI hides is not a permission check. - Validate every argument at runtime. Telefunc’s
shield()does this (with TypeScript it can be generated from the types); otherwise use a schema validator (zod, valibot, ArkType). Never trust a type annotation alone. - Keep server code in server-only files (
*.telefunc.js). Don’t define server functions as closures inside client modules, where they can capture and leak values in scope. - Send plain JSON in both directions. Avoid serialisation formats that rebuild objects, references or code on the server.
- Protect against CSRF: require POST with a custom header and check the
Originheader, or rely on a framework that does (check your Telefunc setup and cookieSameSitesettings). - Expose only what you mean to. Export only the functions the client calls, from files you know; don’t route arbitrary names to arbitrary modules (the
callhelper above only reaches the functions of one module). - Return only what the caller may see: select fields instead of returning whole database rows.