Queries
One-shot queries, subscriptions, framework hooks, Suspense integration, and read durability options.One-shot queries
A one-shot query runs once against the database without subscribing to changes.
export async function readTodosOneshot(db: Db) {return db.all(app.todos.where({ done: false }));}
export const readTodosOneshot = Effect.gen(function* () {const jazz = yield* Jazz;return yield* jazz.all(app.todos.where({ done: false }));});
pub async fn read_todos_oneshot(client: &JazzClient) -> jazz::tools::Result<usize> {let query = Query::from("todos");let rows = client.query(query, None).await?;Ok(rows.len())}
To read a single row, use db.one. It runs the query with a limit of one and resolves to the
first matching row, or null if no row matches or you aren't allowed to read it.
export async function readTodo(db: Db, id: string) {// Resolves to the first matching row, or null if no row matches or you can't read itreturn db.one(app.todos.where({ id }));}
Subscriptions
Subscribe to a query to receive updates whenever the underlying data changes.
export function subscribeTodos(db: Db, onCount: (count: number) => void) {return db.subscribe(app.todos.where({ done: false }), (todos) => onCount(todos.length));}
// Every emission is the complete current result. The subscription is removed// when the stream ends or the fiber running it is interrupted.export const logOpenTodoCount = Effect.gen(function* () {const jazz = yield* Jazz;yield* jazz.stream(app.todos.where({ done: false })).pipe(Stream.runForEach((todos) => Effect.log(`${todos.length} open todos`)));});
pub async fn subscribe_todos(client: &JazzClient,) -> jazz::tools::Result<jazz::tools::SubscriptionStream> {let query = Query::from("todos");client.subscribe(query).await}
Composing queries
Queries are immutable and chainable. Each method returns a new query, so you can store a base and reuse it for different views without side effects.
// Store a base query and reuse it for different views.const openTodos = app.todos.where({ done: false });const byNewest = openTodos.orderBy("id", "desc");const byTitle = openTodos.orderBy("title", "asc").limit(20);const urgent = openTodos.where({ title: { contains: "urgent" } });
pub fn composing_queries() {// Build two views from the same base conditions.let by_title = Query::from("todos").filter(eq(col("done"), lit(false))).order_by("title", OrderDirection::Asc).limit(20);let by_newest = Query::from("todos").filter(eq(col("done"), lit(false))).order_by("id", OrderDirection::Desc);let _ = (by_title, by_newest);}
See Filters & Sorting for the full list of where operators, orderBy, limit, and offset.
Read durability
Queries and subscriptions return results as soon as they are available locally by default. Local reads are effectively instant, and remote updates stream in as they arrive, which is normally a good default. When you need a stronger guarantee for the first result, pass a tier option to fetch data from a different tier before returning.
Write waits distinguish local persistence from Core acceptance. Synchronization continues with either choice.
| Tier | Where | What confirmation means |
|---|---|---|
"local" | This device | Saved by the local persistence driver, without waiting for the network. |
"global" | Core | Authorized and durably accepted by Core. Other clients receive updates through their subscriptions. |
For background on how data flows between tiers, see How Sync Works.
Choosing a tier
Pass a read choice when you need to control whether the first result may use local knowledge — for example, when a user has just navigated to a page and a stale local snapshot would mislead them.
"local-first"uses cached local data and pending local writes immediately, while continuing to sync."remote"waits for Core-authorized query results and excludes pending local edits."local-first-unless-empty"behaves like"local-first", except that an empty local result waits for the first remote answer while the server is reachable. Use it to avoid a flash of empty UI on a fresh device. Offline, never-connected, and failed connections get the local result right away. Queries with anoffsetread the server's page whenever the server can answer.
See Read tiers for pending-write
behavior and how subscriptions change between offline and online operation.
The aliases "local" and "global" remain accepted for reads; write waits use
"local" and "global".
export async function readTodosAtEdgeDurability(db: Db) {return db.all(app.todos.where({ done: false }), { tier: ReadTier.Remote });}
export const readTodosAtEdgeDurability = Effect.gen(function* () {const jazz = yield* Jazz;return yield* jazz.all(app.todos.where({ done: false }), { tier: ReadTier.Remote });});
pub async fn read_todos_at_edge_durability(client: &JazzClient) -> jazz::tools::Result<usize> {let query = Query::from("todos");let rows = client.query(query, Some(DurabilityTier::GlobalServer)).await?;Ok(rows.len())}
Own writes
The read tier also determines how a subscription treats your own local writes:
"local-first"shows pending local writes using locally known data."local-first-unless-empty"shows pending local writes exactly like"local-first"."remote"has no pending overlay: writes appear when reflected in the server's query scope.
See Durability Tiers for the full reference, including which APIs accept these options and how they compose.
Magic columns
You can select and filter on Jazz's magic columns just like other columns. They are omitted from
select("*"), so opt in explicitly when you want them.
Edit metadata columns
$createdBy— the Jazz principal that created the row$createdAt— when the row was first created$updatedBy— the Jazz principal that last updated the row$updatedAt— when the row was last updated
These are useful for showing authorship, when a row last changed, or building "my created items" views without storing duplicate ownership fields.
export async function readTodoEditMetadata(db: Db, author: RowAuthor, updatedSinceMs: number) {return db.all(app.todos.where({$createdBy: author,$updatedAt: { gt: updatedSinceMs },}).select("title", "$createdBy", "$createdAt", "$updatedBy", "$updatedAt"),);}
export const readTodoEditMetadata = Effect.fn("readTodoEditMetadata")(function* (author: RowAuthor,updatedSinceMs: number,) {const jazz = yield* Jazz;return yield* jazz.all(app.todos.where({$createdBy: author,$updatedAt: { gt: updatedSinceMs },}).select("title", "$createdBy", "$createdAt", "$updatedBy", "$updatedAt"),);});
See Permissions for policy examples using $createdBy and the other
magic columns.
Framework hooks
Each framework has a reactive binding that re-renders when query results change. See Framework Patterns for side-by-side examples.
| Framework | API | Notes |
|---|---|---|
| React/Expo | useAll(query) | Returns { data, isLoading, error } |
| Vue | useAll(query) | Returns { data, isLoading, error } refs |
| Svelte | new QuerySubscription(query) | Exposes reactive .current / .isLoading / .error |
| Solid | useAll(() => ({ query })) | Returns { data, isLoading, error } |
Each one has a single-row variant, covered in Reading one row: useOne in
React, Expo, Vue and Solid, and QuerySubscriptionOne in Svelte.
The loading state
useAll's data field (or Vue's data ref and Svelte's QuerySubscription.current) are
undefined until the first result arrives from the requested tier. After that, the value is an
array — empty ([]) if no rows match, or populated with the requested data.
const allTodos = useAll(app.todos);// `allTodos.data` is `undefined` while loading the first result (and `allTodos.isLoading` is `true`).// `allTodos.data` is `[]` when loaded but empty
<script setup lang="ts">import { useAll } from "jazz-tools/vue";import { app } from "../schema.js";const { data: todos } = useAll(app.todos);// undefined = not yet connected; [] = connected, no rows; [...] = rows present</script><template><p v-if="todos === undefined">Connecting…</p><ul v-else><li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li></ul></template>
<script lang="ts">import { QuerySubscription } from 'jazz-tools/svelte';import { app } from '../schema.js';const todos = new QuerySubscription(app.todos);// .current: undefined = not yet connected; [] = connected, no rows; [...] = rows present</script>{#if todos.current === undefined}<p>Connecting…</p>{:else}<ul>{#each todos.current as todo}<li>{todo.title}</li>{/each}</ul>{/if}
import { For, Show } from "solid-js";import { useAll } from "jazz-tools/solid";import { app } from "../schema.js";export function LiveQueryExample() {const todos = useAll(() => ({ query: app.todos }));return (<Show when={todos.data !== undefined} fallback={<p>Connecting...</p>}><ul><For each={todos.data ?? []}>{(todo) => <li>{todo.title}</li>}</For></ul></Show>);}
Reading one row
For detail pages, use useOne (or QuerySubscriptionOne in Svelte). It takes the same query and
options as useAll, limits it to one row, and gives you that row instead of an array:
undefinedwhile the first result is loading, and also when the query failed (seeerror) or was skipped.nullonce loaded, if no row matches or you aren't allowed to read it.- The row itself otherwise, with any relations you asked for in
include(...).
export function TodoDetail({ id }: { id: string }) {const { data: todo, error } = useOne(app.todos.where({ id }).include({ project: true }));// `todo` is `undefined` while loading, `null` if no row matches or you can't read itif (error) return <p>Couldn't load this todo.</p>;if (todo === undefined) return <p>Loading…</p>;if (todo === null) return <p>Todo not found.</p>;return (<article><h1>{todo.title}</h1>{todo.project && <p>In {todo.project.name}</p>}</article>);}
<script setup lang="ts">import { useOne } from "jazz-tools/vue";import { app } from "../schema.js";const props = defineProps<{ id: string }>();const { data: todo } = useOne(() => app.todos.where({ id: props.id }));// undefined = loading; null = no row matches or you can't read it</script><template><p v-if="todo === undefined">Loading…</p><p v-else-if="todo === null">Todo not found.</p><h1 v-else>{{ todo.title }}</h1></template>
<script lang="ts">import { QuerySubscriptionOne } from 'jazz-tools/svelte';import { app } from '../schema.js';let { id }: { id: string } = $props();const todo = new QuerySubscriptionOne(() => app.todos.where({ id }));// .current: undefined = loading; null = no row matches or you can't read it</script>{#if todo.current === undefined}<p>Loading…</p>{:else if todo.current === null}<p>Todo not found.</p>{:else}<h1>{todo.current.title}</h1>{/if}
export function TodoDetailExample(props: { id: string }) {const todo = useOne(() => ({ query: app.todos.where({ id: props.id }) }));// todo.data: undefined = loading; null = no row matches or you can't read itreturn (<Switch><Match when={todo.data === undefined}><p>Loading…</p></Match><Match when={todo.data === null}><p>Todo not found.</p></Match><Match when={todo.data}>{(found) => <h1>{found().title}</h1>}</Match></Switch>);}
Like useAll, useOne accepts undefined instead of a query to skip evaluation. React and Vue
also have useOneSuspense, which waits for the first result instead of reporting a loading state
(see React Suspense and Transitions).
Fine-grained updates
Vue's useAll, Svelte's QuerySubscription, and Solid's useAll reconcile new query results into the existing reactive array in place rather than swapping the reference. When an upstream change touches a single row, only that row's affected fields write into the reactive proxy, so $effect (Svelte), Solid computations/effects, and watch / template bindings (Vue) only re-fire for components that depend on the fields that actually changed.
In practice this means:
- Adding, removing, or reordering rows updates the array structure only — untouched row objects keep their identity, so
{#each items as item (item.id)}and<TransitionGroup>keyed renders are stable. - Editing a single field on one row writes only that field — sibling rows do not re-render, and per-row components that read other fields stay quiet.
- Vue's
useAllreturns{ data, isLoading, error }, wheredatais a deepRef(not ashallowRef), so per-field reactivity composes with the rest of your component tree without manual unwrapping. - React and Solid return
{ data, isLoading, error }; React returns a freshdataarray each tick, while Solid's store keeps fine-grained dependency tracking for fields that changed.
React's granularity benefit comes from React's diffing, not from in-place reconciliation.
Conditional queries
Pass undefined instead of a query to skip evaluation. This is useful when building dynamic queries.
const [filter, setFilter] = useState<string | null>(null);const { data: filtered } = useAll(filter ? app.todos.where({ title: { contains: filter } }) : undefined,);
<script setup lang="ts">import { ref, computed } from "vue";import { useAll } from "jazz-tools/vue";import { app } from "../schema.js";const filter = ref<string | null>(null);const query = computed(() =>filter.value ? app.todos.where({ title: { contains: filter.value } }) : undefined,);const { data: filtered } = useAll(query);</script><template><input v-model="filter" placeholder="Filter by title" /><ul v-if="filtered"><li v-for="todo in filtered" :key="todo.id">{{ todo.title }}</li></ul></template>
<script lang="ts">import { QuerySubscription } from 'jazz-tools/svelte';import { app } from '../schema.js';let filter = $state<string | null>(null);const filtered = new QuerySubscription(() => filter ? app.todos.where({ title: { contains: filter } }) : undefined,);</script><input bind:value={filter} placeholder="Filter by title" />{#if filtered.current}<ul>{#each filtered.current as todo}<li>{todo.title}</li>{/each}</ul>{/if}
import { For, Show, createMemo, createSignal } from "solid-js";import { useAll } from "jazz-tools/solid";import { app } from "../schema.js";export function ConditionalQueryExample() {const [filter, setFilter] = createSignal<string | null>(null);const query = createMemo(() =>filter() ? app.todos.where({ title: { contains: filter()! } }) : undefined,);const filtered = useAll(() => ({ query: query() }));return (<><inputvalue={filter() ?? ""}onInput={(e) => setFilter(e.currentTarget.value || null)}placeholder="Filter by title"/><Show when={filtered.data}><ul><For each={filtered.data ?? []}>{(todo) => <li>{todo.title}</li>}</For></ul></Show></>);}
React Suspense and Transitions
useAllSuspense is a React-specific variant that suspends instead of returning a loading state, keeping the previous result visible while the next one loads.
App.tsxexport function ConcurrentTodoList() {const db = useDb();const [title, setTitle] = useState("");const [filterTitle, setFilterTitle] = useState("");const [showDoneOnly, setShowDoneOnly] = useState(false);const [page, setPage] = useState(0);const [isPending, startTransition] = useTransition();const deferredFilterTitle = useDeferredValue(filterTitle);let query = app.todos.orderBy("id", "desc").limit(25).offset(page * 25);if (deferredFilterTitle.trim()) {query = query.where({ title: { contains: deferredFilterTitle.trim() } });}if (showDoneOnly) {query = query.where({ done: true });}const isLoading = isPending || deferredFilterTitle !== filterTitle;function updatePage(nextPage: number) {startTransition(() => {setPage(nextPage);});}function handleFilterChange(e: React.ChangeEvent<HTMLInputElement>) {setFilterTitle(e.target.value);startTransition(() => {setPage(0);});}async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {e.preventDefault();const trimmedTitle = title.trim();if (!trimmedTitle) {return;}await db.insert(app.todos, { title: trimmedTitle, done: false });setTitle("");}return (<><form onSubmit={(e) => void handleSubmit(e)}><inputtype="text"value={title}onChange={(e) => setTitle(e.target.value)}placeholder="What needs to be done?"required/><button type="submit">Add</button></form><div><inputtype="text"value={filterTitle}onChange={handleFilterChange}placeholder="Filter by title (contains)"aria-label="Filter by title"/><label><inputtype="checkbox"checked={showDoneOnly}onChange={(e) => setShowDoneOnly(e.target.checked)}/>Done only</label></div><Suspense fallback={<p>Loading todos...</p>}><div style={{ opacity: isLoading ? 0.5 : 1, transition: "opacity 0.2s" }}><ConcurrentTodoResults query={query} page={page} onPageChange={updatePage} /></div></Suspense></>);}
useOneSuspense does the same for a single row. It returns the row, or null if no row matches
or you aren't allowed to read it.
export function TodoDetailPage({ id }: { id: string }) {return (<Suspense fallback={<p>Loading…</p>}><TodoTitle id={id} /></Suspense>);}function TodoTitle({ id }: { id: string }) {const todo = useOneSuspense(app.todos.where({ id }));return <h1>{todo ? todo.title : "Todo not found"}</h1>;}