Tarski/ docsJoin the waitlist

Reactive queries

Keep a declared query's result current without polling — durable subscriptions, exactly-once delta delivery, held streams, and explicit rehydrates.

Reactive subscriptions keep a declared query’s result current for a client without polling: register a query with bindings, receive the initial rows at a committed head, then consume result deltas keyed to committed heads with exactly-once cursor resume. Every pushed result is a fresh execution of the query at that head — parity with one-shot execution is the correctness contract, and frames are byte-equal at identical cursors.

The subscription lifecycle

  1. RegisterPOST /v1/queries/{name}/subscriptions with bindings returns the subscription id, the initial rows, the committed head, the result digest, and the query’s transitive read set. Registrations are durable, store-backed leases that survive server restarts.
  2. Consume — either hold the SSE stream (GET /v1/queries/subscriptions/{id}/stream, resuming via Last-Event-ID) or fetch batches (GET /v1/queries/subscriptions/{id}/updates?since_update_id=N). Deltas carry the head, a per-subscription monotonic update id, adds and removes, and the result digest.
  3. Rehydrate when told to. When history is unavailable — a stale cursor, a lost update log after restart, an oversized delta — the server sends an explicit full-replace rehydrate event. Never silence, never fabricated replay.
  4. UnsubscribeDELETE /v1/queries/subscriptions/{id} ends held streams and drops the lease. Expired leases reap to a non-disclosing query.subscription_not_found, and the client simply re-registers.

Efficient by read set

Commits touching only relations outside a subscription’s static read set do no work. Touched subscriptions re-execute at the committed head, and unchanged results are digest-suppressed. One deliberate exception: any commit touching an access.* relation re-executes every live subscription on the lineage — authorization can change what a subscriber may see without touching the read set. Authorization loss emits a terminal revoked event; subscription frames are row-filtered, so a policy-invisible tenant’s append is invisible in the delta stream too.

Backpressure is typed, never silent: transient door pressure defers pushes; a held-stream ring overflow closes the stream with fanout_buffer_full (a resume signal, not data loss); per-tenant ceilings deny with the typed tenant_backpressure shape.

From the SDK

import { createTarskiClient } from '@tarski/client';

const client = createTarskiClient({ baseUrl: 'http://127.0.0.1:7411' });

for await (const item of client.subscribeQuery('workspace-board', {
  bindings: { workspace_id: 'ws_1' },
})) {
  switch (item.kind) {
    case 'initial':   render(item.rows); break;
    case 'delta':     apply(item.adds, item.removes); break;
    case 'rehydrate': render(item.rows); break;   // full replace — rebuild state
    case 'closed':    break;
  }
}

subscribeQuery prefers the held stream and degrades to the batch resume loop on open failure — exactly-once across the transition, cursors handled for you. In the Team Tasks reference app, one workspace-board subscription is the board’s entire data source: initial paints, deltas apply exactly the changed rows, rehydrates replace everything, and writes never re-read.

Local serve tuning

tarski serve exposes subscription knobs as environment variables: TARSKI_SUBSCRIPTION_STREAM_HEARTBEAT_MS, TARSKI_SUBSCRIPTION_STREAM_BUDGET, TARSKI_SUBSCRIPTION_LEASE_TTL_MS, TARSKI_SUBSCRIPTION_LEASE_REAP_INTERVAL_MS, TARSKI_TENANT_HELD_STREAM_CEILING, and TARSKI_TENANT_LIVE_SUBSCRIPTION_CEILING.