> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pylonsync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Reactive queries

> Convex-style auto-rerunning server query handlers — write the query once, the client re-renders on every dependency change.

# Reactive queries

A reactive query is a server-side `query()` handler that automatically
re-runs whenever the data it reads changes. The client subscribes once;
the server pushes a new result every time a mutation touches anything
in the handler's dependency set.

This is the "live joined query" shape Convex made famous. Pylon ships
the same model with native auth, multi-tenant policies, and self-host.

## Writing a reactive query

Any `query()` handler is eligible — no opt-in flag, no special
decorator. The reactivity is unlocked at the call site by using
`db.useReactiveQuery` instead of a one-shot `useFn` / fetch.

```ts theme={null}
// functions/getFeed.ts
import { query, v } from "@pylonsync/functions";

export default query({
  args: { userId: v.id("User") },
  async handler(ctx, args) {
    const posts = await ctx.db.list("Post");
    return Promise.all(
      posts
        .filter((p) => p.authorId !== args.userId) // exclude self
        .slice(0, 50)
        .map(async (p) => ({
          ...p,
          author: await ctx.db.get("User", p.authorId),
        })),
    );
  },
});
```

The runtime watches every `ctx.db.*` call this handler makes and
records the set of entities + row ids it touched. In the example
above the dep set is `{ Post: *, User: { authorId1, authorId2, ... } }`.

## Subscribing from React

```tsx theme={null}
import { db } from "@pylonsync/react";

function Feed({ userId }: { userId: string }) {
  const { data: feed, loading, error } = db.useReactiveQuery<FeedItem[]>(
    "getFeed",
    { userId },
  );
  if (loading) return <Spinner />;
  if (error) return <ErrorBanner error={error} />;
  return feed.map((item) => <PostCard key={item.id} item={item} />);
}
```

On mount: the hook sends `reactive-subscribe` over the WebSocket; the
server runs `getFeed`, captures deps, registers the sub, and pushes
the initial result.

After that, every server-side mutation touching `Post` or a `User`
row in the captured set triggers a re-run. The server hashes the new
result and pushes only when it differs from the last value sent — so
a mutation that doesn't change the rendered output costs one re-run
on the server but no client re-render.

On unmount: the hook sends `reactive-unsubscribe`. The server drops
the subscription and stops re-running.

## Auth context across re-runs

The handler runs under the **subscriber's** auth context on every
re-run — never the mutating user's. A policy like
`auth.userId == row.ownerId` applied at first run applies on every
subsequent re-run.

This is the only correct semantic. If re-runs used the mutating
user's auth, a Stripe webhook (running as an elevated admin) would
re-evaluate `getFeed` with admin privileges and push the unfiltered
result to a logged-in user — a silent read-policy bypass.

## Dependency tracking granularity

The runtime tracks two levels:

* **Entity-level**: any read of `ctx.db.list("Post")` or
  `ctx.db.query("Post", {...})` marks the dep as "entity `Post`,
  any row". Mutations to any Post row dirty the sub.
* **Row-level**: `ctx.db.get("User", "u_123")` marks the dep as
  "entity `User`, row `u_123`". Mutations to other User rows do
  NOT dirty the sub.

The dep set is precise for `get` + `lookup`, coarse for `list` +
`query` + `search`. Handlers that mix both get the union — list
reads contribute entity-level deps; targeted reads contribute
row-level deps on top.

Row-level deps are capped per subscription (default 256). Beyond
the cap, the sub falls back to entity-level matching. Prevents
runaway queries from bloating the registry.

## Coalescing + re-run cadence

Multiple change events touching the same sub within one tick
coalesce to a single re-run. The re-runner thread drains the dirty
set on a notify-driven cadence (no fixed interval — wakes
immediately when work arrives). Bursts of writes don't cause
per-event re-runs.

## When to use reactive queries vs entity queries

| Use case                                   | Reactive query            | Entity query (useQuery)     |
| ------------------------------------------ | ------------------------- | --------------------------- |
| Server-side joins (entity + related rows)  | ✅                         | ❌                           |
| Aggregations (count, sum, group-by)        | ✅                         | ❌                           |
| Computed fields derived from multiple rows | ✅                         | ❌                           |
| Simple `where` filter on one entity        | ✅                         | ✅                           |
| Fully cached IndexedDB replica             | ❌                         | ✅                           |
| Offline-first writes                       | Server roundtrip required | Optimistic UI works offline |

Use **reactive queries** when the value rendered on screen is the
output of a function that touches multiple entities or computes
something the client couldn't easily derive from cached rows. Use
**entity queries** (`db.useQuery`) when the screen is a list of
rows from one entity and the client can filter / sort / paginate
locally for free.

## Cross-machine support

Reactive queries work on multi-machine deployments — the runtime
forwards change events into the registry from both the local
mutation path AND the cluster bus subscriber (see [horizontal
scaling](/operations/scaling)). A mutation on machine A dirties
subs on machine B; B's re-runner runs the handler and pushes the
result to its connected client.

## Limits

* One handler call per re-run. There's no incremental dataflow
  (Materialize / differential dataflow). For very expensive
  handlers, the re-run cost is the full handler cost — design with
  pagination / `limit` to keep handlers fast.
* The hash check skips client pushes when the result hashes
  identical. It doesn't skip the re-run itself. A handler that
  reads 1k rows but produces a small derived value runs the full
  query on every dirty event.
* Reactive subs require the TS function runtime (Bun process). When
  the runtime isn't available (no `functions/` directory), the
  hook receives a `REACTIVE_UNAVAILABLE` error push and the
  consumer can fall back to a one-shot fetch.
