15. Server functions
Server functions know who's calling. Importing anything from #ucm/server is also what puts a function on the
server; placement still needs no annotation.
import { session, setSession, clearSession, rateLimit, HttpError, request } from "#ucm/server";
export async function login(name: string, password: string): Promise<string> {
if (!(await checkPassword(name, password))) throw new HttpError(401, "wrong name or password");
rateLimit("login:" + request().ip, { max: 5, per: 60 }); // a 429 with Retry-After beyond that
setSession({ user: name }); // stored server-side; the cookie is just an id
return name;
}
export async function me(): Promise<string> {
const user = session().user; // unknown, expired or revoked sessions come back empty
if (typeof user !== "string") throw new HttpError(401, "sign in first");
return user;
}
The same module provides tables and transactions and jobs.
15.1 Request context
request()gives the method, URL,Headers, cookies, client IP, and anAbortSignalthat fires if the client disconnects. The context follows the call through any depth ofawaits (AsyncLocalStorage).cookie(name),setCookie,deleteCookie. Cookies default toHttpOnlyandSameSite=Lax, and toSecureover TLS.
15.2 Sessions
session(), setSession(data), clearSession() and revokeSessions(match).
- Session data lives server-side in
.ucm/data/sessions.sqlite, apart from the codebase, and the cookie holds only an unguessable id. setSessionissues a fresh id every time, which blocks session fixation.- Sessions can be revoked:
revokeSessions((s) => s.user === "ada")signs Ada out everywhere and returns how many sessions ended.
15.3 Rate limits and errors
rateLimit(key, { max, per })is a sliding window per key. Beyond the limit it throws a 429 withRetry-After.HttpError(status, message)reaches the browser as a rejected promise carryingstatusand the message.- Other errors are hidden in production. Under
ucm startthe browser gets "internal error" and the server logs the full stack, so database errors and paths don't leak.ucm devshows everything.
15.4 Streaming
A server function can be an async function*, and browser code iterates it with for await, as if it were
local:
- chunks arrive as they're produced (newline-delimited JSON over one response);
- the request context, session included, lasts for the whole stream;
breakin the browser aborts the request, which stops the generator on the server.
15.5 Plain HTTP endpoints
ucm start <app> --http app.api (or ucm dev) serves every function under app.api at /api/…, e.g.
app.api.hooks.stripe at /api/hooks/stripe, for webhooks and outside API clients. --http ns=/prefix picks
another prefix.
- A handler takes a standard
Requestand returns aResponse, plain data (sent as JSON) or nothing (204). - Names resolve on each request, so
ucm mvmoves a URL without a restart. - These routes are for other callers, so they skip the CSRF checks. Check signatures (e.g. a webhook secret) in the handler.
15.6 Testing
withRequest(init, fn) runs fn as if answering a request (method, url, headers, cookies, ip), with
sessions in memory, and returns its value plus the Set-Cookie headers it produced. See testing.
built from 5890e54f