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 }));
}

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 it
return 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));
}

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" } });

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.

TierWhereWhat confirmation means
"local"This deviceSaved by the local persistence driver, without waiting for the network.
"global"CoreAuthorized 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 an offset read 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 });
}
Remote subscriptions stay permission-scoped

Strict remote reads continue using Core-authorized state after the first result. Local-first reads can include cached data and optimistic writes; waiting for a write to reach Core does not change a separate subscription's read choice.

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"),
);
}

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.

FrameworkAPINotes
React/ExpouseAll(query)Returns { data, isLoading, error }
VueuseAll(query)Returns { data, isLoading, error } refs
Sveltenew QuerySubscription(query)Exposes reactive .current / .isLoading / .error
SoliduseAll(() => ({ 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
Note

You're unlikely to see data: undefined in practice unless you're awaiting a more durable tier. Local storage reads are effectively instant and resolve to [] if no data exists yet.

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:

  • undefined while the first result is loading, and also when the query failed (see error) or was skipped.
  • null once 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 it
​
if (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>
);
}

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 useAll returns { data, isLoading, error }, where data is a deep Ref (not a shallowRef), 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 fresh data array 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,
);

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.tsx
export 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)}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="What needs to be done?"
required
/>
<button type="submit">Add</button>
</form>
​
<div>
<input
type="text"
value={filterTitle}
onChange={handleFilterChange}
placeholder="Filter by title (contains)"
aria-label="Filter by title"
/>
<label>
<input
type="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>;
}