Skip to content
Lakefront
Esc
↑↓navigate↵open⌘Jpreview
On this page

Contract

The wire contract, covering every procedure's shapes, the RPC transport client, capability tokens, and the tagged error catalog.

import { ... } from "lakefront/contract";

The wire contract is defined once, as a zod-schema-backed object; lakefront/server’s handlers and lakefront/client’s methods are both derived from it by mapped types, so neither can drift from it.

Functions

isProcedureName

function isProcedureName(name: string): name is ProcedureName;

Whether name is a key of contract.

createRpcClient

function createRpcClient(options: RpcClientOptions): RpcClient;
Parameter Type Default Description
options RpcClientOptions (required) See RpcClientOptions.

Returns a proxy typed against Contract. Each call POSTs to ${url}/rpc/${procedure} and decodes the response against that procedure’s compiled output schema.

orThrow

function orThrow(client: RpcClient): RpcClientOrThrow;

Wraps a client so every call unwraps, throwing a plain Error (not a Panic) carrying the tag and message on a server-side failure.

mintToken

function mintToken(secret: Uint8Array, kid: string, claims: Claims): string;
Parameter Type Default Description
secret Uint8Array (required) Root HMAC secret for this key id.
kid string (required) Key id, embedded in the token.
claims Claims (required) See Claims.

Issues a root capability. Throws if claims.boards or claims.verbs would be empty, since that would be a token authorizing nothing.

const token = mintToken(secret, "env", {
  boards: "*",
  verbs: ["read", "write", "admin"],
  exp: now + 86_400,
});

attenuate

function attenuate(token: string, caveat: Partial<Claims> & { ttlSeconds?: number }): string;
Parameter Type Default Description
token string (required) Token to derive a weaker one from.
caveat Partial<Claims> & { ttlSeconds?: number } (required) Restriction to append; ttlSeconds, when given, wins over a direct exp.

Returns a strictly weaker token, derived locally from the token string alone: no secret, no round-trip. Widening is never possible, since verification intersects the caveat with the base claims rather than replacing them.

const view = attenuate(service, {
  boards: ["acme_roadmap"],
  verbs: ["read"],
  ttlSeconds: 300,
});

verifyToken

function verifyToken(
  token: string,
  secretByKid: (kid: string) => Uint8Array | undefined,
  nowSeconds?: number,
): TokenVerification;
Parameter Type Default Description
token string (required) Token to verify.
secretByKid (kid: string) => Uint8Array | undefined (required) Resolves a key id to its root secret.
nowSeconds number current time Clock for the expiry check.

Returns the token’s effective claims: the base claims intersected with every caveat in the chain, board patterns intersected exactly (the narrower of two overlapping patterns survives), verbs filtered to the overlap, exp taking the minimum. Failure reasons are deliberately coarse; see TokenVerification.

claimsAllow

function claimsAllow(claims: Claims, boardId: string, verb: Verb): boolean;

The authorization predicate every enforcement point reduces to: does verb appear in claims.verbs, and does some board pattern in claims.boards cover boardId.

soleBoardOf

function soleBoardOf(claims: Claims): string | undefined;

The one board a token pins, only when it pins exactly one non-prefix board: what lets a client omit boardId from RpcClientOptions entirely.

messageOf

function messageOf(cause: unknown): string;

Normalizes a thrown value into a message without swallowing its type: cause instanceof Error ? cause.message : String(cause).

Classes

RpcError

class RpcError extends Error {
  readonly status: number;
  readonly procedure: string;
  constructor(status: number, procedure: string, message: string);
}

Thrown internally by createRpcClient for a transport-level failure with no decodable envelope; the retry loop converts a final one into a TransportFailed WireError rather than letting it escape.

Procedures

The board id is never in a procedure’s input: it travels in the request envelope, resolved from auth context by the server. That makes cross-board access unrepresentable in the query language, rather than merely validated.

Procedure Input Output
query.execute { sql: string, tables?: TableBinding[], params?: Cell[], minWatermark?: number | null } QueryResult & { watermark: number, servedBy: string, cache: CacheOutcome, sync: SyncResult, syncMs: number, queryMs: number }
mutate.insert { rows: Row[] (min 1), idempotencyKey?: string } { watermark: number }
mutate.update { updates: { id: string, patch: Row }[] (min 1), idempotencyKey?: string } { watermark: number }
mutate.delete { ids: string[] (min 1), idempotencyKey?: string } { watermark: number }
changes.read { after: number, limit?: number } ChangesPage
node.stats {} EngineStats & { nodeId: string, boards: string[] }

idempotencyKey is z.string().min(1).max(200), optional on every mutation. Semantics: the idempotency guide.

contract is narrower than the engine: data-plane verbs only. Lifecycle and schema operations (createBoard, dropBoard, addColumn, and so on) are admin decisions made next to the engine, outside HTTP. So are transactions: Engine.transaction runs embedded only, and no procedure here spans a multi-write commit over HTTP.

Types

Cell

string | number | boolean | null. Backed by the runtime schema cell.

Row

Readonly<Record<string, Cell>>. Backed by the runtime schema row.

QueryResult

{ columns: string[], rows: Cell[][] }: columnar, not row objects. Per-row object construction is where a JS serving layer burns its latency budget on wide reads. Backed by the runtime schema queryResult.

WalKind

"insert" | "upsert" | "update" | "delete" | "drop". Backed by the runtime schema walKind.

WalPayload

A union keyed by shape: { rows: Row[] } (insert/upsert), { id: string, patch: Row } (update), { ids: string[] } (delete), or { drop: true } (a whole-board tombstone). Backed by the runtime schema walPayload.

TableBinding

{ token: string, entity: string }, both matching /^[A-Za-z0-9_]{1,32}$/. Binds a {{token}} in SQL to a logical entity, constrained because the server builds a regex from it. Backed by the runtime schema tableBinding.

CacheOutcome

"hit" | "rebuild" | "reattach". Backed by the runtime schema cacheOutcome.

SyncResult

{ from: number, to: number, skipped: boolean, changesApplied: number, rowsInserted: number, rowsDeleted: number, columnsAdded: string[] }. Backed by the runtime schema syncSummary.

ParkedBoardStat

{ boardId: string, failures: number, since: number }. The wire shape EngineStats.flusherParked carries; backed by the runtime schema parkedBoard.

EngineStats

{ flusherParked: ParkedBoardStat[], cached: number, capacity: number, hits: number, rebuilds: number, reattaches: number, evictions: number, connectionsInUse: number }: what Engine.stats() returns; node.stats extends it with nodeId and boards. Backed by the runtime schema engineStats.

ChangeRecord

{ seq: number, kind: WalKind, payload: WalPayload, actor?: string }: one change-feed record. Backed by the runtime schema changeRecord.

ChangesPage

Discriminated on resync: { resync: false, records: ChangeRecord[], next: number } | { resync: true, from: number }. See the watch guide for the tail-only contract this encodes. Backed by the runtime schema changesPage.

WireTag

Every discriminant a WireError can carry, closed so a caller branches exhaustively and a typo’d tag is a compile error:

  • Engine errors, derived from ENGINE_ERRORS: InvalidBoardId, EntityNotAllowed, PlaceholderUnbound, LakeUnavailable, QueryFailed, SyncFailed, ReplicaUnavailable, WriteFailed, ReservedColumnName, ColumnTypeConflict, BoardNotFound, BoardExists, UnknownColumn
  • Client-side transport synthesis: MalformedResponse, TransportFailed
  • Server-side, raised before a handler runs: Unauthenticated, Forbidden, InvalidInput, MissingBoardEnvelope, UnknownProcedure

Full descriptions and fields for each: the error catalog.

WireError

{ _tag: WireTag, message: string }. Backed by the runtime schema wireError.

WireEnvelope

{ status: "ok", value: unknown } | { status: "error", error: WireError }: better-result’s own shape, decoded by a discriminant check.

Contract

typeof contract.

ProcedureName

keyof Contract & string.

In

type In<K extends ProcedureName> = z.infer<Contract[K]["input"]>;

Out

type Out<K extends ProcedureName> = z.infer<Contract[K]["output"]>;

Handlers

type Handlers<Ctx> = {
  [K in ProcedureName]: (input: In<K>, ctx: Ctx) => Promise<Result<Out<K>, WireError>>;
};

A server satisfies Handlers<Ctx> on its handler object is the whole conformance check.

RpcClient

type RpcClient = {
  [K in ProcedureName]: (input: In<K>) => Promise<Result<Out<K>, WireError>>;
};

RpcClientOrThrow

type RpcClientOrThrow = {
  [K in ProcedureName]: (input: In<K>) => Promise<Out<K>>;
};

RpcClientOptions

Option Type Default What it does
url string (required) The server node or router URL.
boardId string undefined Sent as x-lakefront-board. Optional when token pins exactly one board, since the server derives it (see soleBoardOf).
token string undefined Capability token, sent as Authorization: Bearer.
fetch typeof globalThis.fetch globalThis.fetch Override for testing or a custom transport.
timeoutMs number 10_000 Per-request deadline (AbortSignal.timeout).
retry { times: number, delayMs: number } { times: 2, delayMs: 50 } Transport-failure retries: network error, timeout, or a 5xx with no decodable envelope. times: 0 disables.

A decoded envelope error is never retried: the server answered, and retrying a refusal is not reconciliation. Only transport failures retry, and only when the call is safe to repeat:

  • Every procedure in IDEMPOTENT_PROCEDURES (query.execute, node.stats, changes.read) always retries.
  • A mutation retries only when its input carries an idempotencyKey; the WAL then answers a retried attempt with the original ack, so it can never double-apply.

The client marks such a request with IDEMPOTENT_HEADER (x-lakefront-idempotent: 1), so the router can apply the same rule without parsing bodies. Retries back off as delayMs * 2^i * (1 + random()).

Verb

const VERBS = ["read", "write", "schema", "admin"] as const;
type Verb = (typeof VERBS)[number];

Claims

Field Type What it means
boards readonly string[] | "*" Boards this token authorizes. An entry ending in * is a prefix grant (e.g. acme_*); the string "*" grants every board.
verbs readonly Verb[] Which of read/write/schema/admin this token authorizes.
exp number Unix seconds. Attenuations can only bring this closer, never further out.
actor string (optional) Who the caller acts for: an audit identity, not an authorization. Once set by a caveat it cannot be changed by further attenuation, only inherited.

TokenVerification

type TokenVerification = Result<{ claims: Claims; kid: string }, AuthFailureReason>;

AuthFailureReason

"malformed" | "unknown-key" | "bad-signature" | "expired". A caller cannot probe which check failed beyond the tag.

EngineError

The union of every tagged error class raised by the engine and carried across the wire unchanged: InvalidBoardId, EntityNotAllowed, PlaceholderUnbound, LakeUnavailable, QueryFailed, SyncFailed, ReplicaUnavailable, WriteFailed, ReservedColumnName, ColumnTypeConflict, BoardNotFound, BoardExists, UnknownColumn, each also exported by name as a TaggedError from better-result. Fields and the conditions that raise each: the error catalog.

Constants

contract

const contract: { "query.execute": ...; "mutate.insert": ...; "mutate.update": ...; "mutate.delete": ...; "changes.read": ...; "node.stats": ... };

The procedure map itself: each entry’s input/output are AOT-compiled zod schemas (z.compile), built once at module load rather than per request. See Procedures for the full table.

WIRE_TAGS

const WIRE_TAGS: readonly WireTag[];

Every value WireTag can hold, as a runtime array; see WireTag.

IDEMPOTENT_HEADER

"x-lakefront-idempotent". Marks a request as safe to retry, hedge, and fail over; the client sets it exactly when the body carries an idempotencyKey, so the router never parses bodies.

IDEMPOTENT_PROCEDURES

ReadonlySet<string> of {"query.execute", "node.stats", "changes.read"}: procedures idempotent by nature and therefore always safe to retry, hedge, and fail over. Imported by both the client’s retry gate and the router’s hedging gate, so they cannot drift.

ENGINE_ERRORS

const ENGINE_ERRORS: {
  InvalidBoardId;
  EntityNotAllowed;
  PlaceholderUnbound;
  LakeUnavailable;
  QueryFailed;
  SyncFailed;
  ReplicaUnavailable;
  WriteFailed;
  ReservedColumnName;
  ColumnTypeConflict;
  BoardNotFound;
  BoardExists;
  UnknownColumn;
};

The single lookup object every derivation in the system uses: WIRE_TAGS, the engine’s own error-wrapping, and the reference catalog are all built from it, so a class added here is everywhere at once.

BASE_COLUMNS

BASE_COLUMNS = { id: "VARCHAR", updated_at: "TIMESTAMP", state: "VARCHAR" };

Fixed columns every board table has; their types are fixed by the system and never inferred from data.

Last updated on September 10, 2026

Was this page helpful?