QUESTPIE
Client

Realtime

live() is find() that keeps arriving. The first snapshot lands at once, then a fresh one lands whenever a matching row changes.

View markdown

A list is on screen. Someone else edits a row. This page is how that list catches up, without a refresh and without a poll.

Subscribe to a query

live() takes the query you would have passed to find(), plus a callback. It returns a function that stops the subscription.

src/app/live-posts.ts
import { client } from "@/lib/client";

const unsubscribe = client.collections.posts.live(
	{ where: { published: true }, orderBy: { createdAt: "desc" }, limit: 10 },
	(snapshot) => {
		render(snapshot.docs);
	},
);

// Later, on unmount:
unsubscribe();

The callback fires once straight away with the current result. It fires again after every change that matches. So you do not need a find() before you subscribe. Treat the first call as "data loaded".

A snapshot is a find() result, same shape and same type. You get docs, totalDocs, hasNextPage and any relations you asked for in with. QUESTPIE re-runs the query under the subscriber's own session before each push. A subscription cannot show a row that reader is not allowed to read.

Every live() call on one client shares a single stream. Two calls with identical options share one topic on it, and both callbacks get every snapshot. Change the where and you get a second topic.

You do not turn it on

Realtime ships in the core module. Change capture, the POST /realtime endpoint and the SSE transport are already there on a plain db: { url } app. realtime: true resolves to {}, which is what an empty config gives you. Write the object form only to change a default.

Two switches turn it off. realtime: { rowLiveQueries: false } closes every collection and global topic app-wide. .options({ realtime: false }) closes one collection. See Realtime adapter.

What live() accepts

OptionWhat it does
whereThe filter. Same operators as find()
withRelations to hydrate into every snapshot
limitRows per snapshot. The default cap is 100
offsetRows to skip
orderByThe sort. Same shape as find()
localeWhich localized variant to read

That is the whole list. columns, groupBy, search, includeDeleted and stage are find() options that live() does not take. The client builds the topic by copying the fields above and dropping the rest, so nothing else reaches the server. Use a one-shot find() when you need one of them.

Stopping a subscription

Always stop one. A leaked subscription holds a topic open and keeps the server pushing snapshots at nobody.

const controller = new AbortController();

client.collections.posts.live(
	{ where: { published: true } },
	(snapshot) => render(snapshot.docs),
	{
		signal: controller.signal,
		onError: (error) => showStaleBanner(error),
	},
);

controller.abort(); // same as calling the returned function

The third argument is LiveSubscribeOptions. signal unsubscribes on abort, so you can hand it a component's lifetime. onError fires when the server refuses this topic, and when the connection fails in a way the client will not retry. Wire it. Without it a dead subscription looks exactly like one with no news.

A refused topic reaches only that topic's onError, as a RealtimeTopicRejectedError. Sibling subscriptions stay up. Errors and limits has the reason codes.

A dropped connection heals itself

The client reconnects with backoff, then resends each topic with the sequence number it last saw. You do not write that loop, and you do not hear about it. A retryable drop never reaches onError.

liveIter(), the loop form

liveIter() yields the same snapshots as an async generator. Use it in a worker, a script or a test, where for await reads better than a callback.

const controller = new AbortController();

for await (const snapshot of client.collections.posts.liveIter(
	{ where: { published: true }, limit: 10 },
	{ signal: controller.signal },
)) {
	console.log("posts now:", snapshot.totalDocs);
}

It takes { signal } and nothing else. There is no onError here. A refused topic or a dead connection makes the generator throw, so wrap the loop in try/catch. Abort the signal to end it.

Globals

A global is one row, so it takes with and locale only.

const stop = client.globals.settings.live({ with: { logo: true } }, (s) => {
	document.title = s.siteName;
});

The snapshot equals the get() result. Both forms behave as they do on a collection.

With TanStack Query

Pass { realtime: true } as the second argument to a query builder. The hook streams instead of fetching once.

import { useQuery } from "@tanstack/react-query";

import { q } from "@/lib/query";

function LivePosts() {
	const { data } = useQuery(
		q.collections.posts.find({ limit: 10 }, { realtime: true }),
	);

	return <p>{data?.totalDocs ?? 0} published posts</p>;
}

Three reads accept the flag: collections.<name>.find, collections.<name>.count and globals.<name>.get. findOne rejects it at compile time, and there is no live findOne. Subscribe with find({ where: { id } }) and read docs[0].

The query key is identical to the non-realtime version, so both share one cache entry. The stream supplies the first value, so the flag adds no extra REST call. TanStack Query covers the rest of the builder.

A live count sends one number

q.collections.posts.count(options, { realtime: true }) opens a count topic. The server counts and streams the scalar. It never serializes the matching rows.

Locale belongs to the topic

locale is part of the subscription. So en and de are two separate streams of the same query.

client.collections.posts.live({ locale: "de" }, render);

client.setLocale() sets the accept-language header on later requests. It does not retarget an open subscription. Unsubscribe and subscribe again with the new locale.

Where each topic lives

TopicPage
Refused topics, limits, reason codesErrors and limits
Hand-built topics and client.realtimeRaw subscriptions
Keyed row events instead of full snapshotsRow deltas
Brokers, admission config, the outboxRealtime adapter
Picking a primitive, bounding React rendersReactive apps
Two people in one fieldCollaborative documents

Next

Channels is the other half. A live query carries your data. A channel carries an event you published on purpose.

On this page