Server functions
import { session, setSession, clearSession, rateLimit, HttpError, request } from "#codebase/app/lib/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;
}
Server functions know who's calling through the server library: tcm lib add tcm server mounts it at
<root>.lib.server. Using any of it but HttpError puts a function on the server, with no annotation. The same
library has tables and transactions and jobs.
Request context
request()gives themethod,url,headers(Headers),cookies, clientip(as the socket sees it) and asignalthat aborts if the client disconnects. It works at any depth ofawaits. Server components get the same context, when fetched and when rendering a page's first paint (React).cookie(name),setCookie(name, value, opts?),deleteCookie(name). Cookies default toHttpOnlyandSameSite=Lax, and toSecureover TLS. Options:maxAge,path,httpOnly,secure,sameSite.
Sessions
session(), setSession(data, maxAge?), clearSession() and revokeSessions(match).
- Session data lives server-side in
.tcm/data/sessions.sqlite, apart from the codebase. The cookie holds only an unguessable id. setSessionlastsmaxAgeseconds (30 days by default) and issues a fresh id every time, which blocks session fixation.revokeSessions((s) => s.user === "ada")signs Ada out everywhere and returns how many sessions ended.
Rate limits and errors
rateLimit(key, { max, per })is a sliding window per key (perin seconds). Beyondmaxit 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
tcm startthe browser gets "internal error" and the server logs the full stack, so database errors and paths don't leak.tcm devshows everything.
Streaming
A server function can be an async function*. 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 the whole stream;
breakin the browser aborts the request, which stops the generator on the server.
Plain HTTP endpoints
tcm start <app> --http app.api (or tcm dev) serves every function under app.api at /api/…: app.api.hooks.stripe
at /api/hooks/stripe. Use it 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
tcm mvmoves a URL without a restart. - These routes skip the CSRF checks. Check signatures (a webhook secret, say) in the handler.
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 (testing).