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

```ts
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

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

Whether `name` is a key of [`contract`](#contract).

### createRpcClient

```ts
function createRpcClient(options: RpcClientOptions): RpcClient;
```

| Parameter | Type               | Default    | Description                                |
| --------- | ------------------ | ---------- | ------------------------------------------ |
| `options` | `RpcClientOptions` | (required) | See [RpcClientOptions](#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

```ts
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

```ts
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](#claims).            |

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

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

### attenuate

```ts
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.

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

### verifyToken

```ts
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](#tokenverification).

### claimsAllow

```ts
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

```ts
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

```ts
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

```ts
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](/guides/idempotency).

`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](/guides/watch) 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](/reference/errors).

### 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

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

### Out

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

### Handlers

```ts
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

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

### RpcClientOrThrow

```ts
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

```ts
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

```ts
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](/reference/errors).

## Constants

### contract

```ts
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](#procedures) for the full table.

### WIRE_TAGS

```ts
const WIRE_TAGS: readonly WireTag[];
```

Every value `WireTag` can hold, as a runtime array; see [WireTag](#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

```ts
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

```ts
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.
