35. Server API reference
Everything server code can import from the server library: the tcm library server, which tcm ships, so
tcm lib add tcm server mounts the release your tcm has at <root>.lib.server, offline (libraries;
it's tcmts.server in tcm's codebase). Using any of it makes a function run on the server (see web apps),
except HttpError and the types, which run anywhere. For how these fit together, see the guides:
server functions, data, background jobs. It runs on tcm's server host,
#tcm/host, one per process, so every copy of the library in an app shares its data, jobs and request.
import { request, session, setSession, table, enqueue, HttpError } from "#codebase/app/lib/server";
(#tcm/server, the module tcm generated for this API before, is the library now: an import of it says so. Versions
of definitions in a codebase's history that imported it, or #tcm/react, still travel: push and pull check them by
re-hashing as they were.)
35.1 Requests
function request(): ServerRequest;
interface ServerRequest {
method: string;
url: string;
headers: Headers;
cookies: Record<string, string>;
ip: string | undefined; // the client's address, as the socket sees it
signal: AbortSignal; // aborted when the client disconnects
}
The request the current server function is answering. It's available anywhere in the call, through any number of
awaits. Outside a request (a job, a test without withRequest) it throws.
35.2 Cookies
function cookie(name: string): string | undefined;
function setCookie(name: string, value: string, opts?: CookieOptions): void;
function deleteCookie(name: string, opts?: { path?: string }): void;
interface CookieOptions {
maxAge?: number; // seconds
path?: string;
httpOnly?: boolean; // default true: not readable by browser scripts
secure?: boolean; // default true when served over TLS
sameSite?: "Strict" | "Lax" | "None"; // default "Lax"
}
35.3 Sessions
function session(): Record<string, unknown>;
function setSession(data: Record<string, unknown>, maxAge?: number): void;
function clearSession(): void;
function revokeSessions(match: (data: Record<string, unknown>) => boolean): number;
session()is the caller's session data: empty if they have none, or it expired or was revoked.setSession(data)starts a new session (default 30 days). The data stays on the server (.tcm/data/sessions.sqlite); the cookie holds only a fresh, unguessable id, so a new id on every sign-in prevents session fixation.revokeSessions(match)ends every session whose data matches, e.g.revokeSessions((s) => s.user === "ada")signs Ada out everywhere. Returns how many ended.
35.4 Errors and limits
class HttpError extends Error {
constructor(status: number, message: string, opts?: { retryAfter?: number });
readonly status: number;
readonly retryAfter?: number; // seconds, sent as Retry-After
}
function rateLimit(key: string, opts: { max: number; per: number }): void;
- Throw an
HttpErrorto fail a call on purpose: the browser's promise rejects with itsstatusandmessage. Any other error reaches the browser as "internal error" in production (it's logged on the server), and in full undertcm dev. rateLimit(key, { max, per })allowsmaxcalls perperseconds forkey(e.g."login:" + request().ip), and throws a 429 withRetry-Afterbeyond that.
35.5 Tables
function table<T>(name: string, opts?: TableOptions<T>): Table<T>;
interface Table<T> {
readonly name: string;
insert(value: T): Promise<string>; // returns the new row's id
put(id: string, value: T): Promise<void>; // store under id, replacing what was there
get(id: string): Promise<T | undefined>;
delete(id: string): Promise<boolean>;
update(id: string, fn: (current: T) => T): Promise<T | undefined>; // atomic read-modify-write
find(where?: Partial<T>, opts?: FindOptions<T>): Promise<{ id: string; value: T }[]>;
findOne(where?: Partial<T>, opts?: Omit<FindOptions<T>, "limit">): Promise<{ id: string; value: T } | undefined>;
count(where?: Partial<T>): Promise<number>;
}
interface TableOptions<T> {
migrate?: ((old: any) => T | Promise<T>) | readonly ((old: any) => unknown)[];
unique?: readonly (keyof T & string)[];
index?: readonly (keyof T & string)[];
renamedFrom?: string;
}
interface FindOptions<T> {
orderBy?: keyof T & string;
desc?: boolean;
limit?: number;
offset?: number;
}
Declare a table at the top level: export const users = table<User>("users"). Rows are checked against T on
every write, and rows stored as an older version of T are checked, migrated or refused when read. find
returns rows whose fields equal where's, oldest first unless ordered. See data for migrations,
indexes and the rest.
35.6 Transactions
function transaction<R>(fn: () => R | Promise<R>): Promise<R>;
All of fn's table writes happen, or, if it throws, none do. Other requests' table calls wait while it runs. Jobs
enqueued inside are queued when it commits and dropped if it rolls back. Nested calls are savepoints. Keep it
short, and don't await anything inside that itself needs a table from outside it.
35.7 Background jobs
function enqueue<A extends unknown[]>(fn: (...args: A) => unknown, ...args: A): string;
function enqueueAfter<A extends unknown[]>(seconds: number, fn: (...args: A) => unknown, ...args: A): string;
function every(spec: string, fn: () => unknown): RecurringJob;
enqueue(fn, ...args)runsfn(...args)in the background, soon, with up to 5 attempts and exponential backoff, and returns the job's id.fnmust be a codebase function (it's recorded by hash, so the job runs the version that queued it), and the arguments plain data.enqueueAfter(seconds, fn, ...args)waits first.export const nightly = every("0 3 * * *", cleanup)is a recurring job: five cron fields in UTC, or an interval like"15m"(s,m,h,d).tcm startandtcm devrun the recurring jobs in the app's namespace, once per slot however many servers there are.
35.8 For tests
function withRequest<T>(
init: { method?: string; url?: string; headers?: Record<string, string>; cookies?: Record<string, string>; ip?: string },
fn: () => T | Promise<T>,
): Promise<{ value: T; setCookies: string[] }>;
function runJobs(opts?: { all?: boolean }): Promise<{ done: number; failed: number }>;
withRequest(init, fn)runsfnas if answering a request, with no network: sessions are kept in memory, and the cookies it set come back insetCookies(pass them incookiesto continue the session).runJobs()runs the queued jobs that are due, inline;{ all: true }runs every queued job.
Under tcm test, tables and jobs live in memory. See testing.