Validation
Argument and return schemas, checked at runtime, in production.
Every function declares an args schema and a returns schema. Both are
checked at runtime — not only in development, and not only in tests.
export const saveNote = mutation({
name: "notes.save",
args: noteArgs, // checked before the handler runs
returns: noteId, // checked before the value crosses back
auth: can("notes:write"),
handler: (ctx, args) => ctx.db.table("notes").insert(args),
});Validating arguments is obvious — they come from a browser. Validating the return is the less obvious half: it is the boundary where a storage row becomes a public API value. Without it, adding a column to a table silently starts publishing it.
Builders
A Schema<T> is anything with a parse that throws on bad input, so you can
write your own or wrap a library.
schema()
The primitive:
import { schema } from "@highx/core";
const noteId = schema((input: unknown): string => {
if (typeof input !== "string" || input.length === 0) throw new Error("Expected a note ID");
return input;
});stringSchema()
import { stringSchema } from "@highx/core";
const title = stringSchema({ minLength: 1, maxLength: 120 });
const body = stringSchema({ maxLength: 10_000 });Put a maxLength on anything a client sends. An unbounded string is an
unbounded row, and rows live in a Durable Object with a storage limit.
objectSchema()
For argument objects. Each field is narrowed explicitly:
import { objectSchema, stringSchema } from "@highx/core";
interface NoteInput extends Record<string, unknown> {
readonly title: string;
readonly color: "amber" | "blue" | "rose";
}
const noteArgs = objectSchema<NoteInput>((input) => {
const color = input["color"];
if (color !== "amber" && color !== "blue" && color !== "rose") {
throw new Error("Invalid note color");
}
return {
title: stringSchema({ minLength: 1, maxLength: 120 }).parse(input["title"]),
color,
};
});Fields the callback does not return are dropped. A client sending
{ title, color, isAdmin: true } gets { title, color } in the handler.
Using Zod
import { z } from "zod";
const noteArgs = z.object({ title: z.string().min(1).max(120) });Every args, returns, input, output, row, and workflow result contract
accepts a native Zod schema directly. HighX calls parse/safeParse at every
boundary, including storage replay. The legacy schema() helper is only a
Zod-backed migration bridge for older internal validators.
Validation is not authorization
Validation answers "is this well-formed?", never "is this allowed?". A
well-formed tenant ID from an attacker is still an attacker's tenant ID —
authorization belongs in auth and ctx.identity.
Failures
A schema failure becomes an invalid_argument error with a safe message. What
your throw said is logged server-side; the client is told what was wrong with
the request, never what the server looks like inside.