Rindle

API index and search · Build metadata

@rindle/api-server

0.0.0 · Public export map; development manifest version (0.0.0).

Source revision 05d0bf2c2e56 · build details
Source revision: 05d0bf2c2e56.
TypeScript input SHA-256: aabe6cfcc4172b870d5e272142958e9ea8d8784c2aa23133156e5a7ee633318e
Generated 2026-09-04T23:58:25.590Z with TypeScript 6.0.3. Public TypeScript checks and declaration emit passed. Package runtime tests are separate.

Entry point source

ApiContext

InterfaceDeclaration · Source: packages/api-server/src/index.ts:183 · Supporting declarations

Request context supplied by the application after authentication. The HTTP adapter or trusted caller owns user; do not populate it from an unverified request body.

export interface ApiContext<User> {
    user: User;
    request?: unknown;
}

ApiMutator

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:276 · Supporting declarations

A server mutator: a plain async function against the live {@link ServerMutationTx}. Two ways to write one (MUTATORS-ISOMORPHIC): drive it directly (the raw escape hatch — an owner-gated cascade, a NOT EXISTS dedup — plus a returned SqlStatement[]/SqlTxn), OR delegate to a SHARED generator (the SAME body the client predicts) via {@link runSharedMutation}, keeping only the server-only authority (arg parse, principal, policy) in the wrapper.

export type ApiMutator<User, Args> = (tx: ServerMutationTx, args: Args, ctx: MutationContext<User>) => MaybePromise<ApiMutatorResult>;

ApiMutatorResult

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:270 · Supporting declarations

export type ApiMutatorResult = void | SqlStatement[] | SqlTxn;

ApiMutators

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:401 · Supporting declarations

export type ApiMutators<User> = Record<string, ApiMutator<User, any>>;

ApiQueries

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:190 · Supporting declarations

export type ApiQueries<User> = Record<string, ApiQuery<User, any>>;

ApiQuery

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:189 · Supporting declarations

export type ApiQuery<User, Args> = (ctx: ApiContext<User>, args: Args) => MaybePromise<ApiQueryResult>;

ApiQueryResult

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:188 · Supporting declarations

export type ApiQueryResult = Ast | Query<any, any, any>;

ArgSchema

InterfaceDeclaration · Source: packages/client/src/mutation-ops.ts:161 · Supporting declarations

The minimal arg validator a shared mutator can carry so the SERVER can parse UNTRUSTED wire args before driving it (the client trusts its typed callsites and never calls this). Structural on purpose — a zod schema satisfies it as-is — so @rindle/client stays validator-library-agnostic.

export interface ArgSchema<Args> {
    parse(raw: unknown): Args;
}

assembleDurableText

FunctionDeclaration · Source: packages/client/src/stream.ts:87 · Supporting declarations

The durable half of the splice for the mapped-table layout: the compacted body followed by whatever chunk rows have not been folded into it yet.

Chunks are ALWAYS the suffix after body — the closing checkpoint rewrites body and drops the chunks it absorbed in ONE transaction — so a reader never observes a torn state where a chunk both is and is not in the body.

export declare function assembleDurableText(message: {
    body?: string | null;
} | null | undefined, chunks?: ReadonlyArray<{
    seq: number;
    text: string;
}>): string;

AuthorizeMutationInput

InterfaceDeclaration · Source: packages/api-server/src/index.ts:209 · Supporting declarations

export interface AuthorizeMutationInput<User> {
    user: User;
    envelope: MutationEnvelope;
    context: ApiContext<User>;
}

AuthorizeQueryInput

InterfaceDeclaration · Source: packages/api-server/src/index.ts:202 · Supporting declarations

export interface AuthorizeQueryInput<User> {
    user: User;
    name: string;
    args: unknown;
    context: ApiContext<User>;
}

Authorizer

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:217 · Supporting declarations

An optional request gate. Return false or throw to deny; true and undefined allow. This gate does not add row predicates or replace access checks inside a mutator.

export type Authorizer<T> = (input: T) => MaybePromise<boolean | void>;

AuthorizeStreamInput

InterfaceDeclaration · Source: packages/api-server/src/streams.ts:169 · Supporting declarations

export interface AuthorizeStreamInput<User> {
    user: User;
    streamId: string;
    /** Where the subscriber claims to be. */
    from: number;
    /** The `meta` this stream was opened with — `undefined` when the stream is not hosted here, which
     *  is precisely when the app must decide from `streamId` and its own durable state. */
    meta: unknown;
    request?: unknown;
}

BackendError

ClassDeclaration · Source: packages/api-server/src/index.ts:1371 · Supporting declarations

Thrown (wrapping the driver error) by a server tx's DB calls, so the seam can tell an INFRA failure (retry) from a mutator-body throw (business rejection).

export declare class BackendError extends Error {
    readonly driverError: unknown;
    constructor(driverError: unknown);
}

buildRenderIndex

FunctionDeclaration · Source: packages/api-server/src/index.ts:1252 · Supporting declarations

Build the {@link RenderIndex} from a typed schema (schema.tables[name] is a TableMeta).

export declare function buildRenderIndex(schema: Schema): RenderIndex;

createRindleApiServer

FunctionDeclaration · Source: packages/api-server/src/index.ts:2783 · Supporting declarations

export declare function createRindleApiServer<User = unknown>(options: RindleApiServerOptions<User>): RindleApiServer<User>;

daemonBackend

FunctionDeclaration · Source: packages/api-server/src/index.ts:2165 · Supporting declarations

Legacy/private-plane adapter. Kept for existing deployments; its mutation policy is shared with {@link sqlBackend}, so the two transports cannot drift.

export declare function daemonBackend(daemon: RindleDaemonClient): MutationBackend;

DEFAULT_RINDLE_API_ROUTES

VariableDeclaration · Source: packages/api-server/src/index.ts:163 · Supporting declarations

export declare const DEFAULT_RINDLE_API_ROUTES: {
    readonly query: "/api/rindle/query";
    readonly read: "/api/rindle/read";
    readonly mutate: "/api/rindle/mutate";
    readonly applyRowChangeTxn: "/api/rindle/apply-row-change-txn";
    readonly claimRoomEpoch: "/api/rindle/claim-room-epoch";
    readonly roomLmids: "/api/rindle/room-lmids";
    readonly roomBoot: "/api/rindle/room-boot";
    readonly stream: "/api/rindle/stream";
};

defineApiMutators

FunctionDeclaration · Source: packages/api-server/src/index.ts:2400 · Supporting declarations

export declare function defineApiMutators<User, M extends ApiMutators<User>>(mutators: M): M;

defineApiQueries

FunctionDeclaration · Source: packages/api-server/src/index.ts:2363 · Supporting declarations

export declare function defineApiQueries<User, Q extends ApiQueries<User>>(queries: Q): Q;

dumpQueryShapes

FunctionDeclaration · Source: packages/api-server/src/index.ts:2507 · Supporting declarations

Dump every registered named query's wire AST — feeder 1 ("exemplar enumeration") of rindle indices suggest (docs/INDEXING.md applied mechanically to the query set).

Because named queries are FUNCTIONS of (args, ctx), one query can build structurally different ASTs on different args; each exemplar invocation contributes its shape, and shapes that differ only in literal values (a limit, a filter string) dedupe to one entry. A query with no configured exemplars is invoked once with no args. The registry is the app's whole server-side query surface, so the resulting document is the complete static shape set — modulo arg-value-dependent branches, which need an exemplar (or runtime shape recording) to surface.

export declare function dumpQueryShapes<User>(opts: {
    schema: Schema;
    queries: ApiQueries<User>;
    exemplars?: Partial<Record<string, ReadonlyArray<ShapeExemplar<User>>>>;
}): Promise<QueryShapesDoc>;

frameResumePoint

FunctionDeclaration · Source: packages/client/src/stream.ts:51 · Supporting declarations

The resume point a frame implies — what rides an SSE id: line so a reconnecting EventSource hands it straight back as Last-Event-ID. undefined for frames that are not a position.

export declare function frameResumePoint(frame: StreamFrame): number | undefined;

guardMutator

FunctionDeclaration · Source: packages/api-server/src/index.ts:2463 · Supporting declarations

Wrap a SHARED (generator) mutator with a row-level ACCESS GUARD — the multi-tenant authz twin of {@link sharedApiMutators}. It parses the untrusted wire args, derives the {@link MutatorCtx} principal (the SAME mapping you pass to sharedApiMutators), evaluates predicate against the OPEN mutation txn (so it can READ the rows the write depends on), and throws forbidden (403 — the client's optimistic write snaps back) when access is denied; otherwise it drives the SAME body the client predicts ({@link runSharedMutation}). Use it for the entries that need server-only authority the client cannot predict, OVERRIDING the auto-wrapped default (spread sharedApiMutators(...) first, then the guarded overrides win by key):

const principal = (ctx) => ({ user: requireUser(ctx.user) });
mutators: defineApiMutators({
  ...sharedApiMutators(sharedMutators, principal),
  updateSlide: guardMutator(sharedMutators.updateSlide, principal,
    async (tx, a, { user }) =>
      (await tx.query(q.slide.where.id(a.slideId).where(editableBy(user)).one())) != null,
    { message: "not permitted to edit this slide" }),
}),

The predicate keeps the shared body READ-FREE, so the client's .folded hot paths (drag/keystroke) still fold — the read is server-side only. Return false to deny (→ the default or opts.message forbidden); return true/nothing to allow. To reject with a different status/message (a business rejection, a not-found), throw a {@link RindleApiError} from inside the predicate instead. principal runs before the predicate, so it too may throw forbidden for an anonymous caller.

export declare function guardMutator<User, Args>(gen: SharedMutatorWithArgs<Args>, principal: (ctx: MutationContext<User>) => MutatorCtx, predicate: (tx: ServerMutationTx, args: Args, ctx: MutatorCtx) => boolean | void | Promise<boolean | void>, opts?: {
    message?: string;
}): ApiMutator<User, unknown>;

isoTx

VariableDeclaration · Source: packages/client/src/mutation-ops.ts:127 · Supporting declarations

The one shared effect factory (stateless — see {@link IsoTx}). Its methods just BUILD a {@link MutationOp}, so the single instance serves every schema; the generic {@link IsoTx} view is applied at the authoring site (a json<T> cell is a parsed object here and is stringified by the funnels, {@link toCell }), hence the cast — the runtime shape is schema-agnostic.

export declare const isoTx: IsoTx;

IsoTx

InterfaceDeclaration · Source: packages/client/src/mutation-ops.ts:102 · Supporting declarations

The tier-AGNOSTIC effect factory a generator mutator writes against. Every method just BUILDS an effect to yield — it performs no I/O and holds no state, so the single {@link isoTx} instance is shared by every mutator on both tiers; only the driver differs. insert/upsert/insertIgnore require non-nullable columns and permit nullable omissions, which become null. update/delete require the PK columns; update also names the columns to change.

export interface IsoTx<S extends ColsMap = ColsMap, P extends Record<string, string> = PkMap<S>> {
    /** Insert a row. Omitted nullable columns become `null`; database defaults are not applied. */
    insert<N extends keyof S & string>(table: N, row: InsertOf<S[N]>): MutationOp;
    /** Update the row identified by its pk columns (REQUIRED); only the named non-pk columns change. */
    update<N extends keyof S & string>(table: N, row: UpdateOf<S[N], PkColsOf<S, P, N>>): MutationOp;
    /** Insert, or replace non-PK columns on PK conflict, with {@link IsoTx.insert}'s omission rules. */
    upsert<N extends keyof S & string>(table: N, row: InsertOf<S[N]>): MutationOp;
    /** Insert with {@link IsoTx.insert}'s omission rules, or do nothing on PK conflict. */
    insertIgnore<N extends keyof S & string>(table: N, row: InsertOf<S[N]>): MutationOp;
    /** Delete the row identified by its pk columns. */
    delete<N extends keyof S & string>(table: N, pk: PkOf<S[N], PkColsOf<S, P, N>>): MutationOp;
    /** Read one row by primary key (read-your-writes). */
    row<N extends keyof S & string>(table: N, pk: PkOf<S[N], PkColsOf<S, P, N>>): ReadEffect;
    /** Run a full query (`where`/`orderBy`/`limit`/join) over the state this mutator is mutating —
     *  read-your-writes, like {@link row} but for arbitrary shapes. Pass a query from the tier-agnostic
     *  builder, e.g. `tx.query(q.issue.where("ownerId", "=", ctx.user))` where `q = newQueryBuilder(schema)`.
     *  The `yield` evaluates to {@link QueryResultRow}`[]` (cast it — the generator's single next-type is a row). */
    query(query: QueryArg): QueryEffect;
    all(effects: readonly YieldEffect[]): BatchEffect;
}

MaybePromise

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:179 · Supporting declarations

export type MaybePromise<T> = T | PromiseLike<T>;

mintRoomFlushCredential

FunctionDeclaration · Source: packages/api-server/src/index.ts:840 · Supporting declarations

Sign the default epoch-bound flush credential (/room-boot mints one per placement).

export declare function mintRoomFlushCredential(opts: {
    shellSecret: string;
    doc: string;
    epoch: number;
    /** Mint time; defaults to `Date.now()`. Injectable for tests. */
    now?: number;
}): Promise<string>;

MutationBackend

InterfaceDeclaration · Source: packages/api-server/src/index.ts:587 · Supporting declarations

Where a mutation runs and who stamps lmid — the seam that makes the mutator authoring surface backend-agnostic (BYO-POSTGRES-LMID-CONTRACT-DESIGN.md §6; MUTATORS-ISOMORPHIC plan). Three implementations ship: {@link sqlBackend} is the preferred managed-SQL path; {@link daemonBackend} keeps the private control-plane compatibility path; and {@link postgresBackend} runs a real interactive PG transaction with confirmation riding the CDC loop down.

The load-bearing invariant is that a mutation ALWAYS advances the client's last_mutation_id:

  • runMutation runs the mutator inside the backend's transaction; on success it advances lmid to envelope.mid in the SAME atomic unit (OPTIMISTIC-WRITES-DESIGN.md §8.2); on a BUSINESS rejection it rolls the data back but STILL advances lmid (§2.4 — else the client's pending queue never drains and the optimistic stack wedges).
  • reject is the pre-flight path (unknown mutator, failed authorization): NO data, lmid alone.
  • An infrastructure failure THROWS from runMutation — never a user rejection (the client retries; mid dedup absorbs any applied prefix).
export interface MutationBackend {
    /** The SQL dialect this backend renders logical ops to (drives placeholder style). */
    readonly dialect: SqlDialect;
    /** Optional raw-SQL surface outside the mutation transaction. Built-in backends provide it;
     *  custom backends may omit it, in which case `scope.sql` fails as an infrastructure error. */
    readonly outsideSql?: ServerSql;
    runMutation(input: MutationRunInput): Promise<MutationOutcome>;
    reject(input: {
        envelope: MutationEnvelope;
        reason: string;
    }): Promise<unknown>;
}

MutationContext

InterfaceDeclaration · Source: packages/api-server/src/index.ts:219 · Supporting declarations

export interface MutationContext<User> {
    user: User;
    envelope: MutationEnvelope;
    daemon: RindleDaemonClient;
    request?: unknown;
}

MutationGen

TypeAliasDeclaration · Source: packages/client/src/mutation-ops.ts:148 · Supporting declarations

What a generator mutator IS: yield tx.<op>() on every side effect; a yield tx.row() expression evaluates to the row. Neither sync nor async — the tier's driver decides, which is what lets one body run synchronously on the client and against a live async transaction on the server.

export type MutationGen = Generator<YieldEffect, void, KeyedRow | undefined>;

MutationOutcome

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:567 · Supporting declarations

The result of {@link MutationBackend.runMutation}: either the data+lmid committed together, or a business rejection whose data was rolled back but whose lmid still advanced (§2.4).

export type MutationOutcome = {
    accepted: true;
    output: SqlTxnOutput;
} | {
    accepted: false;
    reason: string;
    output?: unknown;
};

MutationRejected

ClassDeclaration · Source: packages/api-server/src/index.ts:297 · Supporting declarations

Thrown by {@link MutationScope.transact} when the transacted body BUSINESS-rejects: the data rolled back and lmid advanced alone (§2.4). Catch it to COMPENSATE an outside-tx side effect (refund the charge), then rethrow or return — the mutation's protocol outcome is already sealed as rejected, so a post-reject throw can't change it. A DB/infra failure is NOT this — it propagates as the raw driver error (the client retries; lmid did not advance).

export declare class MutationRejected extends Error {
    readonly reason: string;
    constructor(reason: string);
}

MutationRunInput

InterfaceDeclaration · Source: packages/api-server/src/index.ts:554 · Supporting declarations

The context a {@link MutationBackend} needs to run one mutation inside its transaction.

export interface MutationRunInput {
    envelope: MutationEnvelope;
    /** Schema-derived render metadata (from {@link RindleApiServerOptions.schema}). A logical op on a
     *  table absent here throws loudly — never a silent dropped write. `{}` when no schema is set. */
    render: RenderIndex;
    /** Invoke the (authorized) mutator against the backend-provided tx. A THROW that is NOT a
     *  {@link BackendError} is a BUSINESS rejection (roll the data back, then advance `lmid` alone,
     *  §2.4); a {@link BackendError} (a DB-layer failure) is INFRA and rejects the returned promise. */
    run(tx: ServerMutationTx): Promise<void>;
}

MutationScope

InterfaceDeclaration · Source: packages/api-server/src/index.ts:312 · Supporting declarations

The per-mutation server handle a {@link ScopedMutator} runs against. Code before {@link transact} runs OUTSIDE the transaction; code after a clean transact runs AFTER the commit. The lmid-always-advances invariant is the HARNESS's, not the author's: {@link RindleApiServer.pushMutation} seals the response from this handle's recorded outcome, so an early return, a never-called transact, or a swallowed {@link MutationRejected} still advances lmid and never wedges the client's pending queue.

export interface MutationScope {
    /** Raw SQL OUTSIDE the mutation transaction. Every call commits independently and therefore may
     *  be observed even if {@link transact} later rejects or fails. Calls may also repeat when an
     *  envelope is retried, so outside writes need their own idempotency key/unique constraint. */
    readonly sql: ServerSql;
    /** Open the ONE atomic write transaction and drive `body` inside it, committing (stamping `lmid`
     *  co-transactionally) on a clean return. MAY be called at most once — a second call throws.
     *
     *  Two forms:
     *   - `transact(sharedMutator, args, ctx)` — drive a SHARED (generator) mutator (the same body the
     *     client predicts); pass the already-parsed `args` and the server `ctx` (fold server-only
     *     values like a charge id into `ctx` here).
     *   - `transact(run)` — a raw callback receiving the live {@link ServerMutationTx} (the escape
     *     hatch: `tx.exec`, logical writes, read-your-writes reads).
     *
     *  A THROW from the body that is not a {@link BackendError} is a BUSINESS rejection: the data rolls
     *  back, `lmid` advances alone, and this method throws {@link MutationRejected} (so surrounding
     *  code can compensate). A {@link BackendError} is INFRA: it propagates (the client retries).
     *
     *  **The callback form RETURNS ITS BODY'S VALUE**, and only on the committed path — a rejection
     *  throws, so there is never a value to act on for a transaction that rolled back. That is what
     *  lets a post-commit effect be decided by a TRANSACTIONAL read instead of a second, racy one
     *  afterwards:
     *
     *  ```ts
     *  const kick = await scope.transact(async (tx) => {
     *    const prior = await tx.row("message", { id: a.assistantMessageId });
     *    if (prior) return undefined;                 // a replayed envelope — do NOT re-fire the effect
     *    tx.insert("message", …);
     *    return { streamId: a.assistantMessageId, history: await readHistory(tx, a.chatId) };
     *  });
     *  if (kick) void startGeneration(kick);          // committed, and decided against committed state
     *  ```
     *
     *  A tx-form mutator cannot do this: its return value is already the logical-write channel
     *  ({@link ApiMutatorResult}). A post-commit effect chosen by a transactional read is exactly what
     *  the scoped form is for. */
    transact<T>(run: (tx: ServerMutationTx) => T | Promise<T>): Promise<T>;
    transact<A, C extends MutatorCtx>(mutator: SharedMutator<A, C>, args: A, ctx: C): Promise<void>;
}

MutatorCtx

InterfaceDeclaration · Source: packages/client/src/mutation-ops.ts:141 · Supporting declarations

The minimal per-invocation context a shared mutator sees on BOTH tiers: the acting principal. The server injects its AUTHENTICATED user; the client injects its local user. (The server's own MutationContext is a superset of this.)

export interface MutatorCtx {
    user: string;
}

OpenStreamInput

InterfaceDeclaration · Source: packages/api-server/src/streams.ts:260 · Supporting declarations

export interface OpenStreamInput<User> {
    user: User;
    /** The app's message-row id: the durable pointer AND the live plane's key. The row must already
     *  exist (the app's own mutator wrote it, alongside the user's prompt) with `seq = 0`. */
    streamId: string;
    /** Opaque app payload, passed through to a `commit` callback. Unused in `tables` mode. */
    meta?: unknown;
    request?: unknown;
}

PgPoolLike

InterfaceDeclaration · Source: packages/api-server/src/index.ts:2278 · Supporting declarations

The slice of a node-postgres Pool the plugger needs — structural, so pg stays a dependency of the APP, never of this package.

export interface PgPoolLike {
    connect(): Promise<{
        query(sql: string, params?: unknown[]): Promise<{
            rows: Array<Record<string, unknown>>;
        }>;
        release(err?: unknown): void;
    }>;
}

pgPoolPlugger

FunctionDeclaration · Source: packages/api-server/src/index.ts:2287 · Supporting declarations

Adapt a node-postgres Pool (or anything pool-shaped) to a {@link PostgresPlugger}: one client per transaction, BEGIN/COMMIT bracketing, ROLLBACK + rethrow on failure.

export declare function pgPoolPlugger(pool: PgPoolLike): PostgresPlugger;

PgQuery

InterfaceDeclaration · Source: packages/api-server/src/index.ts:2178 · Supporting declarations

The query surface a {@link PostgresPlugger} transaction exposes. exec runs one statement; query returns rows keyed by column name (read-your-own-writes inside the txn).

export interface PgQuery {
    exec(sql: string, params?: unknown[]): Promise<void>;
    query(sql: string, params?: unknown[]): Promise<Array<Record<string, unknown>>>;
}

PinFanout

InterfaceDeclaration · Source: packages/api-server/src/index.ts:643 · Supporting declarations

The explicit fleet pin fan-out seam (READ-ROUTER-DESIGN.md §4.2). The api-server resolves each pin's authoritative AST under pinUser and hands the ready {@link MaterializeInput}s here; the implementation (the read router) fans EACH across all live followers. Distinct, on purpose, from a per-viewer materialize (which routes ONE) — a pin-assert always sprays ALL.

export interface PinFanout {
    assertPins(pins: readonly MaterializeInput[]): Promise<void>;
}

PinnedQuery

InterfaceDeclaration · Source: packages/api-server/src/index.ts:634 · Supporting declarations

A named query to keep permanently materialized (warm with zero subscribers).

export interface PinnedQuery {
    name: string;
    args?: unknown;
}

postgresBackend

FunctionDeclaration · Source: packages/api-server/src/index.ts:2216 · Supporting declarations

The BYO-Postgres {@link MutationBackend} (BYO-POSTGRES-LMID-CONTRACT-DESIGN.md §6.3): one PG transaction runs the mutator's statements and ALWAYS upserts _rindle_client_mutations — the upsert sits outside any acceptance guard by construction, so the §2.4 footgun (a rejection that forgets to advance lmid and wedges the client's pending queue) cannot be written.

Confirmation does NOT come from this call's response: the lmid row rides the same PG commit through CDC → relay → follower and reaches the client in the same coherent release as the data (§8.2 relocated upstream). A rejection's reason still returns on the HTTP reply, but nothing rejection-shaped travels the replication path — the optimistic prediction snaps back when the advanced lmid arrives.

export declare function postgresBackend(plugger: PostgresPlugger, opts?: PostgresBackendOptions): MutationBackend;

PostgresBackendOptions

InterfaceDeclaration · Source: packages/api-server/src/index.ts:2190 · Supporting declarations

export interface PostgresBackendOptions {
    /** Rewrite each MUTATOR statement's SQL before it runs (never the lmid upsert). The intended
     *  use is dialect bridging for a dual-topology app whose mutators are written SQLite-style:
     *  pass {@link questionToDollarParams} to convert `?` placeholders to `$1..$n`. */
    rewriteSql?: (sql: string) => string;
}

postgresDialect

VariableDeclaration · Source: packages/api-server/src/index.ts:1233 · Supporting declarations

export declare const postgresDialect: SqlDialect;

PostgresPlugger

InterfaceDeclaration · Source: packages/api-server/src/index.ts:2186 · Supporting declarations

The thin driver adapter that keeps pg / postgres.js out of this package's dependencies (BYO-POSTGRES-LMID-CONTRACT-DESIGN.md §6.2): run fn inside ONE transaction — commit on resolve, roll back on throw. {@link pgPoolPlugger} adapts a node-postgres Pool.

export interface PostgresPlugger {
    transaction<T>(fn: (q: PgQuery) => Promise<T>): Promise<T>;
}

PushMutationRequest

InterfaceDeclaration · Source: packages/api-server/src/index.ts:597 · Supporting declarations

export interface PushMutationRequest<User> {
    user: User;
    envelope: MutationEnvelope;
    request?: unknown;
}

PushMutationResponse

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:609 · Supporting declarations

export type PushMutationResponse = {
    accepted: true;
    rejected: false;
    output: SqlTxnOutput;
} | {
    accepted: false;
    rejected: true;
    reason: string;
    output?: unknown;
};

PushMutationsRequest

InterfaceDeclaration · Source: packages/api-server/src/index.ts:603 · Supporting declarations

export interface PushMutationsRequest<User> {
    user: User;
    envelopes: MutationEnvelope[];
    request?: unknown;
}

QueryLeaseLifecycle

InterfaceDeclaration · Source: packages/api-server/src/index.ts:484 · Supporting declarations

The §4 lifecycle block on a query lease (Slice I-iii): present only when BOTH the realtime lifecycle config is on AND the query is realtime-labeled. doorbell rides EVERY labeled lease (occupancy is counted whether or not the query is room-served — the 1→2 upgrade trigger needs solo watchers subscribed BEFORE any room exists, §4.1); fence rides only a ROOM-SERVED lease (the §4.2/§7.1/§3.3 downgrade surfaces are meaningful only where a room domain exists). The §4.2 fence VALUE (finalFlushSeq) is deliberately NOT here — it arrives with the I-v downgrade response; I-iii only stands up the streams.

export interface QueryLeaseLifecycle {
    doorbell: QueryLeaseLifecycleLease;
    fence?: QueryLeaseLifecycleLease[];
}

QueryLeaseLifecycleLease

InterfaceDeclaration · Source: packages/api-server/src/index.ts:463 · Supporting declarations

One minted SYSTEM-STREAM lease on a query lease's lifecycle block (RINDLE-REALTIME-QUERY- ENABLEMENT §4, Slice I-iii): an ordinary daemon materialization over one of the four _rindle_* lifecycle system tables (registered by the daemon's enable_realtime_lifecyclerust/rindle-replica/src/mutations.rs), attachable by the EXISTING client subscribe path (present leaseToken on a subscribe frame, exactly like the primary lease). The identity fields (scope/doc/clientId) document the minted AST's predicate — the client keys its retains and its release-time row filters on them.

export interface QueryLeaseLifecycleLease {
    /** Which system table this lease's subscription serves. */
    table: string;
    leaseToken: string;
    /** DOORBELL only: the §4.1 occupancy scope — the wire room doc (`"<profile>/<key>"`). */
    scope?: string;
    /** FENCE entries only: the room doc the predicate is scoped to. */
    doc?: string;
    /** FENCE ledger/outcome entries: present iff the predicate was ALSO client-scoped (the lease
     *  request carried `clientId`). Absent ⇒ doc-only predicate — the client filters to its own
     *  rows regardless (defense in depth). */
    clientId?: string;
}

QueryLeaseRealtime

InterfaceDeclaration · Source: packages/api-server/src/index.ts:430 · Supporting declarations

The room-serve block on a query lease (RINDLE-REALTIME-QUERY-ENABLEMENT §2.1 step 5 / §2.4, slice G-iv-b): present when the named query carries a realtime label naming a configured room profile (302 §5 — declared, not derived; no coverage proof). The G-v client uses it to open the room transport for THIS query beside — never instead of — its daemon session.

It is a dedicated block on purpose: the ROOM ws is a SEPARATE connection this query opens beside its daemon session (never a migration of the daemon session — the daemon ws host is fixed and placed by the affinity ticket). A room-served lease's top-level fields are byte-identical to the daemon-served ones.

export interface QueryLeaseRealtime {
    /** The client store's gate/domain key for this room source (`connectSource`) AND the string the
     *  wasm engine's `parse_source_key` accepts: any string other than the reserved `"daemon"`
     *  parses as a room source, and the established convention is `"room:" + doc`
     *  (e.g. `room:document/doc:d1`). */
    sourceKey: string;
    /** Where the client opens the ROOM ws for this query (from `realtime.locateRoom`) — its OWN
     *  connection, distinct from the daemon session's fixed ws host. */
    wsEndpoint: string;
    /** The room's self-authorizing signed lease (`@rindle/room/token`): the APPROVED query AST +
     *  doc + subject, HMAC-signed with `realtime.roomTokenKey` so the room shell's
     *  `downstream.tokenKeys` ring verifies it. The room materializes on first presentation. */
    roomToken: string;
    /** Token expiry (ms epoch) — the client's renewal clock (renewal = a fresh lease). */
    exp: number;
    /** The wire room doc (`"<profile>/<key>"`, minted server-side — never client-derived). */
    doc: string;
    /** Per-footprint-table specs: the §2.2 owned/followed split + §3.2 routing metadata. Since
     *  H-iii each spec also carries `footprintWhere` — the same exact membership predicate the boot
     *  wire ships the room gate (one compiler, `compileRoomScopeSpecs`) — feeding the client's §3
     *  prove-or-slow-path router. Advisory routing metadata, never a credential (§3.2). */
    tables: RoomTableSpec[];
}

QueryLeaseRealtimeFence

InterfaceDeclaration · Source: packages/api-server/src/index.ts:495 · Supporting declarations

The §4.2 downgrade fence on a query lease (Slice I-v): rides a labeled reply whose §4.1 occupancy gate CLOSED (so there is NO realtime block) when the server could drain the room. A SIBLING of realtime, never nested inside it — block-ABSENCE is the downgrade signal, and the fence rides alongside that absence. Its finalFlushSeq is the room's last COMMITTED flush seq; the client's frozen room ghost holds visible until the daemon plane has provably absorbed it (_rindle_room_watermark(doc) ≥ finalFlushSeq). Absent from every non-downgrade reply.

export interface QueryLeaseRealtimeFence {
    /** The retiring room source's gate/domain key — `"room:" + doc`, matching what the room-served
     *  lease's {@link QueryLeaseRealtime.sourceKey} carried. */
    sourceKey: string;
    doc: string;
    finalFlushSeq: number;
}

QueryLeaseRequest

InterfaceDeclaration · Source: packages/api-server/src/index.ts:403 · Supporting declarations

export interface QueryLeaseRequest<User> {
    user: User;
    name: string;
    args: unknown;
    request?: unknown;
    /** The browser's stable `clientId` (sent on the query POST) — the anonymous routing-key fallback
     *  when there is no authenticated subject and no session cookie (READ-ROUTER-DESIGN.md §1.5/§2.2).
     *  A routing HINT only, never authorization. */
    clientId?: string;
    /** The browser's opaque follower-affinity ticket (FOLLOWER-AFFINITY-DESIGN.md §3), read off the
     *  query POST and forwarded opaquely on `materialize` so the fleet edge selects the follower
     *  the browser's ws is pinned to (§2, §4) — both legs co-locate. The api-server does NOT verify
     *  it (the fleet does); it holds no signing key. Absent ⇒ single daemon / affinity off. */
    affinity?: string;
}

QueryLeaseResponse

InterfaceDeclaration · Source: packages/api-server/src/index.ts:503 · Supporting declarations

export interface QueryLeaseResponse {
    leaseToken: string;
    materializationId: string;
    queryKey?: string;
    reused?: boolean;
    /** The public daemon/fleet WebSocket endpoint. With the unified `rindle` connection this is
     *  derived from the same ingress URL (or `rindle.wsUrl`) so a browser can open its transport from
     *  this lease and needs no application-authored runtime-config route. */
    wsEndpoint?: string;
    /** Fresh opaque follower-placement ticket minted with this follower-local lease. The optimistic
     *  client offers it on the WebSocket opened at {@link wsEndpoint}, pinning both legs to the same
     *  follower even though the first connection is created only after this response. */
    affinity?: string;
    /** The room-serve block (G-iv-b) — see {@link QueryLeaseRealtime}. Absent ⇒ the lease is
     *  byte-identical to the legacy daemon-served shape. */
    realtime?: QueryLeaseRealtime;
    /** The §4.2 downgrade fence (Slice I-v) — see {@link QueryLeaseRealtimeFence}. Present only on a
     *  labeled reply whose occupancy gate closed AND `realtime.lifecycle.drainRoom` could drain the
     *  room; absent otherwise (including on every room-served reply). */
    realtimeFence?: QueryLeaseRealtimeFence;
    /** The §4 lifecycle system-stream block (Slice I-iii) — see {@link QueryLeaseLifecycle}.
     *  Minted ONLY under the opt-in `realtime.lifecycle` config; absent ⇒ byte-identical to the
     *  pre-lifecycle response. */
    lifecycle?: QueryLeaseLifecycle;
}

QueryReadRequest

InterfaceDeclaration · Source: packages/api-server/src/index.ts:531 · Supporting declarations

A one-shot SSR read of a named query (SSR-DESIGN.md §6): same (name, args) surface as a lease, but the daemon serializes the current view ONCE and registers no subscriber.

export interface QueryReadRequest<User> {
    user: User;
    name: string;
    args: unknown;
    request?: unknown;
    /** The browser's stable `clientId` — the anonymous routing-key fallback (see
     *  {@link QueryLeaseRequest.clientId}). Lets the SSR read co-locate on the follower the booting
     *  client's first subscribe will hit (READ-ROUTER-DESIGN.md §2.4). */
    clientId?: string;
    /** The browser's opaque follower-affinity ticket — see {@link QueryLeaseRequest.affinity}.
     *  Forwarded on the one-shot `query` so an SSR read lands on the same pinned follower. */
    affinity?: string;
}

QueryReadResponse

InterfaceDeclaration · Source: packages/api-server/src/index.ts:547 · Supporting declarations

The assembled (nested-by-name) first-paint snapshot the server-side Store seeds + dehydrates (SSR-DESIGN.md §3.3). rows hydrate without an engine; cvMin is their watermark baseline.

export interface QueryReadResponse {
    rows: Array<{
        cols: Record<string, unknown>;
        [rel: string]: unknown;
    }>;
    cvMin?: number;
    queryKey?: string;
}

queryRealtimeLabel

FunctionDeclaration · Source: packages/api-server/src/rooms.ts:548 · Supporting declarations

The realtime label a registered query carries (stamped by registerQueries from its defineQuery options), or undefined for an unlabeled query. The lease path uses this to derive (room profile, args mapping) for a named query.

export declare function queryRealtimeLabel(query: ApiQuery<any, any> | undefined): RealtimeQueryLabel | undefined;

queryResultToAst

FunctionDeclaration · Source: packages/api-server/src/rooms.ts:42 · Supporting declarations

export declare function queryResultToAst(result: ApiQueryResult): Ast;

QueryShapesDoc

InterfaceDeclaration · Source: packages/api-server/src/index.ts:2490 · Supporting declarations

The query-shapes document rindle indices suggest consumes: the app's synced tables (name + primary key) and one wire AST per structurally distinct shape a registered query can build.

export interface QueryShapesDoc {
    tables: Array<{
        name: string;
        primaryKey: string[];
    }>;
    queries: Array<{
        name: string;
        ast: Ast;
    }>;
}

questionToDollarParams

FunctionDeclaration · Source: packages/api-server/src/index.ts:2322 · Supporting declarations

Rewrite SQLite-style ? positional placeholders to Postgres $1..$n, for mutators written once and run against either backend (pass as {@link PostgresBackendOptions.rewriteSql}). Skips '…' string literals (with '' escapes), "…" quoted identifiers, -- line comments, and non-nested C-style block comments. Do not mix ? and $n styles in one statement.

export declare function questionToDollarParams(sql: string): string;

RealtimeQueryLabel

InterfaceDeclaration · Source: packages/client/src/query.ts:372 · Supporting declarations

The realtime LABEL a named query may declare (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §2.1): which api-server room PROFILE the query wants to be served from, and how the query's args map to that profile's key args. This is the DECLARATION only — the serve decision (covering proof, lease routing) is the server's, and the final room key is minted server-side by the profile's own key under authoritative ctx, so a label can never place a query in a room the server didn't derive itself. Both tiers import the SAME defineQuery value, so they always agree which profile a query belongs to.

export interface RealtimeQueryLabel<Args = any> {
    /** The room profile name — must match a `realtime.rooms` key on the api-server (validated
     *  loudly at `createRindleApiServer` construction). May not contain `/` (the wire room-key
     *  delimiter). */
    readonly room: string;
    /** Map the query's VALIDATED args to the profile's key args (what the server feeds the
     *  profile's `key(args)`). Identity when omitted. Must be pure — both tiers may run it. */
    readonly args?: (queryArgs: Args) => unknown;
}

registerQueries

FunctionDeclaration · Source: packages/api-server/src/index.ts:2385 · Supporting declarations

Register a list of co-located client {@link NamedQuerydefineQuery} values as the server's query surface — the bulk, no-boilerplate counterpart to {@link defineApiQueries}. Each query already carries its wire name and a resolve that re-runs its validator on the UNTRUSTED wire args and builds the authoritative Query, so the server just imports every co-located query and hands the list here. The same validated args build a byte-identical AST on both tiers.

The query's AUTHORITATIVE {@link ApiContext} is forwarded as resolve's ctx — so a context-scoped defineQuery (e.g. "my issues") is built from the server's trusted principal, never the client's. A context-free query simply ignores the extra argument.

Use {@link defineApiQueries} instead (or in addition) only when the server must DIVERGE from the client — define a server-specific defineQuery with the same name and register that.

queries: registerQueries<User>([issuesPageQuery, issueDetailQuery, recentCommentsQuery, usersQuery]),
export declare function registerQueries<User>(queries: readonly NamedQuery<any, any, any>[]): ApiQueries<User>;

RenderIndex

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:1249 · Supporting declarations

export type RenderIndex = Record<string, TableRenderMeta>;

renderOp

FunctionDeclaration · Source: packages/api-server/src/index.ts:1302 · Supporting declarations

Render one {@link MutationOp} to a {sql, params} for the dialect, or null for a no-op (an update whose row names only pk columns — nothing to SET, matching the client's no-op edit).

export declare function renderOp(op: MutationOp, meta: TableRenderMeta, dialect: SqlDialect): SqlStatement | null;

renderPointRead

FunctionDeclaration · Source: packages/api-server/src/index.ts:1348 · Supporting declarations

Render a point read (tx.row) — SELECT <cols> FROM "T" WHERE <pk> — for read-your-writes.

export declare function renderPointRead(table: string, pk: KeyedRow, meta: TableRenderMeta, dialect: SqlDialect): SqlStatement;

RindleApiError

ClassDeclaration · Source: packages/api-server/src/index.ts:1108 · Supporting declarations

export declare class RindleApiError extends Error {
    readonly code: RindleApiErrorCode;
    readonly status: number;
    constructor(code: RindleApiErrorCode, message: string, status: number);
}

RindleApiErrorCode

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:1106 · Supporting declarations

export type RindleApiErrorCode = "bad-request" | "forbidden" | "not-found" | "rejected";

RindleApiRoutes

InterfaceDeclaration · Source: packages/api-server/src/index.ts:613 · Supporting declarations

export interface RindleApiRoutes {
    query: string;
    read: string;
    mutate: string;
    applyRowChangeTxn: string;
    claimRoomEpoch: string;
    roomLmids: string;
    roomBoot: string;
    stream: string;
}

RindleApiServer

InterfaceDeclaration · Source: packages/api-server/src/index.ts:1029 · Supporting declarations

export interface RindleApiServer<User> {
    readonly routes: RindleApiRoutes;
    /** Close the SQL client created from {@link RindleApiServerOptions.database}, and drop every live
     *  stream's readers and timers WITHOUT a durable write (that is {@link drainStreams}). Injected
     *  SQL sessions and custom backends remain caller-owned. Idempotent. */
    close(): void;
    /** Open an LM stream (LM-STREAM-CHECKPOINT §2): commits the durable POINTER row, then hands back
     *  the producer handle. A resolved handle means the message already exists for every client's
     *  query — so a subscriber that arrives before the first token has something to attach to.
     *  Throws 403 unless {@link RindleApiServerOptions.streams} is configured. */
    openStream(input: OpenStreamInput<User>): Promise<StreamHandle>;
    /** Attach a reader at `from` — the same call serves a first-touch subscriber (`from: 0`), a late
     *  joiner (`from` = the seq its IVM view shows), and a reconnect (`from` = `Last-Event-ID`).
     *  Terminates with `end`, or with `stale`/`absent` when the client should fall back to the
     *  durable plane (both are ordinary answers, never errors). */
    subscribeStream(input: SubscribeStreamInput<User>): Promise<StreamSubscription>;
    /** Parse a default `{streamId, from?}` subscribe body and run {@link subscribeStream}. For the
     *  GET + `EventSource` shape, use {@link streamResponse} (or build the body with
     *  `streamRequestFromHttp(request)` yourself). */
    handleStreamJson(body: unknown, context: ApiContext<User>): Promise<StreamSubscription>;
    /** The subscribe route in ONE call: parse a GET (`?streamId=…&from=…`, with `Last-Event-ID`
     *  winning), authorize + subscribe, and encode the SSE response. A refusal comes back as a JSON
     *  error `Response` (403 for denied or unconfigured) rather than a throw, so the route body is a
     *  single expression after authentication. For custom transports, compose
     *  `streamRequestFromHttp` + {@link subscribeStream} + `streamFramesToSse` instead. */
    streamResponse(request: {
        url: string;
        headers: {
            get(name: string): string | null;
        };
    }, context: ApiContext<User> & {
        keepAliveMs?: number;
    }): Promise<Response>;
    /** Checkpoint every live stream's outstanding tail, then seal it `interrupted`
     *  (LM-STREAM-CHECKPOINT §5). Wire it to SIGTERM: without it a rolling deploy drops each
     *  response's un-checkpointed tail and strands rows saying `streaming` forever. */
    drainStreams(): Promise<void>;
    createQueryLease(input: QueryLeaseRequest<User>): Promise<QueryLeaseResponse>;
    /** (Re-)materialize every `pinnedQueries` entry with a pinned policy. Idempotent — the daemon
     *  dedupes by canonical query, so a re-assert reuses the existing materialization. Call it at
     *  startup and whenever the daemon restarts (e.g. from the daemon-client `onBootId` hook), since
     *  the daemon holds no durable materialization state. No-op when `pinnedQueries` is empty. */
    assertPins(): Promise<void>;
    pushMutation(input: PushMutationRequest<User>): Promise<PushMutationResponse>;
    /** Apply an in-order batch (the client mutation queue's flush). Envelopes run strictly
     *  sequentially; a rejection still advances the daemon's lmid, so later envelopes in the
     *  batch stay contiguous and keep applying. A daemon ERROR throws for the whole batch —
     *  the client retries it and the daemon's mid dedup absorbs the already-applied prefix. */
    pushMutations(input: PushMutationsRequest<User>): Promise<PushMutationResponse[]>;
    /** One-shot SSR read (SSR-DESIGN.md §6): resolve `(name, args)` → AST (same authority path as a
     *  lease, `authorizeQuery` enforced), have the daemon serialize the current view once, and return
     *  the assembled rows for the loader to seed + dehydrate. Registers NO subscriber — a dropped
     *  render leaks nothing; the pipeline self-reclaims after the idle TTL ({@link
     *  RindleApiServerOptions.readIdleTtlMs}) unless the browser's follow-up `subscribe` lands first. */
    readQuery(input: QueryReadRequest<User>): Promise<QueryReadResponse>;
    handleQueryJson(body: unknown, context: ApiContext<User>): Promise<QueryLeaseResponse>;
    /** Parse a default `{name, args}` read body and run {@link readQuery}. */
    handleReadJson(body: unknown, context: ApiContext<User>): Promise<QueryReadResponse>;
    /** Accepts `{envelope}` (one) or `{envelopes: [...]}` (an in-order batch → array reply). */
    handleMutateJson(body: unknown, context: ApiContext<User>): Promise<PushMutationResponse | PushMutationResponse[]>;
    /** The room's flush (§5.3.1): gate on `authorizeRoom`, forward the txn to the write
     *  authority, and pass the store's verdict through VERBATIM — `200 {applied, cv}`,
     *  `409 {error:"fenced"|"conflict", …}`, or the loud identity `500`. Write
     *  `status` + `body` as-is; the room's `httpAuthority` decodes them. */
    handleApplyRowChangeTxnJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
    /** Claim the next placement epoch for a doc (§2.5), same gate + envelope. */
    handleClaimRoomEpochJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
    /** The room's boot probe (§3.3), same gate + envelope. */
    handleRoomLmidsJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
    /** The DO shell's cold-boot callback (§10.1; enablement §3.1): authenticate the shell
     *  secret, resolve the doc's footprint, claim the placement epoch, mint the upstream
     *  lease and the flush leg. 403 until {@link RindleApiServerOptions.realtime} is set. */
    handleRoomBootJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
}

RindleApiServerOptions

InterfaceDeclaration · Source: packages/api-server/src/index.ts:930 · Supporting declarations

export interface RindleApiServerOptions<User> {
    /** The sync/IVM control-plane client (leases, materialization, rooms). Optional once
     *  {@link rindle} is configured — the api-server then derives an HTTP client against the single
     *  ingress. Pass one explicitly for a tokened production fleet, a split/routed deployment, or a
     *  custom transport; an explicit client wins over the derivation. */
    daemon?: RindleDaemonClient;
    /** The unified connection (one URL, one key) that derives {@link daemon} and {@link database}
     *  from the fleet's single ingress. `rindle: {}` resolves both halves from the `rindle dev`
     *  environment. Any explicitly configured `daemon` / `database` / `sql` / `backend` field takes
     *  precedence over its derived counterpart. */
    rindle?: RindleConnectionOptions;
    /** Preferred managed setup. The API server constructs and owns its SQL client; authoritative
     *  mutators, `tx.sql`, and `scope.sql` use it, while `daemon` remains only the lease/query/
     *  materialization/room control plane. Mutually exclusive with {@link sql} unless `backend`
     *  explicitly replaces both. */
    database?: RindleDatabaseOptions;
    /** Advanced injection/test seam for an already-created SQL session. Most applications should
     *  configure {@link database} and never import `createSqlClient`. When present (and `backend` is
     *  absent), authoritative mutators run through {@link sqlBackend}. */
    sql?: SqlSession;
    /** Where mutations are applied and `lmid` is stamped ({@link MutationBackend}). Default:
     *  managed `sqlBackend` when `database` or `sql` is configured, otherwise the compatibility
     *  `daemonBackend(daemon)`. Pass `postgresBackend(...)` when Postgres is the source of truth. */
    backend?: MutationBackend;
    /** The typed schema (`createSchema`/`refineSchema`). Required only when a mutator uses the LOGICAL
     *  write vocabulary (`tx.insert`/`update`/`upsert`/`insertIgnore`/`delete`/`row`) — it drives the
     *  dialect SQL renderer (column order, pk, quoting). A logical op with no schema configured throws
     *  loudly. Pure raw-`tx.exec` mutators do not need it. */
    schema?: Schema;
    queries?: ApiQueries<User>;
    runQuery?: RunQuery<User>;
    mutators?: ApiMutators<User>;
    /** Optional gate before resolving a named query, for leases and one-shot reads. If omitted,
     *  no gate runs. The query definition still owns row visibility predicates. */
    authorizeQuery?: Authorizer<AuthorizeQueryInput<User>>;
    /** Optional gate before mutation execution. If omitted, no gate runs. Mutators still own
     *  row-level checks and business rules inside their authoritative transaction. */
    authorizeMutation?: Authorizer<AuthorizeMutationInput<User>>;
    /** Rindle Realtime (RINDLE-REALTIME-ENABLEMENT-DESIGN.md §3.1): the ONE named opt-in.
     *  Presence activates the room flush trio AND `/room-boot`; absence keeps every room
     *  endpoint 403. Takes precedence over the deprecated {@link authorizeRoom}. */
    realtime?: RindleRealtimeOptions<User>;
    /** The LM stream plane (LM-STREAM-CHECKPOINT-DESIGN.md): the ONE named opt-in for streaming a
     *  model response live while the durable store only ever sees coarse checkpoints. Presence
     *  activates {@link RindleApiServer.openStream}/{@link RindleApiServer.subscribeStream} and
     *  `/stream`; absence keeps them 403. */
    streams?: RindleStreamOptions<User>;
    /** The room write-authority gate (§5.3.1): validates the caller is a placed room —
     *  the epoch-bound flush credential rides `context.request`, and what it means is
     *  the app's to define. The room endpoints are DISABLED (403) until this is set:
     *  hosting a write authority is an explicit opt-in, never a default.
     *  @deprecated Use {@link realtime} (its `authorize`) — this bare form gates the
     *  flush trio but never activates `/room-boot`. When both are set, `realtime` wins. */
    authorizeRoom?: Authorizer<ApiContext<User>>;
    routes?: Partial<RindleApiRoutes>;
    mode?: StreamMode;
    materializationPolicy?: MaterializationPolicy | ((input: QueryLeaseRequest<User>) => MaybePromise<MaterializationPolicy>);
    leaseTtlMs?: number;
    /** Idle TTL (ms) the warm pipeline a one-shot SSR {@link RindleApiServer.readQuery read} leaves
     *  behind is held at (SSR-DESIGN.md §3.4) — it must comfortably cover page-load + client-boot +
     *  the follow-up live `subscribe` so the browser lands on a still-warm pipeline (the warm
     *  handoff). The TTL is NOT part of the dedup key (max-wins), so it only ever extends a shared
     *  query's window. Absent ⇒ the daemon's default idle TTL. */
    readIdleTtlMs?: number;
    subject?: string | ((input: QueryLeaseRequest<User>) => MaybePromise<string | undefined>);
    /** The ANONYMOUS routing key forwarded to the read router (READ-ROUTER-DESIGN.md §2.2) — used by
     *  HRW placement when there is no authenticated `subject`. The router keys on `subject ?? this`;
     *  the resolved value rides `metadata.routingKey` to the daemon. Default: the browser-supplied
     *  `clientId` (from the query POST body). NOTE: the default cannot co-locate an ANONYMOUS SSR read
     *  with the booting client — an SSR server can't see the browser's localStorage `clientId`, so the
     *  two legs compute different keys and §2.4's warm handoff misses (still correct — just an extra
     *  first-touch materialize). For anonymous SSR co-location, set this to read a server-set session
     *  cookie from `input.request` (it rides the SSR request AND every browser request). A routing
     *  HINT only — never authorization. Ignored by a single (unrouted) daemon, which has nothing to
     *  route. */
    routingKey?: string | ((input: QueryLeaseRequest<User>) => MaybePromise<string | undefined>);
    /** The EXPLICIT fleet pin fan-out — when set, {@link RindleApiServer.assertPins} fans each
     *  resolved pin across ALL live followers through it (a fleet control action over the machine
     *  list — FOLLOWER-AFFINITY-DESIGN.md §11) instead of materializing each pin once on the (single)
     *  daemon. A per-viewer `materialize` always routes ONE; a pin-assert always fans ALL — never
     *  inferred from `policy.kind`. Absent ⇒ single-daemon behavior (one materialize per pin). */
    pinFanout?: PinFanout;
    /** Named queries to keep permanently materialized via {@link RindleApiServer.assertPins}.
     *  Each is materialized with a `pinned` policy (survives zero subscribers) so late joiners
     *  attach to an already-warm result. Pins are viewer-independent — resolved with `pinUser`. */
    pinnedQueries?: PinnedQuery[];
    /** The user context pins resolve under (pins are shared, so they should not depend on a
     *  per-viewer identity). Defaults to `undefined`. */
    pinUser?: User;
    /** Surfaced when a SCOPED mutator ({@link scoped}) throws from code that runs AFTER `scope.transact`
     *  has already sealed the protocol outcome — a post-commit effect, or a compensation handler running
     *  after a business rejection. The outcome is fixed (this callback CANNOT change the client's
     *  response or the `lmid` advance), but the throw must not vanish: a failed refund is real money.
     *  Absent ⇒ the error is logged to `console.error`. */
    onScopeError?: (err: unknown, info: {
        phase: "committed" | "rejected";
        envelope: MutationEnvelope;
    }) => void;
}

RindleConnectionOptions

InterfaceDeclaration · Source: packages/api-server/src/index.ts:917 · Supporting declarations

The unified fleet connection — one URL, one key. A Rindle is BOTH layers at once: the SQL database below and the sync/IVM control plane above, served by a single ingress that routes each request to the right tier. This option derives both from that one origin: the SQL layer ({@link RindleApiServerOptions.database}) at url with token, and the control-plane client ({@link RindleApiServerOptions.daemon}) at the same url — so an application configures "a rindle", not two subsystems.

The derived control-plane client sends the same bearer. A unified ingress is an explicitly merged customer API-server trust tier: it routes SQL to the master and control requests to a follower, while the browser still receives neither credential. Deployments that keep those trust tiers separate pass an explicit {@link RindleApiServerOptions.daemon}; explicit fields always win over this derivation.

export interface RindleConnectionOptions {
    /** The single ingress origin. Defaults to `$RINDLE_URL` — exported by `rindle dev` whenever
     *  the rendered topology collapsed read+write onto one ingress. */
    url?: string;
    /** Public subscription WebSocket endpoint returned on query leases. Defaults to {@link url} with
     *  `http:` → `ws:` / `https:` → `wss:`. Override when HTTP and WebSocket ingress differ. */
    wsUrl?: string;
    /** The server-side bearer for both legs of the unified ingress. Defaults to
     *  `$RINDLE_DATABASE_TOKEN` (also a `rindle dev` export); required through one of those channels
     *  unless both legs are configured explicitly. It never reaches the browser. */
    token?: string;
}

RindleDatabaseOptions

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:903 · Supporting declarations

Database connection used by the API server's managed SQL path.

intMode defaults to "number" because logical Rindle rows use the JSON-safe {@link WireValue} vocabulary — "bigint" does not survive JSON.stringify, and "string" silently retypes every integer, breaking arithmetic in mutator bodies. The cost is a HARD BOUND: a mutator read of an integer outside ±(2^53 − 1) rejects that mutation rather than silently rounding it. Tables with keys beyond that range (snowflake ids, and so on) must override intMode and have their mutators handle the resulting type. Commit receipts are unaffected — they never decode row values.

export type RindleDatabaseOptions = Pick<SqlClientOptions, "url" | "authToken" | "fetch" | "intMode">;

RindleRealtimeLifecycleOptions

InterfaceDeclaration · Source: packages/api-server/src/index.ts:759 · Supporting declarations

{@link RindleRealtimeOptions.lifecycle}. PRESENCE of the block is the opt-in switch (I-iii); the fields below are the Slice I-iv occupancy knobs (§4.1, decisions D4/D6/D7). All optional — lifecycle: {} gets the designed defaults.

export interface RindleRealtimeLifecycleOptions {
    /** D6 (§4.1): the occupancy threshold for room-serving. A labeled lease whose scope counts
     *  FEWER than this many distinct unexpired sessions (the caller's own included) ships WITHOUT
     *  the realtime block — served from the daemon, indistinguishable from an uncovered query —
     *  but WITH the doorbell, so the 1→2 transition wakes it (that is the point: solo docs never
     *  cost room infrastructure). Default **2** (the design's 1→2 trigger). Set `1` to room-serve
     *  solo viewers (the pre-I-iv behavior under lifecycle config). */
    minSessions?: number;
    /** The §9.1 hysteresis window, ms (default **120_000**). Two consumers: (a) the lazy sweep
     *  (D4) keeps expired session rows lingering at least this long past expiry — Slice I-v's
     *  downgrade decision ("no other unexpired row AND the newest other row expired > graceMs
     *  ago") is read FROM those rows, so they must survive to be read; (b) this slice's gate
     *  applies the same hysteresis upward: a scope with an other-session row expired ≤ graceMs
     *  ago keeps room-serving through the window (see `lifecycleOccupancy` — no flap on one
     *  client's brief lapse). §9.1-tunable: raise it for docs where collaborators churn slowly. */
    graceMs?: number;
    /** TTL of an occupancy session row, ms — `expires_at = now + sessionTtlMs` on every labeled
     *  lease mint/renewal (D7: session identity = the request's `clientId`; two tabs are two
     *  sessions iff their clientIds differ). Default = the server's `leaseTtlMs`, else 5 minutes —
     *  matching the room-token renewal cadence (`roomTokenTtlMs`, renewed 30s early), so a
     *  room-attached client's renewals keep its row unexpired; a daemon-attached solo client's row
     *  MAY lapse (it has no renewal timer) and is refreshed by its next doorbell-triggered
     *  re-lease — occupancy converges through the doorbell itself. */
    sessionTtlMs?: number;
    /** The §4.2 downgrade drain hook (Slice I-v). When the occupancy gate CLOSES for a labeled
     *  lease whose scope PLAUSIBLY hosted a room (an other-session row still lingers — never a
     *  never-shared solo doc), the api-server calls this to drain the room's pending write-behind
     *  and learn its last COMMITTED `flush_seq`, then rides the value back on the lease as the
     *  {@link QueryLeaseRealtimeFence}. The deployment wires it to the room shell's / DO's `/drain`
     *  control. Absent ⇒ no fence is attached (the client hits its loud legacy downgrade path);
     *  a throw fails OPEN to the same (a downgrade never blocks the lease). Concurrent drains across
     *  api-server instances are fine — `/drain` is idempotent. */
    drainRoom?: (doc: string) => Promise<{
        finalFlushSeq: number;
    }>;
}

RindleRealtimeOptions

InterfaceDeclaration · Source: packages/api-server/src/index.ts:685 · Supporting declarations

export interface RindleRealtimeOptions<User> {
    /** The boot shell secret (§10.1 — a host binding, never client-derived). Authenticates the
     *  room's `Authorization: Bearer` on `/room-boot` (the default {@link authorizeBoot}) and keys
     *  the DEFAULT epoch-bound flush credential. */
    shellSecret: string;
    /** NAMED room profiles (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §2.1): profile name → key
     *  derivation + canonical unwindowed footprint + read-only context tables. The wire room key
     *  for a named profile is `"<profile>/<key>"`; `/room-boot` splits it and resolves the
     *  profile's footprint with the bare key. Compiled + validated LOUDLY at construction (§2.3):
     *  a windowed footprint, a context table missing from the schema/footprint, or a registered
     *  query whose realtime label names a missing profile all throw from `createRindleApiServer`. */
    rooms?: Record<string, RoomProfile<User>>;
    /** doc → the room's approved upstream footprint (§3.1) — an `Ast` or fluent `Query`. MAY
     *  delegate to the named-query registry internally; throw `RindleApiError("not-found", …, 404)`
     *  for a doc that shouldn't exist. The §9 footprint budget belongs here — it runs once per
     *  placement, at lease mint.
     *  @deprecated Prefer named {@link rooms} profiles (RINDLE-REALTIME-QUERY-ENABLEMENT §2.1).
     *  This bare form remains as the single-profile LEGACY alias — the anonymous/default profile:
     *  a doc with no known `"<profile>/"` prefix resolves here, byte-identically to before named
     *  profiles existed (and with none of their construction/boot-time validation). */
    resolveFootprint?: (doc: string, ctx: ApiContext<User>) => MaybePromise<ApiQueryResult>;
    /** Gates the flush trio — `authorizeRoom`, relocated. Default: verify the default flush
     *  credential from the {@link ROOM_FLUSH_CREDENTIAL_HEADER} request header — which requires the
     *  transport to pass its incoming request as `context.request` (Fetch `Request` and node
     *  `IncomingMessage` shapes are both understood). */
    authorize?: Authorizer<ApiContext<User>>;
    /** Gates `/room-boot`. Default: constant-time `Authorization: Bearer` check against
     *  {@link shellSecret} (same `context.request` requirement as {@link authorize}). */
    authorizeBoot?: Authorizer<ApiContext<User>>;
    /** Mint the flush headers a placed room presents on every flush call. Default:
     *  {@link mintRoomFlushCredential} under {@link ROOM_FLUSH_CREDENTIAL_HEADER}. Override this
     *  and {@link authorize} TOGETHER — they are the two ends of one credential. */
    mintFlushHeaders?: (input: {
        doc: string;
        epoch: number;
    }) => MaybePromise<Record<string, string>>;
    /** Lease TTL for the room's upstream footprint materialization (defaults to the server-wide
     *  `leaseTtlMs`, else the daemon's default). */
    upstreamLeaseTtlMs?: number;
    /** Static endpoint where rooms open their upstream subscription. In a follower fleet this is the
     *  fleet ws URL; `/room-boot` pairs it with the materialization's fresh placement ticket so the
     *  room lands on the exact follower holding its lease. Absent ⇒ no explicit upstream (the Node
     *  room shell may use its own default; the shipped DO shell requires this endpoint). */
    upstreamWsEndpoint?: string;
    /** Locate (or place) the room serving `doc` and return the ROOM ws endpoint a room-served
     *  lease's client should open (G-iv-b; on the DO shell this is the Worker's room URL). The
     *  endpoint rides the lease's dedicated `realtime.wsEndpoint` — its OWN connection, distinct from
     *  the daemon session's fixed ws host. Absent ⇒ room-serving is OFF: labeled queries serve from
     *  the daemon exactly as today (fail-open). */
    locateRoom?: (doc: string) => MaybePromise<{
        wsEndpoint: string;
    }>;
    /** The room lease token signing key (`@rindle/room/token`): `kid` + secret, matching an entry
     *  in the room shell's `downstream.tokenKeys` ring. Required for room-serving (without it a
     *  labeled query fail-opens to the daemon with a one-time warning). A separate secret from
     *  `shellSecret` on purpose — the shell's ring is the client-token trust domain, the shell
     *  secret is the boot/flush trust domain. */
    roomTokenKey?: {
        kid: string;
        secret: string;
    };
    /** Room lease token TTL, ms (default 5 minutes — the §4.1 short-TTL backstop; renewal is a
     *  fresh lease through this server, never an extension). */
    roomTokenTtlMs?: number;
    /** Loud-diagnostics sink for the realtime layer (profile compilation warnings + the one-time
     *  per-(query, profile) "not room-served" serve-decision warnings). Defaults to
     *  `console.warn`; injectable for tests. */
    warn?: (message: string) => void;
    /** The §4 upgrade/downgrade lifecycle plane (RINDLE-REALTIME-QUERY-ENABLEMENT §4, Slice
     *  I-iii): PRESENCE of this block is the opt-in — every realtime-labeled lease then
     *  additionally mints the doorbell system lease (occupancy, §4.1) and every ROOM-SERVED lease
     *  the fence bundle (watermark + ledger + outcomes, §4.2/§7.1/§3.3) — see
     *  {@link QueryLeaseLifecycle}. Requires the daemon to have run `enable_realtime_lifecycle`
     *  (the four `_rindle_*` system tables must be registered or the minted materializations fail
     *  — which fail-opens with a one-time warning, never blocking the lease). Absent ⇒ the lease
     *  response is byte-identical to pre-lifecycle. */
    lifecycle?: RindleRealtimeLifecycleOptions;
}

RindleStreamOptions

InterfaceDeclaration · Source: packages/api-server/src/streams.ts:214 · Supporting declarations

export interface RindleStreamOptions<User> {
    /** Where checkpoints land: the app's tables (the default path) or a raw `commit` callback. */
    checkpoint: StreamCheckpointTarget;
    /** REQUIRED. Subscribing to a stream is reading someone's chat, so there is no default-allow.
     *  Runs BEFORE existence is checked, so a denial cannot be used to probe for stream ids. */
    authorize: Authorizer<AuthorizeStreamInput<User>>;
    policy?: StreamCheckpointPolicy;
    /** This process's identity — it must be UNIQUE per producer process, because the open CAS trusts
     *  it to distinguish rivals (§5.1). In `tables` mode, map {@link StreamColumns.host} and the open
     *  write persists it on the message row (so the app can route later subscribers to the hosting
     *  instance, §4) and uses it as the single-flight token; setting it WITHOUT a mapped `host` column
     *  is refused at construction. In `commit` mode it rides the `open` input. When a `host` column is
     *  mapped and no hostId is given, a random per-plane token is used — the CAS still holds, routing
     *  just has no stable name to read. */
    hostId?: string;
    /** Slack retained BELOW `durableSeq` so a client whose IVM view lags a checkpoint can still join
     *  without a `stale` round trip. Text at or above `durableSeq` is never trimmed. Default 64 KiB.
     *  `commit`-mode only — REFUSED (a construction-time `TypeError`) in `tables` mode, where
     *  compaction needs the whole produced text at close (§3.4), so the buffer is retained in full. */
    retainChars?: number;
    /** How long a sealed stream stays joinable before eviction. Default 30s. */
    lingerMs?: number;
    /** Per-subscriber frame queue cap; overflow drops that subscriber with `stale` (§4). Default 1024.
     *  Relayed readers reuse the same bound: a slow reader on a relayed stream costs itself the live
     *  leg exactly as a local one does. */
    maxQueuedFrames?: number;
    /** A checkpoint that exhausted its retries. The stream keeps streaming — this is a durability
     *  stall, not a stream stall — so the error must not vanish. Absent ⇒ `console.error`. */
    onCheckpointError?: (err: unknown, info: {
        streamId: string;
        from: number;
        seq: number;
    }) => void;
    /** Cross-process transport for the live plane ({@link StreamRelay}). Without one, a subscriber
     *  that lands on a process not hosting its stream gets `absent` and reads the durable plane at
     *  checkpoint granularity — correct, just chunky. */
    relay?: StreamRelay;
    /** Bound on `relay.attach`: a hung adapter yields `absent`, not a hung HTTP request. An
     *  addressing adapter MAY deliberately spend this window waiting out a subscribe that races its
     *  own kick. Default 2000. */
    relayAttachTimeoutMs?: number;
    /** A diagnostic, never a control path: relay failures (a throwing or rejecting `publish`, a failed
     *  or timed-out `attach`, a conform violation in the frames) land here, wrapped so a throwing hook
     *  cannot reach the plane. The reader-facing outcome is always the same legal `absent`/`stale`.
     *  Absent ⇒ `console.error`. */
    onRelayError?: (err: unknown, info: StreamRelayErrorInfo) => void;
}

ROOM_FLUSH_CREDENTIAL_HEADER

VariableDeclaration · Source: packages/api-server/src/index.ts:802 · Supporting declarations

export declare const ROOM_FLUSH_CREDENTIAL_HEADER = "x-rindle-room-credential";

RoomBootFlush

InterfaceDeclaration · Source: packages/api-server/src/index.ts:658 · Supporting declarations

The room-boot flush leg (enablement §5): where the placed room's write-behind lands and what credential it presents. urls are the trio's ROUTE PATHS (root-relative — the shell resolves them against the boot call's origin), so an app that overrides routes needs no out-of-band sync; headers ride every flush call verbatim (httpAuthority's headers).

export interface RoomBootFlush {
    urls: {
        apply: string;
        claim: string;
        lmids: string;
    };
    headers: Record<string, string>;
}

RoomBootResponse

InterfaceDeclaration · Source: packages/api-server/src/index.ts:666 · Supporting declarations

The /room-boot response (RINDLE-REALTIME-DESIGN.md §10.1 — the DO shell's cold-boot callback): the claimed placement epoch, the room's upstream footprint lease, and the flush leg. A cold room boots inert and serves nothing until this returns.

export interface RoomBootResponse {
    epoch: number;
    upstreamLeaseToken: string;
    /** Where the room opens its upstream subscription (a routed deploy's follower). Absent ⇒ the
     *  shell's statically configured rindled ws endpoint. */
    upstreamWsEndpoint?: string;
    /** Fresh opaque follower-placement ticket minted alongside `upstreamLeaseToken`. The DO offers
     *  it with `rindle.v1` on the separate upstream ws so a static fleet endpoint replays to the
     *  exact follower holding that local lease. Absent when daemon affinity is off. */
    upstreamAffinity?: string;
    /** Per-footprint-table scope specs (H-iv-b), compiled from the resolved footprint AST + the
     *  profile's context set (the legacy anonymous profile compiles with an empty context set):
     *  what the shell hands the wasm room's `enableWritesV2` — the §3.3 commit gate. Optional
     *  only for wire compatibility with pre-H-iv-b servers; a shell that doesn't receive them
     *  enables the v1 table-granular write plane exactly as before. */
    scopes?: RoomScopeSpec[];
    flush: RoomBootFlush;
}

RoomFlushCredentialPayload

InterfaceDeclaration · Source: packages/api-server/src/index.ts:806 · Supporting declarations

export interface RoomFlushCredentialPayload {
    v: 1;
    doc: string;
    epoch: number;
    iat: number;
}

RoomHostResponse

InterfaceDeclaration · Source: packages/api-server/src/index.ts:628 · Supporting declarations

A room-host reply the transport writes VERBATIM (status + JSON body): the daemon's fence/conflict/identity semantics ride specific statuses and body shapes the room's httpAuthority decodes, so this endpoint trio can't run through the throw-on-error result shapes the viewer endpoints use.

export interface RoomHostResponse {
    status: number;
    body: unknown;
}

RoomProfile

InterfaceDeclaration · Source: packages/api-server/src/rooms.ts:26 · Supporting declarations

One NAMED room profile (§2.1). Rooms are document-scoped, not query-scoped: a profile is what stands up and feeds a room — the flat resolveFootprint promoted to a named, reusable unit. Labeled queries opt into a profile; the wire room key for a named profile is minted SERVER-side as "<profile>/<key>", and /room-boot splits it back to resolve the footprint.

export interface RoomProfile<User = unknown> {
    /** Derive the profile-local room key from the (label-mapped) query args. Runs server-side under
     *  authoritative inputs — the client learns the key from the lease, it never computes one. */
    key: (args: any) => string;
    /** Build the room's canonical footprint — the replica boundary the room loads and follows —
     *  from the profile-local doc key (this profile's `key` output; what `/room-boot` receives
     *  after the prefix split). MUST be unwindowed (§2.3): no `limit`/`start`/`one` anywhere in the
     *  AST — enforced loudly at construction when statically resolvable, and again at every boot.
     *  Should be a TOTAL builder over any key (a shape constructor, not an authorizer). */
    footprint: (docKey: string, ctx: ApiContext<User>) => MaybePromise<ApiQueryResult>;
    /** Loaded-but-never-room-written tables (§2.2 — the "followed" set: real external writers, the
     *  daemon stays write-authoritative for them). Must be a subset of the footprint's tables; the
     *  room's writable scope is footprint minus context. Defaults to `[]`. */
    context?: readonly string[];
}

RoomScopeSpec

TypeAliasDeclaration · Source: packages/api-server/src/rooms.ts:249 · Supporting declarations

One footprint table's FULL scope spec (slice H-iv-b): byte-compatible with rindle-room-core's TableScopeSpec (scope.rs), the wasm room's enableWritesV2 input. It rides the boot wire (RoomBootResponse.scopes); since slice H-iii the LEASE wire's {@link RoomTableSpec} carries footprintWhere too (the client-side routing proof), so the two shapes are now structurally IDENTICAL — kept as two names because they are two wires with two consumers (the room gate vs the client router).

footprintWhere is computed for EVERY footprint table — context (kind: "none") ones included, because the gate proves ABSENT READS on any readable table with it (§3.1: an absent read is covered only when the footprint predicate is decidable from the pk columns alone and evaluates true on the read key). ABSENT (no exact membership predicate exists) ⇒ no absent read on that table is provable — the room gate fails closed to a deopt.

footprintWhere is EXACT-only — the opposite discipline from the writable where. The writable side ships the WIDENING extraction ({@link tablePredicate}) and that is sound (the held-row set is the real outer bound). The absent-read proof points the other way: it uses footprintWhere to conclude "a row with this pk would have been IN the footprint, so the room's absence is truth's absence" — a WIDENED predicate over-claims exactly there (a root where mixing pk-column conjuncts with a dropped EXISTS, or a correlated child whose own where reads only its pk, would prove an absence the dropped part can contradict). So footprintWhere is emitted ONLY when the membership predicate is exact ({@link exactMembershipPredicate}): every node reading the table is the footprint ROOT (a child node's implicit correlation to its parent is itself a dropped, non-row-local constraint) and the root's where extraction dropped nothing. An unconstrained exact root (the whole table rides the footprint) emits the vacuous-true {type:"and",conditions:[]} — the gate's combinator pins empty-AND = true — so whole-table footprints keep provable absent reads. Child/correlated tables get NO footprintWhere and their absent reads deopt (fail closed); propagating a parent constraint through a pk-covering correlation is a future refinement.

export type RoomScopeSpec = {
    table: string;
    footprintWhere?: Condition;
    writable: RoomTableSpec["writable"];
};

RoomTableSpec

TypeAliasDeclaration · Source: packages/api-server/src/rooms.ts:211 · Supporting declarations

One footprint table's spec on the lease wire (QueryLeaseResponse.realtime.tables): the §2.2 owned/followed split plus §3.2's per-table routing metadata, compiled from the RESOLVED footprint AST at lease time (so key-dependent and non-static profiles compile too).

writable:

  • none — a context table (§2.2 "followed"): loaded by the room, never room-written; the daemon stays write-authoritative.
  • predicate — a writable table. where is the ROW-LOCAL part of the footprint's predicate for this table (simple column-vs-literal conditions composed with and/or); an ABSENT where means no row-local constraint — every row the room holds for this table is in the writable scope. joinKeyCols are the correlation columns the footprint binds on this table (slice H's "room mutators never write join keys" enforcement input).

Dropping the non-row-local parts (correlated/EXISTS conditions, column-vs-column comparisons) only ever WIDENS the predicate, which is sound here: the room only relays rows the footprint materialized, so the held-row set — not this predicate — is the real outer bound; where is a row-local refinement of it, and a wider refinement can never mark a held footprint row non-writable that the full predicate would have allowed. (An OR with any non-row-local disjunct is dropped WHOLE — keeping only some disjuncts would narrow, which is not sound.)

footprintWhere (H-iii — the lease-wire flip): the EXACT footprint-membership predicate, identical to {@link RoomScopeSpec.footprintWhere} (ONE compiler feeds both wires — the lease's table specs and the boot wire's scopes are now the SAME objects). The client's §3 router consumes it for the pk-membership read proof; it is advisory routing metadata, never a credential (the room gate re-proves engine-side, H-iv).

export type RoomTableSpec = {
    table: string;
    footprintWhere?: Condition;
    writable: {
        kind: "none";
    } | {
        kind: "predicate";
        where?: Condition;
        joinKeyCols: string[];
    };
};

RunQuery

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:200 · Supporting declarations

export type RunQuery<User> = (input: RunQueryInput<User>) => MaybePromise<ApiQueryResult>;

RunQueryInput

InterfaceDeclaration · Source: packages/api-server/src/index.ts:192 · Supporting declarations

export interface RunQueryInput<User> {
    user: User;
    name: string;
    args: unknown;
    query: ApiQuery<User, any>;
    context: ApiContext<User>;
}

runSharedMutation

FunctionDeclaration · Source: packages/api-server/src/index.ts:3708 · Supporting declarations

Run a SHARED generator mutator (the SAME body the client predicts) against a live server transaction (MUTATORS-ISOMORPHIC): bind the tier-agnostic {@link isoTx} factory and drive it — each yielded logical op renders + runs against tx (dialect SQL, per backend), each tx.row suspends for read-your-writes, and tx.all fans out. A server mutator uses this to delegate its write body after parsing untrusted args and applying its server-only authority (principal, policy). A mutator-body throw remains a business rejection; a DB failure propagates as infra.

export declare function runSharedMutation<Args, Ctx extends MutatorCtx>(mutator: SharedMutator<Args, Ctx>, args: Args, ctx: Ctx, tx: ServerMutationTx): Promise<void>;

scoped

FunctionDeclaration · Source: packages/api-server/src/index.ts:390 · Supporting declarations

Mark a mutator as SCOPED so the api-server gives it a {@link MutationScope} (author-controlled tx boundary via scope.transact) rather than running its whole body inside the transaction. Register it alongside the tx-form mutators — it wins by key like any override:

mutators: defineApiMutators({
  ...sharedApiMutators(sharedMutators, sharedCtx),        // tx-form (common case)
  createOrder: scoped(async (scope, raw, ctx) => {        // needs outside-tx work
    const args = createOrder.args.parse(raw);
    const chargeId = await stripe.charge(args.amount, { idempotencyKey: ctx.envelope.mid }); // outside tx
    try {
      await scope.transact(createOrder, args, { ...sharedCtx(ctx), chargeId });              // inside tx
    } catch (e) {
      await stripe.refund(chargeId);                       // compensate — the write rejected
      throw e;
    }
    await sendReceipt(ctx.user);                           // after commit
  }),
}),
export declare function scoped<User, Args>(fn: ScopedMutator<User, Args>): ScopedApiMutator<User, Args>;

ScopedApiMutator

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:367 · Supporting declarations

A {@link ScopedMutator} tagged by {@link scoped} so the harness invokes it with a {@link MutationScope}. Typed as a BRANDED tx-form {@link ApiMutator} purely so it registers in the mutators record without widening it to a union (which would break contextual inference for every plain tx-form entry). Its true runtime shape is (scope, args, ctx); the tag — not the type — routes it, and it is never actually called as a tx-form mutator.

export type ScopedApiMutator<User, Args> = ApiMutator<User, Args> & {
    readonly __rindleScoped: true;
};

ScopedMutator

TypeAliasDeclaration · Source: packages/api-server/src/index.ts:356 · Supporting declarations

A SCOPED server mutator (WORK-OUTSIDE-TX): server-only code, ONE scope.transact, optional post-commit code. Register it by wrapping in {@link scoped} — the tag the api-server routes on to hand it a {@link MutationScope} instead of running its whole body inside the transaction.

export type ScopedMutator<User, Args> = (scope: MutationScope, args: Args, ctx: MutationContext<User>) => void | Promise<void>;

ServerMutationTx

InterfaceDeclaration · Source: packages/api-server/src/index.ts:259 · Supporting declarations

The write handle a server mutator runs against — the ASYNC twin of the client's MutationTx. It is both the isomorphic {@link ServerWriteTx} logical surface (insert/update/upsert/insertIgnore/ delete/row, rendered to dialect SQL) AND the legacy {@link SqlMutationTx} raw escape hatch. Both implementations run reads through the OPEN transaction (read-your-writes): Postgres executes everything live; the SQL-client and daemon adapters accumulate writes and lazily upgrade to an interactive mutation session at the first read (DAEMON-INTERACTIVE-TXN-DESIGN.md §5).

export interface ServerMutationTx extends ServerWriteTx, SqlMutationTx {
    /** Run a full query (a fluent `Query` or its wire `Ast`) INSIDE the open transaction —
     *  read-your-writes, like {@link ServerWriteTx.row} but for arbitrary shapes. Returns the
     *  parsed nested result tree: an array for a plural root, an object or `null` for a `.one()`
     *  root, with cells in their raw SQLite storage-class representations (the same vocabulary
     *  `row` speaks). Remote SQLite backends: compiled by `@rindle/query-compiler`'s sqlite dialect
     *  (bind params, NO casts — §5.4) and executed through the mutation session. Postgres: lands with
     *  the read-compiler catalog integration (POSTGRES-READ-COMPILER-DESIGN.md Phase B). */
    query(q: Ast | Query<any, any, any>): Promise<unknown>;
}

ServerSql

InterfaceDeclaration · Source: packages/api-server/src/index.ts:230 · Supporting declarations

A deliberately narrow raw-SQL facade exposed by the API server. On {@link ServerMutationTx} it is bound to the open mutation transaction; on {@link MutationScope} each call runs in its own transaction outside the mutation boundary. Column aliases should be unique: positional driver rows are keyed by column name, so a duplicate alias is represented by its last value.

export interface ServerSql {
    /** Queue/execute one statement. A transaction-bound call commits with the surrounding mutation. */
    execute(sql: string, params?: readonly WireValue[]): Promise<void>;
    /** Queue/execute an ordered statement batch. An empty batch is a no-op. On an
     *  outside-transaction surface ({@link MutationScope.sql}, `backend.outsideSql`) the batch MUST
     *  execute as ONE atomic transaction — all statements or none. Every built-in backend does (the
     *  daemon's `execute-sql-txn`, the sql-client's `/v1/sql/batch`, the Postgres plugger's
     *  BEGIN/COMMIT); a custom backend that loops statements without a transaction silently breaks
     *  the stream plane's chunk+CAS and compaction invariants, which ride single `batch` calls. */
    batch(statements: readonly SqlStatement[]): Promise<void>;
    /** Run a read and return rows keyed by their column names. */
    query<Row = Record<string, unknown>>(sql: string, params?: readonly WireValue[]): Promise<Row[]>;
}

ShapeExemplar

InterfaceDeclaration · Source: packages/api-server/src/index.ts:2483 · Supporting declarations

One exemplar invocation for {@link dumpQueryShapes} — the args/user a query is built with. Literal values never matter to the dump (shapes are deduped with literals stripped); what an exemplar buys is BRANCH coverage, so supply one per code path a query function can take (an optional filter present/absent, each enum axis, …).

export interface ShapeExemplar<User = unknown> {
    args?: unknown;
    user?: User;
}

shared

FunctionDeclaration · Source: packages/client/src/mutation-ops.ts:180 · Supporting declarations

Co-locate a shared (generator) mutator with the validator for its args — pairing the arg SHAPE with the body that consumes it at ONE site, so neither tier restates it (the client derives its callsite type from Args; the server parses untrusted args through .args). Returns the SAME generator function with an args property attached (Object.assign mutates + returns it), so the registered value is byte-for-byte what the client drove before: {@link isGeneratorMutator} still detects it and it still satisfies ClientRegistry.

export declare function shared<Args, Ctx extends MutatorCtx = MutatorCtx>(args: ArgSchema<Args>, run: SharedMutator<Args, Ctx>): SharedMutatorWithArgs<Args, Ctx>;

sharedApiMutators

FunctionDeclaration · Source: packages/api-server/src/index.ts:2425 · Supporting declarations

Bulk-register a SHARED (generator) mutator registry as server mutators — the mutator twin of {@link registerQueries} (which does the same for co-located defineQuery values). Each shared mutator carries its own arg validator (shared(schema, gen)), so this wraps every one with the UNIVERSAL server triad and nothing else: parse the UNTRUSTED wire args (its .args), map the server {@link MutationContext} to the shared {@link MutatorCtx} principal, and drive the SAME body the client predicts ({@link runSharedMutation}). The point is that a shared mutator whose server run adds NO authority beyond that triad needs no hand-written wrapper.

Server-only AUTHORITY the client cannot predict (a title guard, an owner-gated cascade, a NOT EXISTS dedup) stays an explicit {@link ApiMutator} that OVERRIDES the auto-wrapped default — spread this first, then the overrides win by key:

mutators: defineApiMutators({
  ...sharedApiMutators(sharedMutators, (ctx) => ({ user: requireUser(ctx.user) })),
  createIssue: withTitleGuard(sharedMutators.createIssue), // + server-only policy
  deleteIssue: async (tx, raw, ctx) => { ... },            // raw owner-gated cascade
}),
export declare function sharedApiMutators<User>(registry: Record<string, SharedMutatorWithArgs<any>>, principal: (ctx: MutationContext<User>) => MutatorCtx): ApiMutators<User>;

SharedMutator

TypeAliasDeclaration · Source: packages/client/src/mutation-ops.ts:152 · Supporting declarations

A generator (isomorphic) mutator, shared verbatim by both tiers: the client trusts typed args, the server parses untrusted args into Args before invoking.

export type SharedMutator<Args, Ctx extends MutatorCtx = MutatorCtx> = (tx: IsoTx, args: Args, ctx: Ctx) => MutationGen;

SharedMutatorWithArgs

TypeAliasDeclaration · Source: packages/client/src/mutation-ops.ts:170 · Supporting declarations

A shared mutator that CARRIES its own arg validator, co-located at the def site (shared(schema, gen)). The client registers it exactly like a bare generator mutator — the .args validator is inert there (typed callsites skip the parse); the server ({@link runSharedMutation }, via the api-server's sharedApiMutators) reads .args to parse untrusted wire args before driving the SAME body.

export type SharedMutatorWithArgs<Args, Ctx extends MutatorCtx = MutatorCtx> = SharedMutator<Args, Ctx> & {
    args: ArgSchema<Args>;
};

spliceStreamText

FunctionDeclaration · Source: packages/client/src/stream.ts:75 · Supporting declarations

Merge the durable plane with the live tail.

durable is what the IVM view shows; produced is what a subscription has accumulated (the prefix it joined at, plus every chunk). Both are prefixes of the same response, so the merge is "take the longer" — no diffing, no overlap handling, no ranges.

The length comparison is the whole algorithm, which is why a caller MUST seed its accumulator with the text it joined at: a tail carrying only the chunks it received would read as shorter than the durable text and be discarded. useStreamedText does that for you.

export declare function spliceStreamText(durable: string, produced: string): string;

SplitDaemonClient

ClassDeclaration · Source: packages/api-server/src/index.ts:1137 · Supporting declarations

A read/write-split RindleDaemonClient (READ-ROUTER-DESIGN.md §2.1). Writes (executeSqlTxn / rejectMutation / applyRowChangeTxn / migrate) go to the single write-master, UNCHANGED and never through the router; reads (materialize / query / dematerialize, and raw executeSqlRead) go to the read router. Raw reads default to a replica (consistency:"eventual") so they scale off the write-master; pass consistency:"strong" on a read to route it to the master for read-your-writes after a write to the same data. Hand one of these to {@link createRindleApiServer} as daemon to point the reads leg at the router while writes stay on the master — no placement logic enters the api-server.

const daemon = new SplitDaemonClient(
  new HttpRindleDaemonClient({ baseUrl: MASTER_URL, headers: writeAuth }),  // writes → master
  new HttpRindleDaemonClient({ baseUrl: ROUTER_URL, headers: routerAuth }), // reads  → router
);
export declare class SplitDaemonClient implements RindleDaemonClient {
    private readonly writes;
    private readonly reads;
    constructor(writes: RindleDaemonClient, reads: RindleDaemonClient);
    executeSqlTxn(input: SqlTxn): Promise<SqlTxnOutput>;
    executeSqlRead(input: SqlRead): Promise<SqlReadOutput>;
    rejectMutation(input: MutationRejection): Promise<MutationRejectionOutput>;
    beginMutationSession(input: MutationSessionBegin): Promise<MutationSessionBeginOutput>;
    execInMutationSession(input: MutationSessionExec): Promise<unknown>;
    queryInMutationSession(input: MutationSessionQuery): Promise<SqlReadOutput>;
    commitMutationSession(input: MutationSessionRef): Promise<SqlTxnOutput>;
    rollbackMutationSession(input: MutationSessionRef): Promise<unknown>;
    applyRowChangeTxn(input: RowChangeTxn): Promise<RowChangeTxnOutput>;
    claimRoomEpoch(input: ClaimRoomEpochInput): Promise<ClaimRoomEpochOutput>;
    roomLmids(input: RoomLmidsInput): Promise<RoomLmidsOutput>;
    migrate(input: MigrateInput): Promise<MigrateOutput>;
    materialize(input: MaterializeInput): Promise<MaterializeOutput>;
    query(input: QueryOnceInput): Promise<QueryOnceOutput>;
    dematerialize(input: DematerializeInput): Promise<DematerializeOutput>;
}

sqlBackend

FunctionDeclaration · Source: packages/api-server/src/index.ts:2172 · Supporting declarations

Run API-server mutators through @rindle/sql-client's explicit mutation facade. Query leases, SSR reads and room control continue to use daemon; only authoritative mutation execution moves to the versioned SQL transport.

export declare function sqlBackend(sql: SqlSession): MutationBackend;

SqlDialect

InterfaceDeclaration · Source: packages/api-server/src/index.ts:1224 · Supporting declarations

A SQL dialect for the logical mutation renderer.

export interface SqlDialect {
    readonly name: "sqlite" | "postgres";
    /** Render the i-th (1-based) bind placeholder. sqlite: `?`; postgres: `$i`. */
    placeholder(oneBased: number): string;
    /** Optional value coercion hook (e.g. a future SQLite `0/1` boolean). Default: identity. */
    encodeValue?(v: WireValue, type: ColType): WireValue;
}

sqliteDialect

VariableDeclaration · Source: packages/api-server/src/index.ts:1232 · Supporting declarations

export declare const sqliteDialect: SqlDialect;

SqlMutationTx

InterfaceDeclaration · Source: packages/api-server/src/index.ts:247 · Supporting declarations

The raw-SQL escape hatch for relational/authority statements a keyed op can't express — an owner-gated cascade, a NOT EXISTS dedup. Prefer tx.sql; exec remains the synchronous compatibility shorthand for a queued tx.sql.execute, and statements is the raw write list.

export interface SqlMutationTx {
    readonly sql: ServerSql;
    exec(sql: string, params?: WireValue[]): void;
    readonly statements: readonly SqlStatement[];
}

STREAM_SSE_HEADERS

VariableDeclaration · Source: packages/api-server/src/streams.ts:1652 · Supporting declarations

Headers for the SSE response. x-accel-buffering is the nginx-family opt-out — without it a buffering proxy holds the tokens and hands the user a paragraph at a time.

export declare const STREAM_SSE_HEADERS: Record<string, string>;

STREAM_STATUS_STREAMING

VariableDeclaration · Source: packages/client/src/stream.ts:22 · Supporting declarations

The value in the mapped status column while a stream is live.

export declare const STREAM_STATUS_STREAMING = "streaming";

StreamCheckpointPolicy

InterfaceDeclaration · Source: packages/api-server/src/streams.ts:160 · Supporting declarations

When a checkpoint fires — the FIRST of these wins (§3.1). Checkpoints are serialized, so a slow store degrades to fewer, larger checkpoints, never to a queue of them.

export interface StreamCheckpointPolicy {
    /** Produced-but-uncommitted characters that force a checkpoint. Default 512. */
    chars?: number;
    /** Milliseconds since the last checkpoint that force one. Default 750. */
    intervalMs?: number;
    /** Retries for a failing commit before the slice is left for the next trigger. Default 3. */
    retries?: number;
}

StreamCheckpointTarget

TypeAliasDeclaration · Source: packages/api-server/src/streams.ts:154 · Supporting declarations

export type StreamCheckpointTarget = {
    tables: StreamTables;
} | {
    commit: StreamCommit;
};

streamChunkId

FunctionDeclaration · Source: packages/api-server/src/streams.ts:397 · Supporting declarations

The chunk row's deterministic id: a replayed checkpoint collides with itself and is absorbed by ON CONFLICT DO NOTHING — idempotency without an envelope or a dedup ledger (§3.3).

export declare function streamChunkId(streamId: string, seq: number): string;

streamChunkTableDdl

FunctionDeclaration · Source: packages/api-server/src/streams.ts:406 · Supporting declarations

The chunk table's DDL, for the app's migration. The app owns the message table (this only states the three columns the plane needs on it); the chunk table is entirely protocol-shaped, so it is generated rather than hand-written.

export declare function streamChunkTableDdl(tables: StreamTables, dialect: SqlDialect): string[];

StreamColumns

InterfaceDeclaration · Source: packages/api-server/src/streams.ts:92 · Supporting declarations

Which columns of the app's own tables the plane reads and writes. Every entry has a default except cancel, error, and host, which are opt-in BY NAMING: the plane never emits SQL against a column the app did not ask it to use.

export interface StreamColumns {
    /** The message row's primary key, matched against `streamId`. Default `id`. */
    key?: string;
    /** The compacted response text. Default `body`. */
    body?: string;
    /** {@link STREAM_STATUS_STREAMING} then a {@link StreamStatus}. Default `status`. */
    status?: string;
    /** Total durable length — `length(body) + Σ chunk lengths`. The CAS column (§3.2). Default `seq`. */
    seq?: string;
    /** Opt-in (§6): a truthy value here stops the generation at the next checkpoint. No default —
     *  naming it is what turns cancellation on. */
    cancel?: string;
    /** Opt-in: where a failed generation's message is recorded. No default. */
    error?: string;
    /** Opt-in: where the open write records this producer's identity ({@link RindleStreamOptions.hostId}).
     *  Naming it upgrades the row-level single-flight guard from check-then-act to a true
     *  compare-and-swap — the open write turns conditional and a read-back names the winner (§5.1) —
     *  and gives multi-instance subscribe routing a column to read (§4). No default. */
    host?: string;
    /** Chunk primary key; the plane writes the deterministic `"<streamId>:<seq>"`. Default `id`. */
    chunkKey?: string;
    /** Chunk → message reference. Default `streamId`. */
    chunkStream?: string;
    /** The chunk's END offset — the ordering key. Default `seq`. */
    chunkSeq?: string;
    /** The chunk's slice of the response. Default `text`. */
    chunkText?: string;
}

StreamCommit

TypeAliasDeclaration · Source: packages/api-server/src/streams.ts:85 · Supporting declarations

The escape hatch: persist the checkpoint however the app likes. Retried on throw (§3.3), so it must be idempotent under a repeated (from, seq) — AND under a RE-COVER: an append whose write committed but whose ack was lost leaves the plane believing less than the store holds, so the next append's (from, seq) can OVERLAP text already applied. Apply only the unseen suffix (text.slice(applied - from) when applied > from), or better, return {seq: applied} — the authoritative applied length — and the plane resynchronizes instead of re-covering at all. Return {cancelRequested: true} to tell the producer the reader asked it to stop (§6).

export type StreamCommit = (input: StreamCommitInput) => Promise<void | {
    cancelRequested?: boolean;
    seq?: number;
}>;

StreamCommitInput

TypeAliasDeclaration · Source: packages/api-server/src/streams.ts:62 · Supporting declarations

What a checkpoint hands the durable plane when the app supplies its own {@link StreamCommit}.

export type StreamCommitInput = 
/** The pointer is marked live. The app's own mutator created the row (it owns `chatId`, `role`,
 *  the model name…); this only flips it to `streaming`. */
{
    kind: "open";
    streamId: string;
    meta: unknown;
    hostId?: string;
    startedAt: number;
}
/** A prefix advance carrying ONLY its own slice. `text.length === seq - from`, and `from` is the
 *  last append the PLANE saw confirmed — so appends are contiguous in the fault-free run, but an
 *  append that committed while its ack was lost makes the next one RE-COVER (its `from` lags what
 *  the app already applied). See {@link StreamCommit} for the two ways to stay idempotent. */
 | {
    kind: "append";
    streamId: string;
    from: number;
    seq: number;
    text: string;
}
/** The seal. `body` is the producer's retained text and `bodyFrom` its absolute start offset:
 *  `bodyFrom === 0` — and `body` is the WHOLE response — unless the app opted into trimming via
 *  `retainChars` (`commit` mode only). A compacting app requires `bodyFrom === 0` and writes
 *  `body` wholesale; a non-compacting app appends the outstanding tail —
 *  `body.slice(from - bodyFrom)` — as a final chunk. `seq === bodyFrom + body.length`, always. */
 | {
    kind: "close";
    streamId: string;
    from: number;
    seq: number;
    body: string;
    bodyFrom: number;
    status: StreamStatus;
    error?: string;
};

StreamFrame

TypeAliasDeclaration · Source: packages/client/src/stream.ts:32 · Supporting declarations

One frame of a subscription. A subscription always begins with open and always ends with exactly one terminal frame — end, stale, or absent — after which the iterator completes.

stale and absent are the two "you are on the durable plane now" answers, and both are SAFE: the store holds everything below floorSeq and everything through durableSeq, so the reader's IVM view converges without the stream. Neither is an error.

export type StreamFrame = 
/** Join accepted. `from` is the (clamped) offset the replay starts at. */
{
    type: "open";
    streamId: string;
    from: number;
    seq: number;
    durableSeq: number;
    ended: boolean;
}
/** PRODUCED text — not a durability claim. `text.length === seq - from`, always. */
 | {
    type: "chunk";
    from: number;
    seq: number;
    text: string;
}
/** The store now holds the prefix through `seq`. */
 | {
    type: "durable";
    seq: number;
}
/** Sealed. No further frames. */
 | {
    type: "end";
    seq: number;
    status: StreamStatus;
    error?: string;
}
/** `from` is below the producer's retained buffer floor (or the subscriber fell too far behind):
 *  read the store. (A raw `EventSource` rejoins automatically on its reconnect; `useStreamedText`
 *  deliberately stays on the durable plane instead — correct, at checkpoint granularity.) */
 | {
    type: "stale";
    floorSeq: number;
    durableSeq: number;
}
/** The process serving this subscribe is not hosting the stream (wrong instance, already evicted,
 *  or it never existed): the store is the whole truth. */
 | {
    type: "absent";
};

streamFramesToSse

FunctionDeclaration · Source: packages/api-server/src/streams.ts:1683 · Supporting declarations

Encode a subscription as an SSE body. Each positional frame carries id: <seq>, so a browser EventSource that drops the connection resumes at exactly the right offset with no application code — its own Last-Event-ID header is the from of the next subscribe ({@link * streamRequestFromHttp}).

The reader must close the EventSource on the end frame: EventSource reconnects on ANY close, including a clean one.

export declare function streamFramesToSse(sub: StreamSubscription, opts?: {
    keepAliveMs?: number;
}): ReadableStream<Uint8Array>;

StreamHandle

InterfaceDeclaration · Source: packages/api-server/src/streams.ts:271 · Supporting declarations

The producer's handle (§2). One writer per stream, by construction.

export interface StreamHandle {
    readonly streamId: string;
    /** Total produced code units. */
    readonly seq: number;
    /** Total code units the store has committed. Never exceeds {@link seq} (contract P). */
    readonly durableSeq: number;
    /** True once a checkpoint round-trip has seen the reader's cancel flag (§6). `pump` stops on it;
     *  a hand-rolled generation loop should check it. */
    readonly cancelled: boolean;
    /** Append a delta: fanned to subscribers synchronously, checkpointed on policy. */
    push(text: string): void;
    /** Force a checkpoint and resolve once the store holds every character produced so far. This is
     *  the ORDERING primitive: `await flush()` before writing a discrete row (a tool call, a stop
     *  reason) so the text precedes it in the store (§1). Rejects if the checkpoint cannot commit. */
    flush(): Promise<number>;
    /** Drain a delta iterable into the stream (the shape every LLM SDK's text stream already has),
     *  stopping early — and closing the iterator, which aborts the underlying request — once the
     *  reader has cancelled. */
    pump(deltas: AsyncIterable<string>): Promise<void>;
    /** Seal the stream: `cancelled` if the reader asked it to stop, else `complete`. In `tables` mode
     *  this is the compaction (§3.4) — it writes the whole body and drops the chunk rows in one
     *  transaction, so it also REPAIRS any checkpoint that failed along the way. */
    close(): Promise<void>;
    /** Seal `error` at whatever was produced. Never throws for the reason it is sealing. */
    fail(error: unknown): Promise<void>;
}

StreamOpenRefused

ClassDeclaration · Source: packages/api-server/src/streams.ts:1595 · Supporting declarations

The open probe's verdict: the message row is missing or already advanced. Not retried — it is a settled statement about the app's data, and retrying re-asks a question already answered.

export declare class StreamOpenRefused extends Error {
    constructor(message: string);
}

StreamRelay

InterfaceDeclaration · Source: packages/api-server/src/streams.ts:193 · Supporting declarations

Optional cross-process transport for the LIVE plane (designs-implemented/LM-STREAM-RELAY-DESIGN.md). Both methods are independently optional; which ones you implement is which topology you built — an addressing adapter (a Durable Object named by streamId, fly-replay) implements only attach; a broadcast adapter (Redis pub/sub, NATS) mirrors with publish and subscribes with attach; a log adapter (Redis Streams, Kafka) appends and replays. Never consulted for the durable plane — checkpoints are unaffected by any of this.

The plane does not trust what attach yields: frames are run through the conform pass ({@link StreamRelayConform}) and any contract violation downgrades the subscription to stale, which already means "you are on the durable plane now". A broken relay costs a reader smooth tokens, never corrupted text — and can never reach the producer.

export interface StreamRelay {
    /** Producer side: every frame this process's producer fans out (`chunk`, `durable`, and the
     *  terminal `end`), mirrored outward. MUST NOT block; a throw or rejected promise is caught and
     *  routed to {@link RindleStreamOptions.onRelayError} — a relay outage may cost the live leg,
     *  never the generation. Returned promises are observed but never awaited. */
    publish?(streamId: string, frame: StreamFrame): void | PromiseLike<void>;
    /** Subscriber side: this process is not hosting `streamId`. Return a frame source, or `undefined`
     *  for `absent` — exactly the no-relay answer. Consulted only AFTER `authorize` has passed, and
     *  only on a live-plane miss (a local stream always wins). The plane closes the source
     *  (`return()`) when the reader disconnects. An adapter that cannot serve `from` (pub/sub has no
     *  history) yields `stale` and stops — the reader converges on the durable plane (§5). */
    attach?(streamId: string, from: number): Promise<AsyncIterable<StreamFrame> | undefined>;
}

StreamRelayConform

ClassDeclaration · Source: packages/api-server/src/streams.ts:639 · Supporting declarations

One frame source arriving over a relay, conformed to the CP §4 contract (designs-implemented/LM-STREAM-RELAY-DESIGN.md §4).

An adapter is app code talking to Redis or a socket, and its frames feed spliceStreamText on a browser — so the plane does not trust them. This pass enforces the frame invariants against the prefix actually delivered and downgrades EVERY violation to a legal stale and nothing else: stale already means "you are on the durable plane now, the store is the whole truth", so a broken relay costs a reader smooth tokens, never corrupted text — and cannot wedge a producer.

Replayed spans (a reconnecting adapter re-delivering what it already sent) are ABSORBED rather than punished — deduping against the delivered prefix is what makes reconnect-replay safe without every adapter hand-rolling it. Spans that overlap the prefix but extend past it pass through whole: the client splices at the frame's own offset, so an exact overlap re-covers and appends.

Pure state, no I/O, no plane: feed maps one incoming frame to 0-2 outgoing frames (a missing open is synthesized at the join offset); end/fail close out a source that finished or threw without a terminal. After a terminal, every method returns [].

export declare class StreamRelayConform {
    private readonly streamId;
    /** The requested join offset — the synthesized `open`'s position, and where the prefix starts. */
    private readonly from;
    private readonly onViolation;
    /** End of the delivered prefix. */
    private pos;
    private lastDurable;
    private opened;
    private done;
    constructor(streamId: string, from: number, onViolation?: (reason: string) => void);
    feed(frame: StreamFrame): StreamFrame[];
    /** The source completed without a terminal (a truncated relay): the reader falls back. */
    end(): StreamFrame[];
    /** The source threw mid-iteration, or the plane is dropping a reader that stopped draining:
     *  a bare `stale` at the delivered position. */
    fail(): StreamFrame[];
    /** A synthesized join, for an adapter that (correctly, in broadcast mode) never mirrors the
     *  per-subscriber `open`: positioned at the requested offset, which the reader asked from because
     *  its durable view already holds it. */
    private synthOpen;
    private terminate;
    private violate;
}

StreamRelayErrorInfo

InterfaceDeclaration · Source: packages/api-server/src/streams.ts:207 · Supporting declarations

export interface StreamRelayErrorInfo {
    streamId: string;
    /** Where it failed: mirroring a frame out (`publish`), dialing the adapter (`attach`), or
     *  consuming/conforming its frames (`frames`). */
    phase: "publish" | "attach" | "frames";
}

streamRequestFromHttp

FunctionDeclaration · Source: packages/api-server/src/streams.ts:1662 · Supporting declarations

Pull a subscribe request out of a fetch-style GET: ?streamId=…&from=…, with Last-Event-ID winning over an explicit from (a reconnecting EventSource knows better than its own URL — the URL is the ORIGINAL join point, the header is where it actually got to).

export declare function streamRequestFromHttp(req: {
    url: string;
    headers: {
        get(name: string): string | null;
    };
}): {
    streamId: string;
    from: number;
};

StreamStatus

TypeAliasDeclaration · Source: packages/client/src/stream.ts:19 · Supporting declarations

How a stream ended.

  • complete — the model finished.
  • cancelled — the reader asked it to stop and the producer honoured it.
  • error — the generation threw.
  • interrupted — the host went away mid-generation. The one status that implies the store may be short of what was produced.
export type StreamStatus = "complete" | "cancelled" | "error" | "interrupted";

StreamSubscription

InterfaceDeclaration · Source: packages/api-server/src/streams.ts:307 · Supporting declarations

export interface StreamSubscription {
    readonly streamId: string;
    /** Terminates after exactly one of `end` / `stale` / `absent`. */
    readonly frames: AsyncIterable<StreamFrame>;
    /** Detach early (a disconnected client). Idempotent. */
    close(): void;
}

StreamTables

InterfaceDeclaration · Source: packages/api-server/src/streams.ts:146 · Supporting declarations

The app's tables (§5). The app authors and migrates BOTH — the message row is unambiguously app-owned (it has chatId, role, token counts) and the chunk row must be reachable from the app's own query as a related subquery, which a Rindle system table would make awkward. The plane only needs to be told where things live. Use {@link streamChunkTableDdl} for the chunk table's migration.

The message table carries WHATEVER ELSE the app wants; the plane's hard requirements are only:

mapped column requirement why
key UNIQUE (normally the pk) every checkpoint targets one row by it
seq integer, NOT NULL DEFAULT 0 the CAS column — nothing matches NULL (§3.2)
body text, empty at open compaction overwrites it with the whole response
status text accepting streaming/complete/cancelled/error/interrupted a CHECK
         constraint that omits one of these turns a seal into an infra failure |

| cancel | truthy-readable, if mapped | read on the checkpoint round-trip (§6) | | error | nullable text, if mapped | compaction writes NULL when there is no error | | host | text, if mapped | written at open with the producer's token; the read-back decides the open race (§5.1) |

body is PLANE-OWNED and always a bare string. Rich content (an array of content blocks, tool calls, attachments) belongs in SIBLING columns the app's own mutators write — flush() orders the text before them. For genuinely multi-block streaming, point message at a per-BLOCK table instead: streamId is just an app key, so one stream per block needs nothing from this plane.

export interface StreamTables {
    /** The app's message table. Must already contain the row when {@link StreamPlane.open} runs. */
    message: string;
    /** The append-only chunk table. */
    chunks: string;
    columns?: StreamColumns;
}

SubscribeStreamInput

InterfaceDeclaration · Source: packages/api-server/src/streams.ts:298 · Supporting declarations

export interface SubscribeStreamInput<User> {
    user: User;
    streamId: string;
    /** "I already have this many characters" — from the client's IVM view, or a `Last-Event-ID`.
     *  A non-negative integer; default 0. */
    from?: number;
    request?: unknown;
}

TableRenderMeta

InterfaceDeclaration · Source: packages/api-server/src/index.ts:1236 · Supporting declarations

Per-table metadata the renderer needs (all reachable from a TableMeta).

export interface TableRenderMeta {
    /** Columns in schema (wire) order — the stable INSERT column list + completeness check. */
    columns: string[];
    /** Primary-key column NAMES — the WHERE / ON CONFLICT target / SET partition. */
    pkNames: string[];
    /** Column name → declared type (only consulted by {@link SqlDialect.encodeValue}). */
    types: Record<string, ColType>;
    /** Columns a full insert must name — the non-nullable ones (design 206 §6.2). */
    required: string[];
    /** The nullable (omittable-to-null) columns — an omitted one binds `NULL` (design 206 §6.2). */
    nullable: ReadonlySet<string>;
}

verifyRoomFlushCredential

FunctionDeclaration · Source: packages/api-server/src/index.ts:864 · Supporting declarations

Verify a flush credential's MAC, then its claims; returns the payload or throws. The MAC is checked FIRST — no claim is trusted before it passes.

export declare function verifyRoomFlushCredential(credential: string, shellSecret: string): Promise<RoomFlushCredentialPayload>;