Gonvex

Realtime Subscriptions

Subscribe to query results and receive fresh data when relevant writes commit.

Realtime Subscriptions

Gonvex subscriptions keep query results current over a reconnecting WebSocket connection.

The runtime identifies itself in the first session.ready frame. Its capabilities include protocolVersion, runtimeVersion, and supported protocol features such as syncBatch and syncIntegrity. runtimeVersion should be the deployed Git commit SHA in managed environments. Clients use feature flags, rather than protocol-field presence alone, when deciding whether a missing field is a compatibility case or a corrupt response. Applications can inspect the latest advertisement with client.serverInfo().

The application model is:

useQuery(api.tasks.list, args)
-> runtime executes tasks.list
-> frontend receives result
-> relevant committed writes happen
-> runtime refreshes affected subscriptions
-> frontend receives fresh result

React Usage

import { api } from "./gonvex/_generated/api";
import { useQuery } from "./gonvex/_generated/react";

export function OpenTasks() {
  const tasks = useQuery(api.tasks.list, { status: "open" });

  if (tasks === undefined) return <Spinner />;
  return <TaskList tasks={tasks} />;
}

The hook subscribes when the component mounts and unsubscribes when it unmounts.

Direct Client Usage

Custom surfaces can use the client directly:

const unsubscribe = client.subscribeQuery(
  api.tasks.list,
  { status: "open" },
  (message) => {
    if (message.type === "query.result") {
      render(message.result);
    }
  },
);

unsubscribe();

Subscription Scope

Subscriptions run in runtime context:

project
environment
authenticated user
active tenant
role/permissions
tenant database route

When the native auth provider switches tenants, active subscriptions resubscribe under the new verified tenant context. Cached results are separated by the server-issued scope and cannot cross tenant, identity, or permission boundaries.

Protocol Concept

The wire protocol can be represented as:

{
  "type": "query.subscribe",
  "id": "client-subscription-id",
  "path": "tasks.list",
  "args": { "status": "open" }
}

The runtime answers:

{
  "type": "query.result",
  "id": "client-subscription-id",
  "reason": "initial",
	"subscriptionRevision": { "epoch": "runtime-start-id", "sequence": 42 },
  "result": []
}

After relevant invalidation:

{
  "type": "query.result",
  "id": "client-subscription-id",
  "reason": "invalidate",
  "result": []
}

Only one execution for a shared subscription runs at a time. Changes received during an execution mark it dirty and produce at most one immediate catch-up execution. Revisions prevent an older frame from overwriting newer state.

When a rerun is unchanged, the runtime sends query.progress and the client advances its revision without notifying React. Large keyed lists may use an adaptive query.patch; the client applies it only when baseRevision matches, otherwise it requests a fresh snapshot.

Identical explicitly shareable queries use one runner and retained snapshot per project, tenant, canonical arguments, permission fingerprint, and bundle hash. Late listeners receive the authoritative retained snapshot immediately.

Most application code should use generated hooks/client helpers instead of constructing protocol messages manually.

Reconnect and cache behavior

After an unexpected close, the client reconnects with exponential backoff, reauthenticates, and resubscribes active queries. A transparent browser cache can replay the last safe scoped result while the authoritative server subscription runs in parallel. The server response always wins.

Pending mutations and actions are not replayed after a disconnect because doing so could duplicate non-idempotent writes. They reject with a typed GonvexClientError.

For entity collections that need a durable cursor and normalized IndexedDB storage, use Durable Sync Collections instead of a live query.

Correctness Goal

The realtime contract is:

If a committed write can change a subscribed result, the subscriber should receive fresh data.

Gonvex prefers extra refreshes over stale visible data. Over-refreshing is a performance bug; missing a required refresh is a correctness bug.

Performance Goals

The runtime avoids request storms by:

  • debounce or coalesce write bursts
  • avoid refreshing unrelated subscriptions
  • use query dependency metadata where available
  • compress WebSocket payloads
  • support large surfaces through LiveGrid windows instead of subscribing to entire tables
  • cap retained shared-result bytes and listener fan-out

Tenant Safety

Subscriptions must not leak data across tenants.

The runtime must verify that the authenticated user can access the active tenant before running the query. Tenant selection from the browser is input to verify, not authority to trust.

On this page