From c71ee8db8af28ed70fc1ef4a5738e8ba5c83cc94 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Wed, 14 Oct 2026 14:23:20 +0000 Subject: [PATCH 01/20] docs(queries): document the 'cached' result type Adds a Cached Results section: a query whose result the server confirmed during a previous session starts as 'cached', becomes 'complete' once the server returns it in this session, and goes back to 'unknown' if the stored result is evicted first. Lists all result types, and documents run({type: 'cached'}) and the cached promise from preload(). --- contents/docs/queries.mdx | 82 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/contents/docs/queries.mdx b/contents/docs/queries.mdx index a95342db..fbe71f6a 100644 --- a/contents/docs/queries.mdx +++ b/contents/docs/queries.mdx @@ -584,6 +584,15 @@ const results = await zero.run( ) ``` +To also accept a result the server confirmed during a previous session, pass `{type: 'cached'}`. It resolves as soon as the result is [`cached`](#cached-results) or `complete`, so it doesn't wait for the server when the client already stores a confirmed result: + +```tsx +const results = await zero.run( + queries.issues.byPriority('high'), + {type: 'cached'} +) +``` + ### For Preloading Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. @@ -601,6 +610,8 @@ zero.preload( ) ``` +`preload()` returns `cleanup`, which ends the preload (Zero keeps its rows for the query's [TTL](#ttls)), and two promises: `complete` resolves once the server has returned the result, and `cached` resolves once the result is [`cached`](#cached-results) or `complete`. + ## Missing Data Because Zero returns local results immediately and server results asynchronously, displaying "not found" / 404 UI can be slightly tricky. @@ -765,10 +776,79 @@ view.addListener((issues, resultType) => { -The possible values of `result.type` are currently `complete` and `unknown`. +The possible values of `result.type` are: + +- `unknown`: the data Zero has on the client. Some of it may be missing. +- `cached`: the client stores a result that the server confirmed during a previous session. See [Cached Results](#cached-results). +- `complete`: the server has returned the result during this session. +- `error`: the query failed. See [Handling Errors](#handling-errors). The `complete` value is currently only returned when Zero has received the server result. In the future, Zero will be able to return this result type when it _knows_ that all possible data for this query is already available locally. Additionally, we plan to add a `prefix` result for when the data is known to be a prefix of the complete result. See [Consistency](#consistency) for more information. +### Cached Results + +When Zero stores data on the client (in IndexedDB, or SQLite on React Native), it also remembers which query results the server confirmed. If the client still holds such a result when the app starts again, the query starts as `cached` instead of `unknown`, and becomes `complete` once the server returns the result in this session. If Zero evicts the stored result before then, the query goes back to `unknown`. + +A `cached` result was complete when the server sent it, but it may be out of date. Use it where a previous session's answer is good enough, for example to show stored data or an empty state without waiting for the server, which also works while offline. Wait for `complete` where the data must be current: + + + +```tsx +const [issues, issuesResult] = useQuery( + queries.issues.inbox() +) + +const confirmed = + issuesResult.type === 'cached' || + issuesResult.type === 'complete' + +if (issues.length === 0 && confirmed) { + return
Your inbox is empty
+} +``` + +```tsx +const [issues, issuesResult] = useQuery(() => + queries.issues.inbox() +) + +const confirmed = () => + issuesResult().type === 'cached' || + issuesResult().type === 'complete' + +return ( + 0 || !confirmed()} + fallback={
Your inbox is empty
} + > + {/* ... */} +
+) +``` + +```ts +const view = zero.materialize(queries.issues.inbox()) + +view.addListener((issues, resultType) => { + if ( + issues.length === 0 && + (resultType === 'cached' || resultType === 'complete') + ) { + console.log('Inbox is empty') + } +}) +``` + +
+ +`zero.run(query, {type: 'cached'})` and the `cached` promise from `zero.preload()` resolve on a `cached` or `complete` result. `{type: 'complete'}` and the `complete` promise still wait for the server. + ## Handling Errors If the queries endpoint throws an application or parse error, `zero-cache` will report it to the client using the `type` and `error` fields on the query details object: From 7aab37af274fe6b0f36234ffe93d75ccd50b8697 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Wed, 14 Oct 2026 15:47:02 +0000 Subject: [PATCH 02/20] docs(connection): document the initializing connection state --- contents/docs/connection.mdx | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/contents/docs/connection.mdx b/contents/docs/connection.mdx index 7a197c73..c6474089 100644 --- a/contents/docs/connection.mdx +++ b/contents/docs/connection.mdx @@ -12,6 +12,8 @@ Zero manages a persistent connection to `zero-cache` with the following lifecycl invert="light" /> +Before this lifecycle starts, Zero is in the [`initializing`](#initializing) state while it loads local data. It then moves to `connecting` and never returns to `initializing`. + ## Usage The current connection state is available in the `zero.connection.state` property. This is subscribable and also has reactive hooks for React and SolidJS: @@ -40,6 +42,8 @@ function ConnectionStatus() { const state = useConnectionState() switch (state.name) { + case 'initializing': + return
Loading...
case 'connecting': return
Connecting...
case 'connected': @@ -64,6 +68,10 @@ function ConnectionStatus() { return ( + +
Loading...
+
+
Connecting...
@@ -91,6 +99,9 @@ function ConnectionStatus() { ```ts zero.connection.state.subscribe(state => { switch (state.name) { + case 'initializing': + console.log('Loading local data...') + break case 'connecting': console.log(`Connecting... ${state.reason}`) break @@ -120,6 +131,7 @@ Zero [does not support offline writes](#why-zero-doesnt-support-offline-writes). | State | Reads | Writes | | -------------- | ----- | ----------- | +| `initializing` | ✅ | ✅ (queued) | | `connecting` | ✅ | ✅ (queued) | | `connected` | ✅ | ✅ | | `disconnected` | ✅ | ❌ | @@ -137,11 +149,25 @@ The easiest way to implement this is with a modal overlay that covers the entire ## Details +### Initializing + +Zero starts in the `initializing` state. While `initializing`, Zero loads its local data. It has not contacted `zero-cache` yet. + +How long this takes depends on the device and the amount of local data, not on the network. On a slow device with a lot of local data, it can take tens of seconds. + +Once the local data is loaded, Zero transitions to `connecting`. Zero is only `initializing` once, right after it is created. When Zero reconnects later, it goes straight to `connecting`. + +Show a loading indicator while `initializing`, not an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters `connecting`, not when you create Zero. + +There is no timeout while `initializing`, and the time spent here does not count against `disconnectTimeoutMs`. If the local data never finishes loading, Zero stays in `initializing`. To limit this, add your own timeout. + +Reads and writes are allowed while `initializing`. Queries return results once the local data is loaded. Writes are queued and are sent when Zero connects. + ### Connecting -Zero starts in the `connecting` state. +Zero enters the `connecting` state after [`initializing`](#initializing). -While `connecting`, Zero repeatedly tries to connect to `zero-cache`. After 1 minute of failed attempts, it transitions to `disconnected`. This timeout can be configured with the `disconnectTimeoutMs` constructor parameter: +While `connecting`, Zero repeatedly tries to connect to `zero-cache`. After 1 minute of failed attempts, it transitions to `disconnected`. Time spent `initializing` does not count toward this minute. This timeout can be configured with the `disconnectTimeoutMs` constructor parameter: ```tsx const opts: ZeroOptions = { From ad8b067092ae3c065228678b9eb2dce32758e9e0 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Wed, 14 Oct 2026 15:48:45 +0000 Subject: [PATCH 03/20] docs(connection): document client storage failures and zero.kvStore --- contents/docs/connection.mdx | 57 ++++++++++++++++++++++++++++++++++ contents/docs/react-native.mdx | 2 ++ 2 files changed, 59 insertions(+) diff --git a/contents/docs/connection.mdx b/contents/docs/connection.mdx index c6474089..2be62c67 100644 --- a/contents/docs/connection.mdx +++ b/contents/docs/connection.mdx @@ -255,6 +255,63 @@ Most applications will never call `close()`, and even if they do, they should no Reads and writes are both rejected while Zero is in the `closed` state. +## Storage Failures + +Zero saves synced data on the device: in IndexedDB in the browser, or in SQLite on [React Native](/docs/react-native). This storage can fail. For example, the device can run out of space, or the database can fail to open. + +When this happens, Zero calls `onStorageFailure` once with a `StorageFailureError`. Its `kind` says what failed: + +| `kind` | Meaning | +| ------------- | --------------------------------------- | +| `full` | The device is out of space. | +| `cannot-open` | The database can't be opened. | +| `io-error` | A read or write to the database failed. | + +In the browser, Zero reports a storage failure only when IndexedDB fails to open: `full` if the browser says the disk is full, otherwise `cannot-open`. + +Zero keeps working after a storage failure: + +- **If storage fails while Zero is starting**, Zero runs in memory for the rest of the session, as if you had set `kvStore: 'mem'`. Nothing is saved on the device. +- **If storage fails later**, Zero stops saving data on the device. Queries and mutations keep working on the data in memory, and mutations are still sent to `zero-cache`. A read that needs data Zero hasn't loaded into memory can still fail. + +Zero tries the device's storage again only when the app creates a new `Zero` instance, for example after a restart. + +By default, Zero only logs the failure. Use `onStorageFailure` to tell the user what to do: + +```ts +import type {ZeroOptions} from '@rocicorp/zero' + +const opts: ZeroOptions = { + // ... + onStorageFailure: failure => { + // showBanner stands in for your app's own UI. + if (failure.kind === 'full') { + showBanner( + 'Your device is out of space. Free up some space, then restart the app.' + ) + } else { + showBanner( + 'Data could not be saved on this device. Restart the app to try again.' + ) + } + } +} +``` + +`StorageFailureError` is exported from `@rocicorp/zero`. Its `cause` is the original error from the browser or the SQLite driver. A custom `kvStore` can throw a `StorageFailureError` to get the same handling. + +### Checking the Store + +`zero.kvStore.kind` tells you which storage Zero is using: `'idb'`, `'mem'`, `'expo-sqlite'`, or `'op-sqlite'`. A custom store reports its own `kind`, if it sets one. + +```ts +if (zero.kvStore.kind === 'mem') { + // Data is not saved on this device. +} +``` + +Without IndexedDB, `kind` is `'mem'` from the start and `onStorageFailure` isn't called. When IndexedDB fails to open, `kind` changes to `'mem'` only when Zero first reads or writes the store, so check `kind` when you need it, not once at startup. + ## Why Zero Doesn't Support Offline Writes Supporting offline writes in collaborative applications is inherently difficult, and no sync engine or CRDT algorithm can automatically solve it for you. Despite what their marketing says 😉. diff --git a/contents/docs/react-native.mdx b/contents/docs/react-native.mdx index cdf5097d..b7b5f26e 100644 --- a/contents/docs/react-native.mdx +++ b/contents/docs/react-native.mdx @@ -55,6 +55,8 @@ export default function RootLayout() { For a complete example, see [zslack](./samples#zslack). +To handle a full disk or a database that fails to open, see [Storage Failures](./connection#storage-failures). + Date: Wed, 14 Oct 2026 15:49:43 +0000 Subject: [PATCH 04/20] docs(queries): explain query identity and building queries inline in React --- contents/docs/react.mdx | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/contents/docs/react.mdx b/contents/docs/react.mdx index b430073d..3b621852 100644 --- a/contents/docs/react.mdx +++ b/contents/docs/react.mdx @@ -115,6 +115,45 @@ const [issues] = useSuspenseQuery(issueQuery, { Use the `suspendUntil` parameter to control how long to suspend for. The value `complete` suspends until authoritative results from the server are received. The `partial` value suspends until any non-empty data is received, or for a empty result that is `complete`. +## Query Identity + +You don't need to memoize queries. Build them inline, where you use them, and pass them straight to `useQuery`. + +Zero caches the queries you build with [ZQL](/docs/zql). Build the same query again and you usually get back the same `Query` object, even when it has `related()` or `whereExists()` callbacks written inline. So rebuilding a query on every render is cheap. You can also use a query as a dependency of `useEffect` or `useMemo`, or pass it as a prop to a component wrapped in `React.memo`: + +```tsx +import {useEffect} from 'react' +import {useQuery} from '@rocicorp/zero/react' +import {zql} from 'schema.ts' + +function IssueSearch({text}: {text: string}) { + // Rebuilt on every render. No useMemo needed. + const query = zql.issue + .where('title', 'ILIKE', `%${text}%`) + .orderBy('created', 'desc') + const [issues] = useQuery(query) + + // Scroll back to the top when the search changes. + useEffect(() => { + window.scrollTo(0, 0) + }, [query]) + + return issues.map(issue => ( +
{issue.title}
+ )) +} +``` + +This example passes ZQL to `useQuery` directly, so it runs [only on the client](/docs/queries#local-only-queries). + +Calls to [registered queries](/docs/queries#query-registries) work a little differently. Each call like `queries.posts.byStatus({status})` returns a new object. You can still pass it inline: `useQuery` sees that it's the same query with the same arguments and reuses its previous work. But don't use the returned object as a dependency. Use its arguments instead. + +Keep in mind: + +- Identity follows how you build a query. `zql.issue.where('status', 'open').where('priority', 'high')` and `zql.issue.where('priority', 'high').where('status', 'open')` return the same rows, but they can be different objects. +- A different value, such as a different ID in `where()`, gives a different object. +- Identity is an optimization, not a promise. Zero can hand back a new object for a query it has built before. When that happens, an effect or memo that depends on the query just runs again, so make sure that's safe. + ## Examples See [the sample directory](/docs/samples) for more complete React examples. From 80bf38a4395526fa481a3121d6fce567c92124d0 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Wed, 14 Oct 2026 15:50:27 +0000 Subject: [PATCH 05/20] docs(mutators): document return values, tx.retryOn() and isolation levels --- contents/docs/mutators.mdx | 129 ++++++++++++++++++++++++++++++++--- contents/docs/server-zql.mdx | 78 +++++++++++++++++++++ 2 files changed, 199 insertions(+), 8 deletions(-) diff --git a/contents/docs/mutators.mdx b/contents/docs/mutators.mdx index 588bf923..d4158264 100644 --- a/contents/docs/mutators.mdx +++ b/contents/docs/mutators.mdx @@ -864,6 +864,75 @@ If Zero receives HTTP 401 or 403, the client will enter the needs auth state and If you want a different behavior, it is possible to [implement the mutate endpoint](#custom-mutate-implementation) yourself and handle errors differently. +### Retrying Mutators + +Some errors go away if you try again. For example, at the `repeatable read` or `serializable` [isolation level](/docs/server-zql#isolation-level), Postgres can abort a transaction that conflicts with a concurrent one. The error is a serialization failure, code `40001`. + +Call `tx.retryOn()` in a server mutator to have Zero run the mutator again, in a fresh transaction, when it fails with an error you choose: + +```ts +function isSerializationFailure(error: unknown) { + return ( + error instanceof Error && + 'code' in error && + error.code === '40001' + ) +} + +const updateIssue = defineMutator( + z.object({id: z.string(), title: z.string()}), + async ({tx, args: {id, title}}) => { + if (tx.location === 'server') { + tx.retryOn(isSerializationFailure) + } + await tx.mutate.issue.update({id, title}) + } +) +``` + +`retryOn()` takes a predicate. It gets the error and the attempt number: `1` when deciding the first re-run, `2` for the second, and so on. Return `true` to run the mutator again. If it returns anything else or throws, Zero records the error and skips the mutation, as with any [other error](#handling-errors). + +The predicate sees errors the mutator throws, and also errors Postgres raises at `COMMIT`, which the mutator can't catch. Everything the failed run wrote is rolled back, and the re-run reads fresh data. + +The error is passed as it was thrown. node-postgres and postgres.js errors have the Postgres error code in `code`. Other libraries can wrap the error, so check what yours throws. + +The predicate can also return a promise. Zero waits for it, so you can use it to back off before the next run: + +```ts +tx.retryOn(async (error, attempt) => { + if (!isSerializationFailure(error)) { + return false + } + // Wait 50ms, then 100ms, then 150ms, and so on. + await new Promise(resolve => + setTimeout(resolve, attempt * 50) + ) + return true +}) +``` + +A few things to know: + +- `retryOn()` covers only the current run. The re-run calls it again because it runs the mutator again. Call it before anything that can fail. +- You can call `retryOn()` more than once. Zero runs the mutator again if any predicate returns `true`. +- Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run fails, Zero records the error. +- The predicate runs after the transaction has ended, so it can't use `tx`. Reading or writing through `tx` there throws. +- Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. +- `retryOn()` is only available on the server, when `tx.location === 'server'`. + + + The built-in adapters and `ZQLDatabase` support + `retryOn()`. If you pass your own `Database` to + `handleMutateRequest`, it must pass the + `addRetryPredicate` it receives in + `TransactionProviderInput` on to the transaction it + creates with `CRUDMutatorFactory.createTransaction()`. + Otherwise `tx.retryOn()` throws. + + ### Custom Mutate URL By default, Zero sends mutations to the URL specified in the `ZERO_MUTATE_URL` parameter. @@ -1064,14 +1133,58 @@ const read2 = await zero.run( If the client-side mutator fails, `.server` also resolves to an error result. Awaiting `.server` therefore covers both client- and server-side failures. - - There is not yet a way to return data from mutators in the - success case. [Let us know](https://discord.rocicorp.dev/) - if you need this. - +### Returning Data + +A mutator can return a JSON value. The caller gets it in the `data` field of the success result. + +The client and server runs can return different values. For example, the server can return a value that only the database knows: + +```ts +const createIssue = defineMutator( + z.object({id: z.string(), title: z.string()}), + async ({tx, args: {id, title}}) => { + await tx.mutate.issue.insert({id, title}) + + if (tx.location === 'client') { + // The client doesn't know the short ID yet. + return {shortID: null} + } + + // Postgres assigns shortID when the row is inserted. + const issue = await tx.run( + zql.issue.where('id', id).one() + ) + return {shortID: issue?.shortID ?? null} + } +) +``` + +The `data` of the `.client` result is what the client run returned. The `data` of the `.server` result is what the server run returned: + +```ts +const write = zero.mutate( + mutators.issue.create({ + id: crypto.randomUUID(), + title: 'New title' + }) +) + +const serverRes = await write.server +if (serverRes.type === 'success') { + // `data` is a plain JSON value, so check its shape. + const {shortID} = z + .object({shortID: z.number().nullable()}) + .parse(serverRes.data) + console.log('Short ID:', shortID) +} +``` + +A few things to know: + +- `data` is typed as `ReadonlyJSONValue | undefined`, not as the mutator's return type. Check its shape before you use it. +- If the mutator returns nothing, `data` is `undefined`. +- The server value only reaches the client if your [mutate handler](#implementing-the-endpoint) returns the mutator's result, as `return mutator.fn({args, tx})` does in the examples above. +- Zero saves the server's return value in your database along with the mutation's result, so the client still gets it if the response from your mutate endpoint is lost. A mutator that returns nothing doesn't write this. ## Permissions diff --git a/contents/docs/server-zql.mdx b/contents/docs/server-zql.mdx index b3be33f8..4931459d 100644 --- a/contents/docs/server-zql.mdx +++ b/contents/docs/server-zql.mdx @@ -287,6 +287,84 @@ export const mutators = defineMutators({ +### Isolation Level + +By default, each transaction starts with a plain `BEGIN`, so Postgres uses its default isolation level. On a stock Postgres, that is `read committed`. + +To use a different level, pass `isolationLevel` in the options argument of the adapter factory. It can be `'read committed'`, `'repeatable read'`, or `'serializable'`: + + + +```ts +export const dbProvider = zeroDrizzle( + schema, + drizzleClient, + { + isolationLevel: 'serializable' + } +) +``` + +```ts +export const dbProvider = zeroKysely(schema, kysely, { + isolationLevel: 'serializable' +}) +``` + +```ts +// Use the Postgres spelling, not Prisma's 'Serializable'. +export const dbProvider = zeroPrisma(schema, prisma, { + isolationLevel: 'serializable' +}) +``` + +```ts +// The third argument is an optional logger. +export const dbProvider = zeroNodePg( + schema, + pool, + undefined, + { + isolationLevel: 'serializable' + } +) +``` + +```ts +export const dbProvider = zeroPostgresJS(schema, sql, { + isolationLevel: 'serializable' +}) +``` + + + +Every transaction the adapter opens uses this level, including the ones your mutators run in. The `TransactionOptions` and `IsolationLevel` types are exported from `@rocicorp/zero/server`. + +At `repeatable read` and `serializable`, Postgres can abort a transaction that conflicts with a concurrent one. Use [`tx.retryOn()`](/docs/mutators#retrying-mutators) to run the mutator again when that happens. + ### Custom Database To implement support for some other Postgres bindings library, you will implement the `DBConnection` interface. From e75f90530bc5437b22570a2361a0a842077f65c9 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Wed, 14 Oct 2026 15:54:33 +0000 Subject: [PATCH 06/20] docs(zero-cache-config): document legacy query, operator password, readiness and hydration options Add ZERO_ALLOW_LEGACY_QUERIES, ZERO_OPERATOR_PASSWORD, ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS, ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS, ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS and ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS. Update defaults (replication lag report interval, CVR GC threshold, flow control consensus proportion, auth revalidate/retransform intervals, litestream v5 backups, multipart concurrency), remove the litestream VFS options, and move ZERO_CHANGE_STREAMER_MODE and ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT to deprecated flags. --- contents/docs/debug/slow-queries.mdx | 2 +- contents/docs/deprecated/ad-hoc-queries.mdx | 2 + contents/docs/zero-cache-config.mdx | 201 +++++++++++++------- 3 files changed, 130 insertions(+), 75 deletions(-) diff --git a/contents/docs/debug/slow-queries.mdx b/contents/docs/debug/slow-queries.mdx index 7b248531..6792a297 100644 --- a/contents/docs/debug/slow-queries.mdx +++ b/contents/docs/debug/slow-queries.mdx @@ -45,4 +45,4 @@ Fly.io provides physically attached SSDs, even for their smallest VMs. Deploying ## /statz -`zero-cache` makes some internal health statistics available via the `/statz` endpoint of `zero-cache`. In order to access this, you must configure an [admin password](/docs/zero-cache-config#admin-password). +`zero-cache` makes some internal health statistics available via the `/statz` endpoint of `zero-cache`. In order to access this, you must configure an [admin password](/docs/zero-cache-config#admin-password) or an [operator password](/docs/zero-cache-config#operator-password). diff --git a/contents/docs/deprecated/ad-hoc-queries.mdx b/contents/docs/deprecated/ad-hoc-queries.mdx index 91eab209..4cd46201 100644 --- a/contents/docs/deprecated/ad-hoc-queries.mdx +++ b/contents/docs/deprecated/ad-hoc-queries.mdx @@ -23,6 +23,8 @@ const schema = createSchema({ }) ``` +Ad-hoc queries send their query ASTs directly to `zero-cache`, which rejects them by default. To allow them, also set [`ZERO_ALLOW_LEGACY_QUERIES`](/docs/zero-cache-config#allow-legacy-queries) to `true` on `zero-cache`. + Once enabled, queries are available at `z.query.`. ```tsx diff --git a/contents/docs/zero-cache-config.mdx b/contents/docs/zero-cache-config.mdx index 98d676d6..89379f90 100644 --- a/contents/docs/zero-cache-config.mdx +++ b/contents/docs/zero-cache-config.mdx @@ -22,12 +22,24 @@ A password used to administer zero-cache server, for example to access the `/sta This is required in production (when `NODE_ENV=production`) because we want all Zero servers to be debuggable using admin tools by default, without needing a restart. But we also don't want to expose sensitive data using them. +The admin password also grants access to everything that the [operator password](#operator-password) does. + flag: `--admin-password`
env: `ZERO_ADMIN_PASSWORD`
required: in production (when `NODE_ENV=production`) ## Optional Flags +### Allow Legacy Queries + +Allows clients to send query ASTs directly to `zero-cache`. [Ad-hoc queries](/docs/deprecated/ad-hoc-queries) (also called legacy queries) work this way. + +When this is `false`, `zero-cache` rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use [queries](/docs/queries). + +flag: `--allow-legacy-queries`
+env: `ZERO_ALLOW_LEGACY_QUERIES`
+default: `false` + ### App ID Unique identifier for the app. @@ -82,7 +94,7 @@ Use this to bound how long already-open connections can continue after logout, s flag: `--auth-revalidate-interval-seconds`
env: `ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS`
-default: `unset` +default: `300` ### Auth Retransform Interval Seconds @@ -96,7 +108,7 @@ If clients already call `updateAuth` whenever auth changes, this mainly serves a flag: `--auth-retransform-interval-seconds`
env: `ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS`
-default: `unset` +default: `300` ### Auto Reset @@ -131,13 +143,13 @@ default: `0.04` During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. -For example, if the majority responds in 2.5 ms, a proportion of `2` waits up to another 5 ms. The default lets a subscriber take roughly three times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. +For example, if the majority responds in 2.5 ms, a proportion of `2` waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: `--change-streamer-flow-control-consensus-timeout-proportion`
env: `ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION`
-default: `2` +default: `4` ### Change Streamer Flow Control Slow Subscriber Grace Period Seconds @@ -151,26 +163,6 @@ flag: `--change-streamer-flow-control-slow-subscriber-grace-period-seconds`
env: `ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS`
default: `30` -### Change Streamer Mode - -The mode for running or connecting to the change-streamer: - -- `dedicated`: runs the change-streamer and shuts down when another - change-streamer takes over the replication slot. This is appropriate in a - single-node configuration, or for the replication-manager in a - multi-node configuration. -- `discover`: connects to the change-streamer as internally advertised in the - change-db. This is appropriate for the view-syncers in a multi-node setup. - This may not work in all networking configurations (e.g., some private - networking or port forwarding setups). Using `ZERO_CHANGE_STREAMER_URI` with - an explicit routable hostname is recommended instead. - -This option is ignored if `ZERO_CHANGE_STREAMER_URI` is set. - -flag: `--change-streamer-mode`
-env: `ZERO_CHANGE_STREAMER_MODE`
-default: `dedicated` - ### Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to `--port + 1`. @@ -205,9 +197,11 @@ env: `ZERO_CVR_DB`
The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. +Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. + flag: `--cvr-garbage-collection-inactivity-threshold-hours`
env: `ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS`
-default: `48` +default: `168` (one week) ### CVR Garbage Collection Initial Batch Size @@ -296,7 +290,7 @@ The location of the litestream backup, usually an s3:// URL. This is only consul In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. -When enabling Litestream v5 backups, configure this as a folder URL ending in `/`, for example `s3://acme-zero-backups/v1/`. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. +With Litestream v5 backups (the default), configure this as a folder URL ending in `/`, for example `s3://acme-zero-backups/v1/`. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: `--litestream-backup-url`
env: `ZERO_LITESTREAM_BACKUP_URL`
@@ -312,7 +306,7 @@ env: `ZERO_LITESTREAM_ENDPOINT`
### Litestream Checkpoint Threshold MB -The size of the WAL file at which to perform an SQLite checkpoint to apply the writes in the WAL to the main database file. Each checkpoint creates a new WAL segment file that will be backed up by Litestream v3. Smaller thresholds may improve read performance, at the expense of creating more files to download when restoring the replica from the backup. This setting is ignored when backing up with Litestream v5. +The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: `--litestream-checkpoint-threshold-mb`
env: `ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB`
@@ -348,14 +342,14 @@ env: `ZERO_LITESTREAM_EXECUTABLE`
### Litestream V5 Executable -Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and `ZERO_LITESTREAM_RESTORE_USING_V5` is enabled, this executable is required. The official Zero Docker image includes Litestream 0.5.16. +Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and `ZERO_LITESTREAM_RESTORE_USING_V5` or `ZERO_LITESTREAM_BACKUP_USING_V5` is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build `0.5.18-zero.16`. Restores fork the source backup with `litestream restore -fork-to-url`, so a custom executable must support that flag. flag: `--litestream-executable-v5`
env: `ZERO_LITESTREAM_EXECUTABLE_V5`
### Litestream Restore Using V5 -Use `ZERO_LITESTREAM_EXECUTABLE_V5` for restores. Set this to `false` to force legacy restore behavior. When a backup URL is configured, disabling this option is also the way to run without a v5 executable. +Use `ZERO_LITESTREAM_EXECUTABLE_V5` for restores. Set this to `false` to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and `ZERO_LITESTREAM_BACKUP_USING_V5` to `false`. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. @@ -365,43 +359,15 @@ default: `true` ### Litestream Backup Using V5 -Write LTX backups with Litestream v0.5.x. This is disabled by default to continue writing legacy WAL backups. Enabling it requires v5 restore and `ZERO_LITESTREAM_VFS_QUERY_EXECUTABLE`. +Write LTX backups with Litestream v0.5.x. Set this to `false` to write legacy WAL backups with Litestream v0.3.x instead. This requires `ZERO_LITESTREAM_RESTORE_USING_V5`, which is enabled by default. -With v5 backup enabled, Zero acknowledges upstream changes only after they are applied to the replica and confirmed in the backup. Each replica uses a distinct backup subpath under the folder URL configured by `ZERO_LITESTREAM_BACKUP_URL`. Configure an object-store lifecycle policy to remove old subpaths; Zero does not delete them automatically. Rolling back to a server that does not understand replica-specific paths can require a full resync. +Zero requests an incremental backup every `ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS`, and acknowledges upstream changes only after a backup containing them has completed. + +Each replication-manager backs up to its own subfolder of the folder URL configured by `ZERO_LITESTREAM_BACKUP_URL`. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about `ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS` old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: `--litestream-backup-using-v5`
env: `ZERO_LITESTREAM_BACKUP_USING_V5`
-default: `false` - -### Litestream VFS Query Executable - -Path to the `vfs-query` executable used to monitor Litestream v5 backup progress. This is required when `ZERO_LITESTREAM_BACKUP_USING_V5` is enabled. The official Zero Docker image includes this executable. - -flag: `--litestream-vfs-query-executable`
-env: `ZERO_LITESTREAM_VFS_QUERY_EXECUTABLE`
- -### Litestream VFS Poll Interval (ms) - -How often the Litestream VFS polls backup storage for the latest backup. This controls how quickly Zero can confirm a backup and acknowledge the corresponding upstream changes. - -flag: `--litestream-vfs-poll-interval-ms`
-env: `ZERO_LITESTREAM_VFS_POLL_INTERVAL_MS`
-default: `15000` - -### Litestream VFS Poll Timeout (ms) - -Reserved for configuring request timeouts to the Litestream VFS poller. In this release, `vfs-query` uses its internal 30-second timeout regardless of this value. - -flag: `--litestream-vfs-poll-timeout-ms`
-env: `ZERO_LITESTREAM_VFS_POLL_TIMEOUT_MS`
-default: `10000` - -### Litestream VFS Log File - -Reserved for redirecting Litestream VFS logs. In this release, `vfs-query` writes logs to the child process's standard error regardless of this value. - -flag: `--litestream-vfs-log-file`
-env: `ZERO_LITESTREAM_VFS_LOG_FILE`
+default: `true` ### Litestream Incremental Backup Interval Minutes @@ -427,21 +393,14 @@ flag: `--litestream-max-checkpoint-page-count`
env: `ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT`
default: `minCheckpointPageCount * 10`
-### Litestream Minimum Checkpoint Page Count - -The WAL page count at which SQLite attempts a PASSIVE checkpoint, which transfers pages to the main database file without blocking writers. Defaults to `checkpointThresholdMB * 250` (since SQLite page size is 4KB). This setting is ignored when backing up with Litestream v5. - -flag: `--litestream-min-checkpoint-page-count`
-env: `ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT`
-default: `checkpointThresholdMB * 250`
- ### Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. +Leave this unset to use `48`, lowered as needed so that the parts fit in a quarter of the memory available to the container. + flag: `--litestream-multipart-concurrency`
env: `ZERO_LITESTREAM_MULTIPART_CONCURRENCY`
-default: `48` ### Litestream Multipart Size @@ -607,6 +566,23 @@ The number of processes to use for view syncing. Leave this unset to use `max(1, flag: `--num-sync-workers`
env: `ZERO_NUM_SYNC_WORKERS`
+### Operator Password + +A password that grants access only to the diagnostic endpoints that cannot expose application data. People who operate `zero-cache` can use it to diagnose the server without being able to read the data it serves. These endpoints are: + +- `/profz` and `/profrmz`: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain `LIKE` patterns from client queries. +- `/statz`: counts and sizes. +- `/plannerz`: the replica's schema and planner statistics. + +The other protected endpoints, the [inspector](/docs/debug/inspector) and `/heapz`, still require the [admin password](#admin-password). + +For `/profrmz` in a multi-node deployment, the replication-manager must be configured with the same operator password. + +If the operator password is the same as the admin password, it grants admin access. + +flag: `--operator-password`
+env: `ZERO_OPERATOR_PASSWORD`
+ ### Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. @@ -728,17 +704,39 @@ Performs a VACUUM at server startup if the specified number of hours has elapsed flag: `--replica-vacuum-interval-hours`
env: `ZERO_REPLICA_VACUUM_INTERVAL_HOURS`
+### Replication Lag Initial Readiness Threshold (ms) + +The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. + +This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. + +When [lag reporting](#replication-lag-report-interval-ms) is disabled, readiness does not wait for replication lag. + +flag: `--replication-lag-initial-readiness-threshold-ms`
+env: `ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS`
+default: `1000` + +### Replication Lag Max Readiness Delay (ms) + +The maximum time that a server waits for replication lag to fall below [`ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS`](#replication-lag-initial-readiness-threshold-ms) before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. + +flag: `--replication-lag-max-readiness-delay-ms`
+env: `ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS`
+default: `600000` (10 minutes) + ### Replication Lag Report Interval (ms) -The minimum interval at which replication lag reports are written upstream and reported via the `zero.replication.total_lag` [OpenTelemetry metric](/docs/otel). If an expected report is not received before the next interval, Zero emits a new report and increments `zero.replication.lag_report_retries`. +The approximate interval at which replication lag reports are written upstream and reported via the `zero.replication.total_lag` [OpenTelemetry metric](/docs/otel). If an expected report is not received before the next interval, Zero emits a new report and increments `zero.replication.lag_report_retries`. + +Lag reports are also used at startup: a new server waits for replication lag to fall below [`ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS`](#replication-lag-initial-readiness-threshold-ms) before it reports ready. This interval sets how long the server waits for the next report. -This feature requires write access to upstream Postgres (uses `pg_logical_emit_message()`). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting. +This feature requires write access to upstream Postgres (uses `pg_logical_emit_message()`). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: `--replication-lag-report-interval-ms`
env: `ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS`
-default: `30_000` +default: `10000` ### Server Version @@ -824,6 +822,30 @@ See [WAL Sender Timeout](/docs/connecting-to-postgres#wal-sender-timeout). flag: `--upstream-pg-stream-inbound-timeout-ms`
env: `ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS`
+### View Syncer Hydration Budget (ms) + +A soft time limit for hydrating [inactive (cached) queries](/docs/queries#query-caching) when a view-syncer hydrates a client group's queries, for example after a client reconnects. + +Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your [query endpoint](#query-url) does not count toward the budget. + +A value of `0` disables the budget. + +flag: `--view-syncer-hydration-budget-ms`
+env: `ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS`
+default: `0` + +### View Syncer Query Hydration Timeout (ms) + +The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. + +If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see [Handling Errors](/docs/queries#handling-errors)). For the next five minutes, the query is rejected without being run again. After that, it can be retried. + +A value of `0` disables the limit. + +flag: `--view-syncer-query-hydration-timeout-ms`
+env: `ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS`
+default: `0` + ### Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression @@ -864,6 +886,37 @@ default: `10` flag: `--change-max-conns`
env: `ZERO_CHANGE_MAX_CONNS`
+### Change Streamer Mode + +`ZERO_CHANGE_STREAMER_MODE` is deprecated. Set [`ZERO_CHANGE_STREAMER_URI`](#change-streamer-uri) on view-syncers instead. + +The mode for running or connecting to the change-streamer: + +- `dedicated`: runs the change-streamer and shuts down when another + change-streamer takes over the replication slot. This is appropriate in a + single-node configuration, or for the replication-manager in a + multi-node configuration. +- `discover`: connects to the change-streamer as internally advertised in the + change-db. This is appropriate for the view-syncers in a multi-node setup. + This may not work in all networking configurations (e.g., some private + networking or port forwarding setups). + +This option is ignored if `ZERO_CHANGE_STREAMER_URI` is set. + +flag: `--change-streamer-mode`
+env: `ZERO_CHANGE_STREAMER_MODE`
+default: `dedicated` + +### Litestream Minimum Checkpoint Page Count + +`ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT` is deprecated. Use [`ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB`](#litestream-checkpoint-threshold-mb) instead. + +The WAL page count at which SQLite attempts a PASSIVE checkpoint. If unset, it is derived from the checkpoint threshold as `checkpointThresholdMB * 256` (4 KiB pages). + +flag: `--litestream-min-checkpoint-page-count`
+env: `ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT`
+default: `checkpointThresholdMB * 256` + ### Auth JWK A public key in JWK format used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. From 94e3a1eaba77903beb29272af3b532a2adbf3d36 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Wed, 14 Oct 2026 18:26:34 +0000 Subject: [PATCH 07/20] docs(release-notes): add Zero 1.11 release notes --- contents/docs/release-notes/1.11.mdx | 229 ++++++++++++++++++++++++++ contents/docs/release-notes/index.mdx | 1 + 2 files changed, 230 insertions(+) create mode 100644 contents/docs/release-notes/1.11.mdx diff --git a/contents/docs/release-notes/1.11.mdx b/contents/docs/release-notes/1.11.mdx new file mode 100644 index 00000000..c32fcff9 --- /dev/null +++ b/contents/docs/release-notes/1.11.mdx @@ -0,0 +1,229 @@ +--- +title: Zero 1.11 +description: Cached Results and Sturdier Client Storage +--- + +## Installation + +```bash +npm install @rocicorp/zero@1.11 +``` + + + [Deploy `zero-cache` before updating + clients](/docs/self-host#rolling-updates). Zero 1.11 + clients cannot connect to an older server. To roll back, + roll back clients first. In multi-node deployments, + upgrade the replication-manager before the view-syncers, + and roll back the view-syncers first. + + +## Overview + +Zero 1.11 shows cached query results sooner, recovers from client storage failures, and makes Litestream v5 the default for backups. It also speeds up queries on the client and server and fixes a long list of correctness and reliability issues. + +## Features + +- Queries whose results a previous session stored locally now start with the new [`'cached'` result type](/docs/queries#cached-results), so apps can show stored data right away and still tell it apart from data confirmed by the server. ([#6578](https://github.com/rocicorp/mono/pull/6578)) (thanks [@pawarren](https://github.com/pawarren)!) +- Building the same query again usually returns the same `Query` object, so `useQuery` no longer rebuilds and rehashes its query on every render, and queries built inline can be used as [React dependencies](/docs/react#query-identity). ([#6469](https://github.com/rocicorp/mono/pull/6469), [#6496](https://github.com/rocicorp/mono/pull/6496)) (thanks [@charpeni](https://github.com/charpeni)!) +- Zero now starts in a new [`'initializing'` connection state](/docs/connection#initializing) while it loads local data. Slow local loading no longer counts against the connect timeout. ([#6561](https://github.com/rocicorp/mono/pull/6561)) +- When local storage fails to open, fills up, or reports I/O errors, Zero reports it once through the new [`onStorageFailure`](/docs/connection#storage-failures) option and keeps running in memory. `zero.kvStore` reports which store is in use. ([#6585](https://github.com/rocicorp/mono/pull/6585), [#6657](https://github.com/rocicorp/mono/pull/6657), [#6686](https://github.com/rocicorp/mono/pull/6686)) (thanks [@pawarren](https://github.com/pawarren) and [@Karavil](https://github.com/Karavil)!) +- Upgrading from 1.10 keeps each client's local data and its pending mutations. Future protocol changes no longer start clients over with an empty local database. ([d597c69e8](https://github.com/rocicorp/mono/commit/d597c69e8), [28e581483](https://github.com/rocicorp/mono/commit/28e581483)) +- Server mutators can [return a JSON value](/docs/mutators#returning-data) to the client, [run again](/docs/mutators#retrying-mutators) in a new transaction when they fail with an error that `tx.retryOn()` accepts, and run at a chosen [transaction isolation level](/docs/server-zql#isolation-level). ([7756eb9bb](https://github.com/rocicorp/mono/commit/7756eb9bb), [4fe91c0d8](https://github.com/rocicorp/mono/commit/4fe91c0d8), [cb2821f3a](https://github.com/rocicorp/mono/commit/cb2821f3a)) (thanks [@pawarren](https://github.com/pawarren)!) +- [Litestream v5 backups](/docs/zero-cache-config#litestream-backup-using-v5) are now the default. Replicas restored from another replica's backup continue it incrementally, periodic snapshots keep backups compact, and the replica's WAL stays bounded. ([fb536f686](https://github.com/rocicorp/mono/commit/fb536f686), [c1a586810](https://github.com/rocicorp/mono/commit/c1a586810), [#6476](https://github.com/rocicorp/mono/pull/6476), [#6539](https://github.com/rocicorp/mono/pull/6539), [#6717](https://github.com/rocicorp/mono/pull/6717)) +- A `zero-cache` that comes online now [waits until replication has caught up](/docs/zero-cache-config#replication-lag-initial-readiness-threshold-ms) before reporting ready. ([1c5ddcdb4](https://github.com/rocicorp/mono/commit/1c5ddcdb4)) +- Inactive client groups are now kept for [7 days instead of 48 hours](/docs/zero-cache-config#cvr-garbage-collection-inactivity-threshold-hours), so clients that return within a week avoid a full resync. ([#6484](https://github.com/rocicorp/mono/pull/6484)) +- New tools for diagnosing slow queries: `zero-cache` logs which queries are slow and why, with index suggestions, and periodically logs per-query statistics. A new [operator password](/docs/zero-cache-config#operator-password) gives access to CPU profiles (`/profz`) and replica planner statistics (`/plannerz`) without the admin password. ([#6446](https://github.com/rocicorp/mono/pull/6446), [#6639](https://github.com/rocicorp/mono/pull/6639), [#6673](https://github.com/rocicorp/mono/pull/6673), [#6675](https://github.com/rocicorp/mono/pull/6675), [#6699](https://github.com/rocicorp/mono/pull/6699)) +- Opt-in limits protect `zero-cache` from expensive queries: a [hydration budget](/docs/zero-cache-config#view-syncer-hydration-budget-ms) for reconnecting clients and a [per-query hydration timeout](/docs/zero-cache-config#view-syncer-query-hydration-timeout-ms). ([#6477](https://github.com/rocicorp/mono/pull/6477), [#6499](https://github.com/rocicorp/mono/pull/6499)) +- New [OpenTelemetry metrics](/docs/otel) for WAL usage, index-build progress in replication status events, and restore progress in a new view-syncer status event. ([#6591](https://github.com/rocicorp/mono/pull/6591), [#6621](https://github.com/rocicorp/mono/pull/6621), [#6714](https://github.com/rocicorp/mono/pull/6714)) +- Server mutations can send their logs to your own `LogSink`. ([#6677](https://github.com/rocicorp/mono/pull/6677)) (thanks [@pawarren](https://github.com/pawarren)!) +- The `zero` Docker image shrank from about 1.09 GB to 807 MB. ([#6695](https://github.com/rocicorp/mono/pull/6695)) + +## Performance + +### Server-Side Incremental Updates + +When a transaction replicates, `zero-cache` brings every client group's queries up to date with it. Zero 1.11 does this about **2.5x faster**: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU. + + + +### Client Cold Start + +Apps usually register their queries before Zero has loaded local data, for example when `useQuery` runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once ([#6472](https://github.com/rocicorp/mono/pull/6472)). With 13 queries and about 30,000 local rows, startup is about **7x faster**. + + + +### Building and Hashing Queries + +Components build their queries on every render, and Zero hashes each query to identify it. Building the same query now returns the same object with its hash already computed ([#6469](https://github.com/rocicorp/mono/pull/6469)), so building and hashing a query is about **7x to 17x faster**, depending on its size. + + + +{/* TODO: React Native benchmark results. */} + +### Slower in Zero 1.11: Client Writes With Many Joined Queries + +One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about **1.2x longer** in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped ([#6645](https://github.com/rocicorp/mono/pull/6645), [#6708](https://github.com/rocicorp/mono/pull/6708)). When every change reaches some parent, as here, keeping that index up to date is extra work. + + + +## Fixes + +### Query Results + +- Queries using `or` with a flipped `exists` no longer return wrong rows. ([#6636](https://github.com/rocicorp/mono/pull/6636)) +- Joins no longer leave a deleted row on clients, keep a stale row in a `limit()` query, or fail with `node does not exist`. ([#6660](https://github.com/rocicorp/mono/pull/6660)) +- Rows behind an `exists` inside a [scalar subquery](/docs/zql#scalar-subqueries) are now synced, and changes to them propagate. ([913c0665f](https://github.com/rocicorp/mono/commit/913c0665f)) +- Catching up a client after a large update no longer fails with `Missing row`, and a schema change during rehydration no longer fails with `table has no column named`. ([#6662](https://github.com/rocicorp/mono/pull/6662), [#6616](https://github.com/rocicorp/mono/pull/6616)) +- Queries whose SQLite plan nests one multi-index `OR` inside another no longer crash `zero-cache` in a loop. ([6ea17bfcd](https://github.com/rocicorp/mono/commit/6ea17bfcd)) (thanks [@robertoborgone](https://github.com/robertoborgone)!) +- `zero-cache` no longer calls [`ZERO_QUERY_URL`](/docs/queries#custom-query-url) again for unchanged custom queries when it rebuilds its query pipelines. Permission changes still re-transform every query. ([#6601](https://github.com/rocicorp/mono/pull/6601)) +- In React and SolidJS, two custom queries with the same query but different names or arguments no longer share one view. A SolidJS view rebuilt after its query changes uses the current TTL. ([#6461](https://github.com/rocicorp/mono/pull/6461), [#6497](https://github.com/rocicorp/mono/pull/6497)) +- In mutators, a row just inserted without its optional columns now reads back with those columns as `null`. ([d4816752c](https://github.com/rocicorp/mono/commit/d4816752c)) + +### Clients and React Native + +- SQLite-backed stores (expo-sqlite, op-sqlite) no longer return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. ([#6495](https://github.com/rocicorp/mono/pull/6495), [#6655](https://github.com/rocicorp/mono/pull/6655), [#6683](https://github.com/rocicorp/mono/pull/6683)) (thanks [@pawarren](https://github.com/pawarren)!) +- Healthy clients no longer log `ChunkNotFoundError` during persist, and a write that would corrupt the local store now fails instead. ([#6670](https://github.com/rocicorp/mono/pull/6670), [#6559](https://github.com/rocicorp/mono/pull/6559)) (thanks [@pawarren](https://github.com/pawarren)!) +- With op-sqlite and a custom `location` or `encryptionKey`, dropping the database now deletes the right file. ([#6656](https://github.com/rocicorp/mono/pull/6656)) (thanks [@pawarren](https://github.com/pawarren)!) +- Clients no longer go offline after the app resumes from the background or the computer wakes from sleep, and React Native connect timeouts retry instead of leaving the client stuck in an error state. ([#6510](https://github.com/rocicorp/mono/pull/6510), [#6485](https://github.com/rocicorp/mono/pull/6485)) (thanks [@pawarren](https://github.com/pawarren)!) +- Slow local startup no longer makes a healthy server look unreachable. ([#6475](https://github.com/rocicorp/mono/pull/6475)) +- A mutator that throws while being replayed no longer disconnects the client in a loop. The mutation is still sent to the server. ([#6555](https://github.com/rocicorp/mono/pull/6555)) +- Client memory no longer grows with every query used in a session. ([#6474](https://github.com/rocicorp/mono/pull/6474)) +- `useSuspenseQuery` with an options object and no `suspendUntil` now suspends until the first rows arrive, as documented (see Breaking Changes). ([ffe67b23a](https://github.com/rocicorp/mono/commit/ffe67b23a)) (thanks [@charpeni](https://github.com/charpeni)!) +- Routine reconnects, auth errors and server rebalancing are logged as warnings or info instead of errors, and fatal errors now include their cause. ([#6581](https://github.com/rocicorp/mono/pull/6581), [#6692](https://github.com/rocicorp/mono/pull/6692), [#6498](https://github.com/rocicorp/mono/pull/6498), [#6682](https://github.com/rocicorp/mono/pull/6682), [#6691](https://github.com/rocicorp/mono/pull/6691), [42316d521](https://github.com/rocicorp/mono/commit/42316d521), [075b4215e](https://github.com/rocicorp/mono/commit/075b4215e), [79adc09fb](https://github.com/rocicorp/mono/commit/79adc09fb)) (thanks [@pawarren](https://github.com/pawarren)!) + +### zero-cache Reliability + +- Fixed memory and resource leaks in `zero-cache` around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. ([#6500](https://github.com/rocicorp/mono/pull/6500), [#6514](https://github.com/rocicorp/mono/pull/6514), [#6515](https://github.com/rocicorp/mono/pull/6515), [#6516](https://github.com/rocicorp/mono/pull/6516), [#6525](https://github.com/rocicorp/mono/pull/6525), [#6526](https://github.com/rocicorp/mono/pull/6526), [#6527](https://github.com/rocicorp/mono/pull/6527), [#6528](https://github.com/rocicorp/mono/pull/6528), [#6540](https://github.com/rocicorp/mono/pull/6540), [#6541](https://github.com/rocicorp/mono/pull/6541), [#6542](https://github.com/rocicorp/mono/pull/6542), [#6698](https://github.com/rocicorp/mono/pull/6698)) +- Fixed view-syncer crashes and `Internal error`s when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. ([#6481](https://github.com/rocicorp/mono/pull/6481), [#6530](https://github.com/rocicorp/mono/pull/6530), [#6600](https://github.com/rocicorp/mono/pull/6600), [#6614](https://github.com/rocicorp/mono/pull/6614), [#6631](https://github.com/rocicorp/mono/pull/6631), [d545bc7e2](https://github.com/rocicorp/mono/commit/d545bc7e2)) (thanks [@jdnvn](https://github.com/jdnvn)!) +- Servers using `zeroNodePg` no longer crash when Postgres closes a pooled connection. ([1b220fa07](https://github.com/rocicorp/mono/commit/1b220fa07)) (thanks [@pawarren](https://github.com/pawarren)!) +- Analyzing a query in the [Inspector](/docs/debug/inspector) no longer runs a view-syncer out of memory on large tables. ([#6596](https://github.com/rocicorp/mono/pull/6596)) +- Error messages no longer include row values, query literals or mutation arguments. ([#6413](https://github.com/rocicorp/mono/pull/6413)) + +### Replication, Backfills, and Backups + +- Replication no longer rolls back or stops after an interrupted stream, and a replication-manager whose slot was invalidated now resyncs instead of crash-looping. ([#6562](https://github.com/rocicorp/mono/pull/6562), [#6590](https://github.com/rocicorp/mono/pull/6590)) +- Backfills no longer apply rows before replication has caught up to them, resume when a view-syncer moves to another replication-manager, and no longer leak a Postgres connection that holds back vacuum. ([#6685](https://github.com/rocicorp/mono/pull/6685), [#6684](https://github.com/rocicorp/mono/pull/6684), [#6513](https://github.com/rocicorp/mono/pull/6513)) +- Replication slots are more robust: creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. ([25a9ea1ac](https://github.com/rocicorp/mono/commit/25a9ea1ac), [f3bba7d73](https://github.com/rocicorp/mono/commit/f3bba7d73), [7a998b566](https://github.com/rocicorp/mono/commit/7a998b566), [caf79b161](https://github.com/rocicorp/mono/commit/caf79b161), [#6568](https://github.com/rocicorp/mono/pull/6568)) +- Replication lag is now measured correctly on Aurora. ([e6a2590db](https://github.com/rocicorp/mono/commit/e6a2590db)) +- After initial sync, the replication-manager waits for a backup that covers its replica before serving, so restoring view-syncers no longer fall back to Postgres. ([#6465](https://github.com/rocicorp/mono/pull/6465)) +- Fewer unnecessary query pipeline resets, and dropping an upstream index now refreshes table schemas. ([4341cdcbd](https://github.com/rocicorp/mono/commit/4341cdcbd), [89787f827](https://github.com/rocicorp/mono/commit/89787f827), [bb71cc931](https://github.com/rocicorp/mono/commit/bb71cc931)) +- View-syncers fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. ([0af1a7109](https://github.com/rocicorp/mono/commit/0af1a7109), [#6598](https://github.com/rocicorp/mono/pull/6598)) +- During a resync, the old replication-manager keeps serving until the new one takes over. ([21f349135](https://github.com/rocicorp/mono/commit/21f349135)) +- Change-database connections no longer leak after a failed catch-up, and a change-database reset no longer hangs. ([#6529](https://github.com/rocicorp/mono/pull/6529), [#6632](https://github.com/rocicorp/mono/pull/6632)) +- Litestream's upload concurrency now scales with available memory, avoiding out-of-memory crashes in small containers. ([240c05730](https://github.com/rocicorp/mono/commit/240c05730)) +- Configuration values that start with a dash, such as some shard IDs, are now parsed correctly. ([81e6ac570](https://github.com/rocicorp/mono/commit/81e6ac570)) +- In the `zero` Docker image, initial sync and backfills no longer leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. ([3f139f52a](https://github.com/rocicorp/mono/commit/3f139f52a), [22fa3c27e](https://github.com/rocicorp/mono/commit/22fa3c27e)) + +### Type Checking + +- A rejected `whereExists(..., {scalar: true})` now explains why, and a misspelled relationship name lists the valid ones. ([cc020b368](https://github.com/rocicorp/mono/commit/cc020b368), [12a36dcfe](https://github.com/rocicorp/mono/commit/12a36dcfe)) + +## Breaking Changes + +- **Legacy queries:** `zero-cache` no longer accepts queries sent directly by clients. If you still use legacy queries, set [`ZERO_ALLOW_LEGACY_QUERIES=true`](/docs/zero-cache-config#allow-legacy-queries) or move to [custom queries](/docs/queries). ([#6399](https://github.com/rocicorp/mono/pull/6399)) +- **New states:** code that switches over every result type or connection state must handle `'cached'` and `'initializing'`. Treat `'cached'` like `'unknown'`: the data may be out of date. ([#6578](https://github.com/rocicorp/mono/pull/6578), [#6561](https://github.com/rocicorp/mono/pull/6561)) +- **Litestream v5 by default:** remove the `ZERO_LITESTREAM_VFS_*` options, which no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with `/` and add a lifecycle policy to your bucket. To keep v4 backups, set `ZERO_LITESTREAM_BACKUP_USING_V5=false`. If you set `ZERO_LITESTREAM_EXECUTABLE_V5`, use a Litestream build with `-fork-to-url`. ([fb536f686](https://github.com/rocicorp/mono/commit/fb536f686), [c1a586810](https://github.com/rocicorp/mono/commit/c1a586810)) +- **Changed defaults:** inactive client groups are kept for 168 hours instead of 48, replication lag is reported every 10 seconds instead of 30, and `ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION` is 4 instead of 2. Startup can take longer while `zero-cache` waits for a backup and for replication to catch up; give readiness probes more time. ([#6484](https://github.com/rocicorp/mono/pull/6484), [1c5ddcdb4](https://github.com/rocicorp/mono/commit/1c5ddcdb4), [#6521](https://github.com/rocicorp/mono/pull/6521), [#6465](https://github.com/rocicorp/mono/pull/6465)) +- **Server mutators:** mutator return values must be JSON. A custom `Database` must pass `addRetryPredicate` on to the transaction it creates for `tx.retryOn()` to work. Using `tx` after its transaction ended now throws. ([7756eb9bb](https://github.com/rocicorp/mono/commit/7756eb9bb), [4fe91c0d8](https://github.com/rocicorp/mono/commit/4fe91c0d8)) +- **`ZQLDatabase.run()`** no longer wraps reads in a transaction, so per-transaction settings such as `SET LOCAL` no longer apply to it. ([#6577](https://github.com/rocicorp/mono/pull/6577)) +- **`useSuspenseQuery`** with an options object and no `suspendUntil` now suspends until the first rows arrive, as documented. Pass `suspendUntil: 'complete'` for the old behavior. ([ffe67b23a](https://github.com/rocicorp/mono/commit/ffe67b23a)) +- **Upgrading from 1.9 or earlier:** clients start with a new local database, and mutations they had not yet sent are lost. Clients upgrading from 1.10 keep theirs. ([d597c69e8](https://github.com/rocicorp/mono/commit/d597c69e8), [28e581483](https://github.com/rocicorp/mono/commit/28e581483)) +- **Client storage:** a client whose local store is corrupt now deletes it and calls `onClientStateNotFound`. Mutations that were not yet sent are lost. ([#6558](https://github.com/rocicorp/mono/pull/6558)) +- **One-time query reload:** queries are identified differently in 1.11, so after an upgrade, and again after a rollback, `zero-cache` transforms and hydrates each client's queries once more. Expect a burst of requests to `ZERO_QUERY_URL`. ([#6450](https://github.com/rocicorp/mono/pull/6450), [#6462](https://github.com/rocicorp/mono/pull/6462)) +- **Deprecated:** `ZERO_CHANGE_STREAMER_MODE`; use [`ZERO_CHANGE_STREAMER_URI`](/docs/zero-cache-config#change-streamer-uri). ([698286b55](https://github.com/rocicorp/mono/commit/698286b55)) +- **Events:** a new `zero/events/status/view-syncer/v1` event type. Event consumers that reject unknown types must accept it. ([#6621](https://github.com/rocicorp/mono/pull/6621)) diff --git a/contents/docs/release-notes/index.mdx b/contents/docs/release-notes/index.mdx index cabe1ee9..b7ac9238 100644 --- a/contents/docs/release-notes/index.mdx +++ b/contents/docs/release-notes/index.mdx @@ -2,6 +2,7 @@ title: Release Notes --- +- [Zero 1.11: Cached Results and Sturdier Client Storage](/docs/release-notes/1.11) - [Zero 1.10: Unique Keys and More Reliable Replication](/docs/release-notes/1.10) - [Zero 1.9: Stability and Query Correctness](/docs/release-notes/1.9) - [Zero 1.8: Observability and Reliability](/docs/release-notes/1.8) From cd00c54746c617ebd1a12deed543cd7491ac6d07 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Wed, 14 Oct 2026 18:26:51 +0000 Subject: [PATCH 08/20] chore(search): regenerate the search index --- assets/search-index.json | 1906 ++++++++++++++++++++++---------------- 1 file changed, 1133 insertions(+), 773 deletions(-) diff --git a/assets/search-index.json b/assets/search-index.json index aba652ce..a8133e29 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -14,7 +14,7 @@ "kind": "page" }, { - "id": "78-agents#options", + "id": "79-agents#options", "title": "Agent Support", "searchTitle": "Options", "sectionTitle": "Options", @@ -126,7 +126,7 @@ "kind": "page" }, { - "id": "79-auth#set-userid-on-client", + "id": "80-auth#set-userid-on-client", "title": "Authentication", "searchTitle": "Set userID on Client", "sectionTitle": "Set userID on Client", @@ -136,7 +136,7 @@ "kind": "section" }, { - "id": "80-auth#define-the-context-type", + "id": "81-auth#define-the-context-type", "title": "Authentication", "searchTitle": "Define the Context Type", "sectionTitle": "Define the Context Type", @@ -146,7 +146,7 @@ "kind": "section" }, { - "id": "81-auth#send-credentials", + "id": "82-auth#send-credentials", "title": "Authentication", "searchTitle": "Send Credentials", "sectionTitle": "Send Credentials", @@ -156,7 +156,7 @@ "kind": "section" }, { - "id": "82-auth#cookies", + "id": "83-auth#cookies", "title": "Authentication", "searchTitle": "Cookies", "sectionTitle": "Cookies", @@ -166,7 +166,7 @@ "kind": "section" }, { - "id": "83-auth#cookie-deployment", + "id": "84-auth#cookie-deployment", "title": "Authentication", "searchTitle": "Cookie Deployment", "sectionTitle": "Cookie Deployment", @@ -176,7 +176,7 @@ "kind": "section" }, { - "id": "84-auth#tokens", + "id": "85-auth#tokens", "title": "Authentication", "searchTitle": "Tokens", "sectionTitle": "Tokens", @@ -186,7 +186,7 @@ "kind": "section" }, { - "id": "85-auth#implement-api-endpoints", + "id": "86-auth#implement-api-endpoints", "title": "Authentication", "searchTitle": "Implement API Endpoints", "sectionTitle": "Implement API Endpoints", @@ -196,7 +196,7 @@ "kind": "section" }, { - "id": "86-auth#query", + "id": "87-auth#query", "title": "Authentication", "searchTitle": "Query", "sectionTitle": "Query", @@ -206,7 +206,7 @@ "kind": "section" }, { - "id": "87-auth#mutate", + "id": "88-auth#mutate", "title": "Authentication", "searchTitle": "Mutate", "sectionTitle": "Mutate", @@ -216,7 +216,7 @@ "kind": "section" }, { - "id": "88-auth#updating-tokens", + "id": "89-auth#updating-tokens", "title": "Authentication", "searchTitle": "Updating Tokens", "sectionTitle": "Updating Tokens", @@ -226,7 +226,7 @@ "kind": "section" }, { - "id": "89-auth#auth-failure-and-refresh", + "id": "90-auth#auth-failure-and-refresh", "title": "Authentication", "searchTitle": "Auth Failure and Refresh", "sectionTitle": "Auth Failure and Refresh", @@ -236,7 +236,7 @@ "kind": "section" }, { - "id": "90-auth#permission-patterns", + "id": "91-auth#permission-patterns", "title": "Authentication", "searchTitle": "Permission Patterns", "sectionTitle": "Permission Patterns", @@ -246,7 +246,7 @@ "kind": "section" }, { - "id": "91-auth#read-permissions", + "id": "92-auth#read-permissions", "title": "Authentication", "searchTitle": "Read Permissions", "sectionTitle": "Read Permissions", @@ -256,7 +256,7 @@ "kind": "section" }, { - "id": "92-auth#only-owned-rows", + "id": "93-auth#only-owned-rows", "title": "Authentication", "searchTitle": "Only Owned Rows", "sectionTitle": "Only Owned Rows", @@ -266,7 +266,7 @@ "kind": "section" }, { - "id": "93-auth#owned-or-shared-rows", + "id": "94-auth#owned-or-shared-rows", "title": "Authentication", "searchTitle": "Owned or Shared Rows", "sectionTitle": "Owned or Shared Rows", @@ -276,7 +276,7 @@ "kind": "section" }, { - "id": "94-auth#owned-rows-or-all-if-admin", + "id": "95-auth#owned-rows-or-all-if-admin", "title": "Authentication", "searchTitle": "Owned Rows or All if Admin", "sectionTitle": "Owned Rows or All if Admin", @@ -286,7 +286,7 @@ "kind": "section" }, { - "id": "95-auth#deny-by-returning-no-rows", + "id": "96-auth#deny-by-returning-no-rows", "title": "Authentication", "searchTitle": "Deny by Returning No Rows", "sectionTitle": "Deny by Returning No Rows", @@ -296,7 +296,7 @@ "kind": "section" }, { - "id": "96-auth#write-permissions", + "id": "97-auth#write-permissions", "title": "Authentication", "searchTitle": "Write Permissions", "sectionTitle": "Write Permissions", @@ -306,7 +306,7 @@ "kind": "section" }, { - "id": "97-auth#enforce-ownership", + "id": "98-auth#enforce-ownership", "title": "Authentication", "searchTitle": "Enforce Ownership", "sectionTitle": "Enforce Ownership", @@ -316,7 +316,7 @@ "kind": "section" }, { - "id": "98-auth#edit-owned-rows", + "id": "99-auth#edit-owned-rows", "title": "Authentication", "searchTitle": "Edit Owned Rows", "sectionTitle": "Edit Owned Rows", @@ -326,7 +326,7 @@ "kind": "section" }, { - "id": "99-auth#edit-owned-or-shared-rows", + "id": "100-auth#edit-owned-or-shared-rows", "title": "Authentication", "searchTitle": "Edit Owned or Shared Rows", "sectionTitle": "Edit Owned or Shared Rows", @@ -336,7 +336,7 @@ "kind": "section" }, { - "id": "100-auth#edit-owned-or-all-if-admin", + "id": "101-auth#edit-owned-or-all-if-admin", "title": "Authentication", "searchTitle": "Edit Owned or All if Admin", "sectionTitle": "Edit Owned or All if Admin", @@ -346,7 +346,7 @@ "kind": "section" }, { - "id": "101-auth#logging-out", + "id": "102-auth#logging-out", "title": "Authentication", "searchTitle": "Logging Out", "sectionTitle": "Logging Out", @@ -383,7 +383,7 @@ "kind": "page" }, { - "id": "102-community#ui-frameworks", + "id": "103-community#ui-frameworks", "title": "From the Community", "searchTitle": "UI Frameworks", "sectionTitle": "UI Frameworks", @@ -393,7 +393,7 @@ "kind": "section" }, { - "id": "103-community#miscellaneous", + "id": "104-community#miscellaneous", "title": "From the Community", "searchTitle": "Miscellaneous", "sectionTitle": "Miscellaneous", @@ -517,7 +517,7 @@ "kind": "page" }, { - "id": "104-connecting-to-postgres#event-triggers", + "id": "105-connecting-to-postgres#event-triggers", "title": "Connecting to Postgres", "searchTitle": "Event Triggers", "sectionTitle": "Event Triggers", @@ -527,7 +527,7 @@ "kind": "section" }, { - "id": "105-connecting-to-postgres#configuration", + "id": "106-connecting-to-postgres#configuration", "title": "Connecting to Postgres", "searchTitle": "Configuration", "sectionTitle": "Configuration", @@ -537,7 +537,7 @@ "kind": "section" }, { - "id": "106-connecting-to-postgres#wal-level", + "id": "107-connecting-to-postgres#wal-level", "title": "Connecting to Postgres", "searchTitle": "WAL Level", "sectionTitle": "WAL Level", @@ -547,7 +547,7 @@ "kind": "section" }, { - "id": "107-connecting-to-postgres#change-database-stall-detection", + "id": "108-connecting-to-postgres#change-database-stall-detection", "title": "Connecting to Postgres", "searchTitle": "Change Database Stall Detection", "sectionTitle": "Change Database Stall Detection", @@ -557,7 +557,7 @@ "kind": "section" }, { - "id": "108-connecting-to-postgres#wal-sender-timeout", + "id": "109-connecting-to-postgres#wal-sender-timeout", "title": "Connecting to Postgres", "searchTitle": "WAL Sender Timeout", "sectionTitle": "WAL Sender Timeout", @@ -567,7 +567,7 @@ "kind": "section" }, { - "id": "109-connecting-to-postgres#bounding-wal-size", + "id": "110-connecting-to-postgres#bounding-wal-size", "title": "Connecting to Postgres", "searchTitle": "Bounding WAL Size", "sectionTitle": "Bounding WAL Size", @@ -577,7 +577,7 @@ "kind": "section" }, { - "id": "110-connecting-to-postgres#provider-specific-notes", + "id": "111-connecting-to-postgres#provider-specific-notes", "title": "Connecting to Postgres", "searchTitle": "Provider-Specific Notes", "sectionTitle": "Provider-Specific Notes", @@ -587,7 +587,7 @@ "kind": "section" }, { - "id": "111-connecting-to-postgres#planetscale-for-postgres", + "id": "112-connecting-to-postgres#planetscale-for-postgres", "title": "Connecting to Postgres", "searchTitle": "PlanetScale for Postgres", "sectionTitle": "PlanetScale for Postgres", @@ -597,7 +597,7 @@ "kind": "section" }, { - "id": "112-connecting-to-postgres#roles", + "id": "113-connecting-to-postgres#roles", "title": "Connecting to Postgres", "searchTitle": "Roles", "sectionTitle": "Roles", @@ -607,7 +607,7 @@ "kind": "section" }, { - "id": "113-connecting-to-postgres#connection-limits", + "id": "114-connecting-to-postgres#connection-limits", "title": "Connecting to Postgres", "searchTitle": "Connection Limits", "sectionTitle": "Connection Limits", @@ -617,7 +617,7 @@ "kind": "section" }, { - "id": "114-connecting-to-postgres#pooling", + "id": "115-connecting-to-postgres#pooling", "title": "Connecting to Postgres", "searchTitle": "Pooling", "sectionTitle": "Pooling", @@ -627,7 +627,7 @@ "kind": "section" }, { - "id": "115-connecting-to-postgres#high-availability", + "id": "116-connecting-to-postgres#high-availability", "title": "Connecting to Postgres", "searchTitle": "High Availability", "sectionTitle": "High Availability", @@ -637,7 +637,7 @@ "kind": "section" }, { - "id": "116-connecting-to-postgres#neon", + "id": "117-connecting-to-postgres#neon", "title": "Connecting to Postgres", "searchTitle": "Neon", "sectionTitle": "Neon", @@ -647,7 +647,7 @@ "kind": "section" }, { - "id": "117-connecting-to-postgres#logical-replication", + "id": "118-connecting-to-postgres#logical-replication", "title": "Connecting to Postgres", "searchTitle": "Logical Replication", "sectionTitle": "Logical Replication", @@ -657,7 +657,7 @@ "kind": "section" }, { - "id": "118-connecting-to-postgres#branching", + "id": "119-connecting-to-postgres#branching", "title": "Connecting to Postgres", "searchTitle": "Branching", "sectionTitle": "Branching", @@ -667,7 +667,7 @@ "kind": "section" }, { - "id": "119-connecting-to-postgres#flyio", + "id": "120-connecting-to-postgres#flyio", "title": "Connecting to Postgres", "searchTitle": "Fly.io", "sectionTitle": "Fly.io", @@ -677,7 +677,7 @@ "kind": "section" }, { - "id": "120-connecting-to-postgres#networking", + "id": "121-connecting-to-postgres#networking", "title": "Connecting to Postgres", "searchTitle": "Networking", "sectionTitle": "Networking", @@ -687,7 +687,7 @@ "kind": "section" }, { - "id": "121-connecting-to-postgres#permissions", + "id": "122-connecting-to-postgres#permissions", "title": "Connecting to Postgres", "searchTitle": "Permissions", "sectionTitle": "Permissions", @@ -697,7 +697,7 @@ "kind": "section" }, { - "id": "122-connecting-to-postgres#pooling-1", + "id": "123-connecting-to-postgres#pooling-1", "title": "Connecting to Postgres", "searchTitle": "Pooling", "sectionTitle": "Pooling", @@ -707,7 +707,7 @@ "kind": "section" }, { - "id": "123-connecting-to-postgres#supabase", + "id": "124-connecting-to-postgres#supabase", "title": "Connecting to Postgres", "searchTitle": "Supabase", "sectionTitle": "Supabase", @@ -717,7 +717,7 @@ "kind": "section" }, { - "id": "124-connecting-to-postgres#publication-changes", + "id": "125-connecting-to-postgres#publication-changes", "title": "Connecting to Postgres", "searchTitle": "Publication Changes", "sectionTitle": "Publication Changes", @@ -727,7 +727,7 @@ "kind": "section" }, { - "id": "125-connecting-to-postgres#ipv4", + "id": "126-connecting-to-postgres#ipv4", "title": "Connecting to Postgres", "searchTitle": "IPv4", "sectionTitle": "IPv4", @@ -737,7 +737,7 @@ "kind": "section" }, { - "id": "126-connecting-to-postgres#high-availability-1", + "id": "127-connecting-to-postgres#high-availability-1", "title": "Connecting to Postgres", "searchTitle": "High Availability", "sectionTitle": "High Availability", @@ -747,7 +747,7 @@ "kind": "section" }, { - "id": "127-connecting-to-postgres#render", + "id": "128-connecting-to-postgres#render", "title": "Connecting to Postgres", "searchTitle": "Render", "sectionTitle": "Render", @@ -757,7 +757,7 @@ "kind": "section" }, { - "id": "128-connecting-to-postgres#google-cloud-sql", + "id": "129-connecting-to-postgres#google-cloud-sql", "title": "Connecting to Postgres", "searchTitle": "Google Cloud SQL", "sectionTitle": "Google Cloud SQL", @@ -767,7 +767,7 @@ "kind": "section" }, { - "id": "129-connecting-to-postgres#schema-change-hooks", + "id": "130-connecting-to-postgres#schema-change-hooks", "title": "Connecting to Postgres", "searchTitle": "Schema Change Hooks", "sectionTitle": "Schema Change Hooks", @@ -781,7 +781,7 @@ "title": "Connection Status", "searchTitle": "Connection Status", "url": "/docs/connection", - "content": "Overview Zero manages a persistent connection to zero-cache with the following lifecycle: Usage The current connection state is available in the zero.connection.state property. This is subscribable and also has reactive hooks for React and SolidJS: import {useConnectionState} from '@rocicorp/zero/react' function ConnectionStatus() { const state = useConnectionState() switch (state.name) { case 'connecting': return
Connecting...
case 'connected': return
Connected
case 'disconnected': return
Offline
case 'error': return
Error
case 'needs-auth': return
Session expired
default: return null } }import {useConnectionState} from '@rocicorp/zero/solid' function ConnectionStatus() { const state = useConnectionState() return (
Connecting...
Connected
Offline
Error
Session expired
) }zero.connection.state.subscribe(state => { switch (state.name) { case 'connecting': console.log(`Connecting... ${state.reason}`) break case 'connected': console.log('Connected') break case 'disconnected': console.log(`Disconnected ${state.reason}`) break case 'error': console.log(`Error ${state.reason}`) break case 'needs-auth': console.log('Session expired') break default: return null } }) Offline Zero does not support offline writes. When the client is in the disconnected, error, or needs-auth states, reads from synced data continue to work, but writes are rejected. Offline UI While Zero is in the disconnected, error, or needs-auth states, you should prevent the user from inputting data to your application to avoid data loss. Zero automates this as best it can by rejecting writes in these states. But there can still be cases where the user can lose work – for example by typing into a textarea that is only written to Zero when the user presses a button. The easiest way to implement this is with a modal overlay that covers the entire screen and tells the user to reconnect. However, you could also continue to let the user use the app read-only, and only disable inputs. Details Connecting Zero starts in the connecting state. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state. Why Zero Doesn't Support Offline Writes Supporting offline writes in collaborative applications is inherently difficult, and no sync engine or CRDT algorithm can automatically solve it for you. Despite what their marketing says 😉. Example Imagine two users are editing an article about cats. One goes offline and does a bunch of work on the article, while the other decides that the article should actually be about dogs and rewrites it. When the offline user reconnects, there is no way that any software algorithm can automatically resolve their conflict. One or the other of them is going to be upset. This is a trivial data model with a single field, and is already unsolvable. Real-world applications are much worse: Foreign keys and other constraints can pass while offline, but break when the user reconnects. Custom business logic and authorization rules can pass while offline, but break when the user reconnects. The application's schema can change while offline, and the user's data may not be processable by the new schema. Just take your own schema and ask yourself what should really happen if one user takes their device offline for a week and makes arbitrarily complex changes while other users are working online. Tradeoffs It is of course possible to create applications that support offline writes well (Git exists!). But it requires significant tradeoffs. For example, you could: Disallow destructive operations (i.e., users can create tasks while offline, but cannot edit or delete them). Support custom UX to allow users to fork and merge conflicts when they occur. Restrict offline writes to a single device. Accept potential user data loss. Zero's Position While we recognize that offline writes would be useful, the reality is that for most of the apps we want to support, the user is online the vast majority of the time and the cost to support offline is extremely high. There is simply more value in making the online experience great first, and that's where we're focused right now. We would like to revisit this in the future, but it's not a priority right now.", + "content": "Overview Zero manages a persistent connection to zero-cache with the following lifecycle: Before this lifecycle starts, Zero is in the initializing state while it loads local data. It then moves to connecting and never returns to initializing. Usage The current connection state is available in the zero.connection.state property. This is subscribable and also has reactive hooks for React and SolidJS: import {useConnectionState} from '@rocicorp/zero/react' function ConnectionStatus() { const state = useConnectionState() switch (state.name) { case 'initializing': return
Loading...
case 'connecting': return
Connecting...
case 'connected': return
Connected
case 'disconnected': return
Offline
case 'error': return
Error
case 'needs-auth': return
Session expired
default: return null } }import {useConnectionState} from '@rocicorp/zero/solid' function ConnectionStatus() { const state = useConnectionState() return (
Loading...
Connecting...
Connected
Offline
Error
Session expired
) }zero.connection.state.subscribe(state => { switch (state.name) { case 'initializing': console.log('Loading local data...') break case 'connecting': console.log(`Connecting... ${state.reason}`) break case 'connected': console.log('Connected') break case 'disconnected': console.log(`Disconnected ${state.reason}`) break case 'error': console.log(`Error ${state.reason}`) break case 'needs-auth': console.log('Session expired') break default: return null } }) Offline Zero does not support offline writes. When the client is in the disconnected, error, or needs-auth states, reads from synced data continue to work, but writes are rejected. Offline UI While Zero is in the disconnected, error, or needs-auth states, you should prevent the user from inputting data to your application to avoid data loss. Zero automates this as best it can by rejecting writes in these states. But there can still be cases where the user can lose work – for example by typing into a textarea that is only written to Zero when the user presses a button. The easiest way to implement this is with a modal overlay that covers the entire screen and tells the user to reconnect. However, you could also continue to let the user use the app read-only, and only disable inputs. Details Initializing Zero starts in the initializing state. While initializing, Zero loads its local data. It has not contacted zero-cache yet. How long this takes depends on the device and the amount of local data, not on the network. On a slow device with a lot of local data, it can take tens of seconds. Once the local data is loaded, Zero transitions to connecting. Zero is only initializing once, right after it is created. When Zero reconnects later, it goes straight to connecting. Show a loading indicator while initializing, not an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting, not when you create Zero. There is no timeout while initializing, and the time spent here does not count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing. To limit this, add your own timeout. Reads and writes are allowed while initializing. Queries return results once the local data is loaded. Writes are queued and are sent when Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state. Storage Failures Zero saves synced data on the device: in IndexedDB in the browser, or in SQLite on React Native. This storage can fail. For example, the device can run out of space, or the database can fail to open. When this happens, Zero calls onStorageFailure once with a StorageFailureError. Its kind says what failed: In the browser, Zero reports a storage failure only when IndexedDB fails to open: full if the browser says the disk is full, otherwise cannot-open. Zero keeps working after a storage failure: If storage fails while Zero is starting, Zero runs in memory for the rest of the session, as if you had set kvStore: 'mem'. Nothing is saved on the device. If storage fails later, Zero stops saving data on the device. Queries and mutations keep working on the data in memory, and mutations are still sent to zero-cache. A read that needs data Zero hasn't loaded into memory can still fail. Zero tries the device's storage again only when the app creates a new Zero instance, for example after a restart. By default, Zero only logs the failure. Use onStorageFailure to tell the user what to do: import type {ZeroOptions} from '@rocicorp/zero' const opts: ZeroOptions = { // ... onStorageFailure: failure => { // showBanner stands in for your app's own UI. if (failure.kind === 'full') { showBanner( 'Your device is out of space. Free up some space, then restart the app.' ) } else { showBanner( 'Data could not be saved on this device. Restart the app to try again.' ) } } } StorageFailureError is exported from @rocicorp/zero. Its cause is the original error from the browser or the SQLite driver. A custom kvStore can throw a StorageFailureError to get the same handling. Checking the Store zero.kvStore.kind tells you which storage Zero is using: 'idb', 'mem', 'expo-sqlite', or 'op-sqlite'. A custom store reports its own kind, if it sets one. if (zero.kvStore.kind === 'mem') { // Data is not saved on this device. } Without IndexedDB, kind is 'mem' from the start and onStorageFailure isn't called. When IndexedDB fails to open, kind changes to 'mem' only when Zero first reads or writes the store, so check kind when you need it, not once at startup. Why Zero Doesn't Support Offline Writes Supporting offline writes in collaborative applications is inherently difficult, and no sync engine or CRDT algorithm can automatically solve it for you. Despite what their marketing says 😉. Example Imagine two users are editing an article about cats. One goes offline and does a bunch of work on the article, while the other decides that the article should actually be about dogs and rewrites it. When the offline user reconnects, there is no way that any software algorithm can automatically resolve their conflict. One or the other of them is going to be upset. This is a trivial data model with a single field, and is already unsolvable. Real-world applications are much worse: Foreign keys and other constraints can pass while offline, but break when the user reconnects. Custom business logic and authorization rules can pass while offline, but break when the user reconnects. The application's schema can change while offline, and the user's data may not be processable by the new schema. Just take your own schema and ask yourself what should really happen if one user takes their device offline for a week and makes arbitrarily complex changes while other users are working online. Tradeoffs It is of course possible to create applications that support offline writes well (Git exists!). But it requires significant tradeoffs. For example, you could: Disallow destructive operations (i.e., users can create tasks while offline, but cannot edit or delete them). Support custom UX to allow users to fork and merge conflicts when they occur. Restrict offline writes to a single device. Accept potential user data loss. Zero's Position While we recognize that offline writes would be useful, the reality is that for most of the apps we want to support, the user is online the vast majority of the time and the cost to support offline is extremely high. There is simply more value in making the online experience great first, and that's where we're focused right now. We would like to revisit this in the future, but it's not a priority right now.", "headings": [ { "text": "Overview", @@ -803,6 +803,10 @@ "text": "Details", "id": "details" }, + { + "text": "Initializing", + "id": "initializing" + }, { "text": "Connecting", "id": "connecting" @@ -827,6 +831,14 @@ "text": "Closed", "id": "closed" }, + { + "text": "Storage Failures", + "id": "storage-failures" + }, + { + "text": "Checking the Store", + "id": "checking-the-store" + }, { "text": "Why Zero Doesn't Support Offline Writes", "id": "why-zero-doesnt-support-offline-writes" @@ -847,27 +859,27 @@ "kind": "page" }, { - "id": "130-connection#overview", + "id": "131-connection#overview", "title": "Connection Status", "searchTitle": "Overview", "sectionTitle": "Overview", "sectionId": "overview", "url": "/docs/connection", - "content": "Zero manages a persistent connection to zero-cache with the following lifecycle:", + "content": "Zero manages a persistent connection to zero-cache with the following lifecycle: Before this lifecycle starts, Zero is in the initializing state while it loads local data. It then moves to connecting and never returns to initializing.", "kind": "section" }, { - "id": "131-connection#usage", + "id": "132-connection#usage", "title": "Connection Status", "searchTitle": "Usage", "sectionTitle": "Usage", "sectionId": "usage", "url": "/docs/connection", - "content": "The current connection state is available in the zero.connection.state property. This is subscribable and also has reactive hooks for React and SolidJS: import {useConnectionState} from '@rocicorp/zero/react' function ConnectionStatus() { const state = useConnectionState() switch (state.name) { case 'connecting': return
Connecting...
case 'connected': return
Connected
case 'disconnected': return
Offline
case 'error': return
Error
case 'needs-auth': return
Session expired
default: return null } }import {useConnectionState} from '@rocicorp/zero/solid' function ConnectionStatus() { const state = useConnectionState() return (
Connecting...
Connected
Offline
Error
Session expired
) }zero.connection.state.subscribe(state => { switch (state.name) { case 'connecting': console.log(`Connecting... ${state.reason}`) break case 'connected': console.log('Connected') break case 'disconnected': console.log(`Disconnected ${state.reason}`) break case 'error': console.log(`Error ${state.reason}`) break case 'needs-auth': console.log('Session expired') break default: return null } })", + "content": "The current connection state is available in the zero.connection.state property. This is subscribable and also has reactive hooks for React and SolidJS: import {useConnectionState} from '@rocicorp/zero/react' function ConnectionStatus() { const state = useConnectionState() switch (state.name) { case 'initializing': return
Loading...
case 'connecting': return
Connecting...
case 'connected': return
Connected
case 'disconnected': return
Offline
case 'error': return
Error
case 'needs-auth': return
Session expired
default: return null } }import {useConnectionState} from '@rocicorp/zero/solid' function ConnectionStatus() { const state = useConnectionState() return (
Loading...
Connecting...
Connected
Offline
Error
Session expired
) }zero.connection.state.subscribe(state => { switch (state.name) { case 'initializing': console.log('Loading local data...') break case 'connecting': console.log(`Connecting... ${state.reason}`) break case 'connected': console.log('Connected') break case 'disconnected': console.log(`Disconnected ${state.reason}`) break case 'error': console.log(`Error ${state.reason}`) break case 'needs-auth': console.log('Session expired') break default: return null } })", "kind": "section" }, { - "id": "132-connection#offline", + "id": "133-connection#offline", "title": "Connection Status", "searchTitle": "Offline", "sectionTitle": "Offline", @@ -877,7 +889,7 @@ "kind": "section" }, { - "id": "133-connection#offline-ui", + "id": "134-connection#offline-ui", "title": "Connection Status", "searchTitle": "Offline UI", "sectionTitle": "Offline UI", @@ -887,27 +899,37 @@ "kind": "section" }, { - "id": "134-connection#details", + "id": "135-connection#details", "title": "Connection Status", "searchTitle": "Details", "sectionTitle": "Details", "sectionId": "details", "url": "/docs/connection", - "content": "Connecting Zero starts in the connecting state. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state.", + "content": "Initializing Zero starts in the initializing state. While initializing, Zero loads its local data. It has not contacted zero-cache yet. How long this takes depends on the device and the amount of local data, not on the network. On a slow device with a lot of local data, it can take tens of seconds. Once the local data is loaded, Zero transitions to connecting. Zero is only initializing once, right after it is created. When Zero reconnects later, it goes straight to connecting. Show a loading indicator while initializing, not an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting, not when you create Zero. There is no timeout while initializing, and the time spent here does not count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing. To limit this, add your own timeout. Reads and writes are allowed while initializing. Queries return results once the local data is loaded. Writes are queued and are sent when Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state.", + "kind": "section" + }, + { + "id": "136-connection#initializing", + "title": "Connection Status", + "searchTitle": "Initializing", + "sectionTitle": "Initializing", + "sectionId": "initializing", + "url": "/docs/connection", + "content": "Zero starts in the initializing state. While initializing, Zero loads its local data. It has not contacted zero-cache yet. How long this takes depends on the device and the amount of local data, not on the network. On a slow device with a lot of local data, it can take tens of seconds. Once the local data is loaded, Zero transitions to connecting. Zero is only initializing once, right after it is created. When Zero reconnects later, it goes straight to connecting. Show a loading indicator while initializing, not an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting, not when you create Zero. There is no timeout while initializing, and the time spent here does not count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing. To limit this, add your own timeout. Reads and writes are allowed while initializing. Queries return results once the local data is loaded. Writes are queued and are sent when Zero connects.", "kind": "section" }, { - "id": "135-connection#connecting", + "id": "137-connection#connecting", "title": "Connection Status", "searchTitle": "Connecting", "sectionTitle": "Connecting", "sectionId": "connecting", "url": "/docs/connection", - "content": "Zero starts in the connecting state. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information.", + "content": "Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information.", "kind": "section" }, { - "id": "136-connection#connected", + "id": "138-connection#connected", "title": "Connection Status", "searchTitle": "Connected", "sectionTitle": "Connected", @@ -917,7 +939,7 @@ "kind": "section" }, { - "id": "137-connection#disconnected", + "id": "139-connection#disconnected", "title": "Connection Status", "searchTitle": "Disconnected", "sectionTitle": "Disconnected", @@ -927,7 +949,7 @@ "kind": "section" }, { - "id": "138-connection#error", + "id": "140-connection#error", "title": "Connection Status", "searchTitle": "Error", "sectionTitle": "Error", @@ -937,7 +959,7 @@ "kind": "section" }, { - "id": "139-connection#needs-auth", + "id": "141-connection#needs-auth", "title": "Connection Status", "searchTitle": "Needs-Auth", "sectionTitle": "Needs-Auth", @@ -947,7 +969,7 @@ "kind": "section" }, { - "id": "140-connection#closed", + "id": "142-connection#closed", "title": "Connection Status", "searchTitle": "Closed", "sectionTitle": "Closed", @@ -957,7 +979,27 @@ "kind": "section" }, { - "id": "141-connection#why-zero-doesnt-support-offline-writes", + "id": "143-connection#storage-failures", + "title": "Connection Status", + "searchTitle": "Storage Failures", + "sectionTitle": "Storage Failures", + "sectionId": "storage-failures", + "url": "/docs/connection", + "content": "Zero saves synced data on the device: in IndexedDB in the browser, or in SQLite on React Native. This storage can fail. For example, the device can run out of space, or the database can fail to open. When this happens, Zero calls onStorageFailure once with a StorageFailureError. Its kind says what failed: In the browser, Zero reports a storage failure only when IndexedDB fails to open: full if the browser says the disk is full, otherwise cannot-open. Zero keeps working after a storage failure: If storage fails while Zero is starting, Zero runs in memory for the rest of the session, as if you had set kvStore: 'mem'. Nothing is saved on the device. If storage fails later, Zero stops saving data on the device. Queries and mutations keep working on the data in memory, and mutations are still sent to zero-cache. A read that needs data Zero hasn't loaded into memory can still fail. Zero tries the device's storage again only when the app creates a new Zero instance, for example after a restart. By default, Zero only logs the failure. Use onStorageFailure to tell the user what to do: import type {ZeroOptions} from '@rocicorp/zero' const opts: ZeroOptions = { // ... onStorageFailure: failure => { // showBanner stands in for your app's own UI. if (failure.kind === 'full') { showBanner( 'Your device is out of space. Free up some space, then restart the app.' ) } else { showBanner( 'Data could not be saved on this device. Restart the app to try again.' ) } } } StorageFailureError is exported from @rocicorp/zero. Its cause is the original error from the browser or the SQLite driver. A custom kvStore can throw a StorageFailureError to get the same handling. Checking the Store zero.kvStore.kind tells you which storage Zero is using: 'idb', 'mem', 'expo-sqlite', or 'op-sqlite'. A custom store reports its own kind, if it sets one. if (zero.kvStore.kind === 'mem') { // Data is not saved on this device. } Without IndexedDB, kind is 'mem' from the start and onStorageFailure isn't called. When IndexedDB fails to open, kind changes to 'mem' only when Zero first reads or writes the store, so check kind when you need it, not once at startup.", + "kind": "section" + }, + { + "id": "144-connection#checking-the-store", + "title": "Connection Status", + "searchTitle": "Checking the Store", + "sectionTitle": "Checking the Store", + "sectionId": "checking-the-store", + "url": "/docs/connection", + "content": "zero.kvStore.kind tells you which storage Zero is using: 'idb', 'mem', 'expo-sqlite', or 'op-sqlite'. A custom store reports its own kind, if it sets one. if (zero.kvStore.kind === 'mem') { // Data is not saved on this device. } Without IndexedDB, kind is 'mem' from the start and onStorageFailure isn't called. When IndexedDB fails to open, kind changes to 'mem' only when Zero first reads or writes the store, so check kind when you need it, not once at startup.", + "kind": "section" + }, + { + "id": "145-connection#why-zero-doesnt-support-offline-writes", "title": "Connection Status", "searchTitle": "Why Zero Doesn't Support Offline Writes", "sectionTitle": "Why Zero Doesn't Support Offline Writes", @@ -967,7 +1009,7 @@ "kind": "section" }, { - "id": "142-connection#example", + "id": "146-connection#example", "title": "Connection Status", "searchTitle": "Example", "sectionTitle": "Example", @@ -977,7 +1019,7 @@ "kind": "section" }, { - "id": "143-connection#tradeoffs", + "id": "147-connection#tradeoffs", "title": "Connection Status", "searchTitle": "Tradeoffs", "sectionTitle": "Tradeoffs", @@ -987,7 +1029,7 @@ "kind": "section" }, { - "id": "144-connection#zeros-position", + "id": "148-connection#zeros-position", "title": "Connection Status", "searchTitle": "Zero's Position", "sectionTitle": "Zero's Position", @@ -1035,7 +1077,7 @@ "kind": "page" }, { - "id": "145-debug/analyze-query-cli#set-up", + "id": "149-debug/analyze-query-cli#set-up", "title": "Analyze Query CLI", "searchTitle": "Set Up", "sectionTitle": "Set Up", @@ -1045,7 +1087,7 @@ "kind": "section" }, { - "id": "146-debug/analyze-query-cli#run-zql-queries", + "id": "150-debug/analyze-query-cli#run-zql-queries", "title": "Analyze Query CLI", "searchTitle": "Run ZQL Queries", "sectionTitle": "Run ZQL Queries", @@ -1055,7 +1097,7 @@ "kind": "section" }, { - "id": "147-debug/analyze-query-cli#production-use", + "id": "151-debug/analyze-query-cli#production-use", "title": "Analyze Query CLI", "searchTitle": "Production Use", "sectionTitle": "Production Use", @@ -1065,7 +1107,7 @@ "kind": "section" }, { - "id": "148-debug/analyze-query-cli#env-var-shorthand", + "id": "152-debug/analyze-query-cli#env-var-shorthand", "title": "Analyze Query CLI", "searchTitle": "Env Var Shorthand", "sectionTitle": "Env Var Shorthand", @@ -1075,7 +1117,7 @@ "kind": "section" }, { - "id": "149-debug/analyze-query-cli#other-input-modes", + "id": "153-debug/analyze-query-cli#other-input-modes", "title": "Analyze Query CLI", "searchTitle": "Other Input Modes", "sectionTitle": "Other Input Modes", @@ -1085,7 +1127,7 @@ "kind": "section" }, { - "id": "150-debug/analyze-query-cli#output", + "id": "154-debug/analyze-query-cli#output", "title": "Analyze Query CLI", "searchTitle": "Output", "sectionTitle": "Output", @@ -1095,7 +1137,7 @@ "kind": "section" }, { - "id": "151-debug/analyze-query-cli#optional-output", + "id": "155-debug/analyze-query-cli#optional-output", "title": "Analyze Query CLI", "searchTitle": "Optional Output", "sectionTitle": "Optional Output", @@ -1155,7 +1197,7 @@ "kind": "page" }, { - "id": "152-debug/inspector#accessing-the-inspector", + "id": "156-debug/inspector#accessing-the-inspector", "title": "Inspector", "searchTitle": "Accessing the Inspector", "sectionTitle": "Accessing the Inspector", @@ -1165,7 +1207,7 @@ "kind": "section" }, { - "id": "153-debug/inspector#clients-and-groups", + "id": "157-debug/inspector#clients-and-groups", "title": "Inspector", "searchTitle": "Clients and Groups", "sectionTitle": "Clients and Groups", @@ -1175,7 +1217,7 @@ "kind": "section" }, { - "id": "154-debug/inspector#queries", + "id": "158-debug/inspector#queries", "title": "Inspector", "searchTitle": "Queries", "sectionTitle": "Queries", @@ -1185,7 +1227,7 @@ "kind": "section" }, { - "id": "155-debug/inspector#analyzing-queries", + "id": "159-debug/inspector#analyzing-queries", "title": "Inspector", "searchTitle": "Analyzing Queries", "sectionTitle": "Analyzing Queries", @@ -1195,7 +1237,7 @@ "kind": "section" }, { - "id": "156-debug/inspector#interpreting-query-analysis", + "id": "160-debug/inspector#interpreting-query-analysis", "title": "Inspector", "searchTitle": "Interpreting Query Analysis", "sectionTitle": "Interpreting Query Analysis", @@ -1205,7 +1247,7 @@ "kind": "section" }, { - "id": "157-debug/inspector#viewing-sqlite-plans", + "id": "161-debug/inspector#viewing-sqlite-plans", "title": "Inspector", "searchTitle": "Viewing SQLite Plans", "sectionTitle": "Viewing SQLite Plans", @@ -1215,7 +1257,7 @@ "kind": "section" }, { - "id": "158-debug/inspector#viewing-zero-plans", + "id": "162-debug/inspector#viewing-zero-plans", "title": "Inspector", "searchTitle": "Viewing Zero Plans", "sectionTitle": "Viewing Zero Plans", @@ -1225,7 +1267,7 @@ "kind": "section" }, { - "id": "159-debug/inspector#analyzing-arbitrary-zql", + "id": "163-debug/inspector#analyzing-arbitrary-zql", "title": "Inspector", "searchTitle": "Analyzing Arbitrary ZQL", "sectionTitle": "Analyzing Arbitrary ZQL", @@ -1235,7 +1277,7 @@ "kind": "section" }, { - "id": "160-debug/inspector#table-data", + "id": "164-debug/inspector#table-data", "title": "Inspector", "searchTitle": "Table Data", "sectionTitle": "Table Data", @@ -1245,7 +1287,7 @@ "kind": "section" }, { - "id": "161-debug/inspector#server-version", + "id": "165-debug/inspector#server-version", "title": "Inspector", "searchTitle": "Server Version", "sectionTitle": "Server Version", @@ -1286,7 +1328,7 @@ "kind": "page" }, { - "id": "162-debug/replication#resetting", + "id": "166-debug/replication#resetting", "title": "Replication", "searchTitle": "Resetting", "sectionTitle": "Resetting", @@ -1296,7 +1338,7 @@ "kind": "section" }, { - "id": "163-debug/replication#inspecting", + "id": "167-debug/replication#inspecting", "title": "Replication", "searchTitle": "Inspecting", "sectionTitle": "Inspecting", @@ -1306,7 +1348,7 @@ "kind": "section" }, { - "id": "164-debug/replication#miscellaneous", + "id": "168-debug/replication#miscellaneous", "title": "Replication", "searchTitle": "Miscellaneous", "sectionTitle": "Miscellaneous", @@ -1320,7 +1362,7 @@ "title": "Slow Queries", "searchTitle": "Slow Queries", "url": "/docs/debug/slow-queries", - "content": "In the zero-cache logs, you may see statements indicating a query is slow: hash=3rhuw19xt9vry transformationHash=1nv7ot74gxfl7 Slow query materialization 325.46865100000286 Or, you may just notice queries taking longer than expected in the UI. Here are some tips to help debug such slow queries. Analyze Queries Use the inspector or analyze query CLI to analyze queries and get detailed information about the query plan and performance. Check ttl If you are seeing unexpected UI flicker when moving between views, it is possible that the queries backing these views have a ttl of never. Set the ttl to something like 5m to keep data cached across navigations. You may alternately want to preload some data at app startup. Conversely, if you are setting ttl to long values, then you may have many backgrounded queries running that the app is not using. You can see which queries are running using the inspector. Ensure that only expected queries are running. Locality If you see log lines like: flushed cvr ... (124ms) this indicates that zero-cache is likely deployed too far away from your CVR database. If you did not configure a CVR database URL then this will be your product's Postgres DB. A slow CVR flush can slow down Zero, since it must complete the flush before sending query result(s) to clients. Try moving zero-cache to be deployed as close as possible to the CVR database. Check Storage zero-cache is effectively a database. It requires fast (low latency and high bandwidth) disk access to perform well. If you're running on network attached storage with high latency, or on AWS with low IOPS, then this is the most likely culprit. Some hosting providers scale IOPS with vCPU. Increasing the vCPU will increase storage throughput and likely resolve the issue. Fly.io provides physically attached SSDs, even for their smallest VMs. Deploying zero-cache there (or any other provider that offers physically attached SSDs) is another option. /statz zero-cache makes some internal health statistics available via the /statz endpoint of zero-cache. In order to access this, you must configure an admin password.", + "content": "In the zero-cache logs, you may see statements indicating a query is slow: hash=3rhuw19xt9vry transformationHash=1nv7ot74gxfl7 Slow query materialization 325.46865100000286 Or, you may just notice queries taking longer than expected in the UI. Here are some tips to help debug such slow queries. Analyze Queries Use the inspector or analyze query CLI to analyze queries and get detailed information about the query plan and performance. Check ttl If you are seeing unexpected UI flicker when moving between views, it is possible that the queries backing these views have a ttl of never. Set the ttl to something like 5m to keep data cached across navigations. You may alternately want to preload some data at app startup. Conversely, if you are setting ttl to long values, then you may have many backgrounded queries running that the app is not using. You can see which queries are running using the inspector. Ensure that only expected queries are running. Locality If you see log lines like: flushed cvr ... (124ms) this indicates that zero-cache is likely deployed too far away from your CVR database. If you did not configure a CVR database URL then this will be your product's Postgres DB. A slow CVR flush can slow down Zero, since it must complete the flush before sending query result(s) to clients. Try moving zero-cache to be deployed as close as possible to the CVR database. Check Storage zero-cache is effectively a database. It requires fast (low latency and high bandwidth) disk access to perform well. If you're running on network attached storage with high latency, or on AWS with low IOPS, then this is the most likely culprit. Some hosting providers scale IOPS with vCPU. Increasing the vCPU will increase storage throughput and likely resolve the issue. Fly.io provides physically attached SSDs, even for their smallest VMs. Deploying zero-cache there (or any other provider that offers physically attached SSDs) is another option. /statz zero-cache makes some internal health statistics available via the /statz endpoint of zero-cache. In order to access this, you must configure an admin password or an operator password.", "headings": [ { "text": "Analyze Queries", @@ -1346,7 +1388,7 @@ "kind": "page" }, { - "id": "165-debug/slow-queries#analyze-queries", + "id": "169-debug/slow-queries#analyze-queries", "title": "Slow Queries", "searchTitle": "Analyze Queries", "sectionTitle": "Analyze Queries", @@ -1356,7 +1398,7 @@ "kind": "section" }, { - "id": "166-debug/slow-queries#check-ttl", + "id": "170-debug/slow-queries#check-ttl", "title": "Slow Queries", "searchTitle": "Check ttl", "sectionTitle": "Check ttl", @@ -1366,7 +1408,7 @@ "kind": "section" }, { - "id": "167-debug/slow-queries#locality", + "id": "171-debug/slow-queries#locality", "title": "Slow Queries", "searchTitle": "Locality", "sectionTitle": "Locality", @@ -1376,7 +1418,7 @@ "kind": "section" }, { - "id": "168-debug/slow-queries#check-storage", + "id": "172-debug/slow-queries#check-storage", "title": "Slow Queries", "searchTitle": "Check Storage", "sectionTitle": "Check Storage", @@ -1386,13 +1428,13 @@ "kind": "section" }, { - "id": "169-debug/slow-queries#statz", + "id": "173-debug/slow-queries#statz", "title": "Slow Queries", "searchTitle": "/statz", "sectionTitle": "/statz", "sectionId": "statz", "url": "/docs/debug/slow-queries", - "content": "zero-cache makes some internal health statistics available via the /statz endpoint of zero-cache. In order to access this, you must configure an admin password.", + "content": "zero-cache makes some internal health statistics available via the /statz endpoint of zero-cache. In order to access this, you must configure an admin password or an operator password.", "kind": "section" }, { @@ -1409,7 +1451,7 @@ "title": "Ad-Hoc Queries (Deprecated)", "searchTitle": "Ad-Hoc Queries (Deprecated)", "url": "/docs/deprecated/ad-hoc-queries", - "content": "It will be removed in a future release of Zero. Please move to the new queries API. Overview Zero generates a query API for every table you sync. To enable, set the enableLegacyQueries option to true in your schema: const schema = createSchema({ // ... enableLegacyQueries: true }) Once enabled, queries are available at z.query.. const zero = new Zero(...); const issues = await zero.query.issue.where('priority', 'high').run(); Each table is a ZQL builder object. See ZQL for details.", + "content": "It will be removed in a future release of Zero. Please move to the new queries API. Overview Zero generates a query API for every table you sync. To enable, set the enableLegacyQueries option to true in your schema: const schema = createSchema({ // ... enableLegacyQueries: true }) Ad-hoc queries send their query ASTs directly to zero-cache, which rejects them by default. To allow them, also set ZERO_ALLOW_LEGACY_QUERIES to true on zero-cache. Once enabled, queries are available at z.query.. const zero = new Zero(...); const issues = await zero.query.issue.where('priority', 'high').run(); Each table is a ZQL builder object. See ZQL for details.", "headings": [ { "text": "Overview", @@ -1419,13 +1461,13 @@ "kind": "page" }, { - "id": "170-deprecated/ad-hoc-queries#overview", + "id": "174-deprecated/ad-hoc-queries#overview", "title": "Ad-Hoc Queries (Deprecated)", "searchTitle": "Overview", "sectionTitle": "Overview", "sectionId": "overview", "url": "/docs/deprecated/ad-hoc-queries", - "content": "Zero generates a query API for every table you sync. To enable, set the enableLegacyQueries option to true in your schema: const schema = createSchema({ // ... enableLegacyQueries: true }) Once enabled, queries are available at z.query.. const zero = new Zero(...); const issues = await zero.query.issue.where('priority', 'high').run(); Each table is a ZQL builder object. See ZQL for details.", + "content": "Zero generates a query API for every table you sync. To enable, set the enableLegacyQueries option to true in your schema: const schema = createSchema({ // ... enableLegacyQueries: true }) Ad-hoc queries send their query ASTs directly to zero-cache, which rejects them by default. To allow them, also set ZERO_ALLOW_LEGACY_QUERIES to true on zero-cache. Once enabled, queries are available at z.query.. const zero = new Zero(...); const issues = await zero.query.issue.where('priority', 'high').run(); Each table is a ZQL builder object. See ZQL for details.", "kind": "section" }, { @@ -1443,7 +1485,7 @@ "kind": "page" }, { - "id": "171-deprecated/crud-mutators#overview", + "id": "175-deprecated/crud-mutators#overview", "title": "CRUD Mutators (Deprecated)", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -1515,7 +1557,7 @@ "kind": "page" }, { - "id": "172-deprecated/rls-permissions#define-permissions", + "id": "176-deprecated/rls-permissions#define-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Define Permissions", "sectionTitle": "Define Permissions", @@ -1525,7 +1567,7 @@ "kind": "section" }, { - "id": "173-deprecated/rls-permissions#access-is-denied-by-default", + "id": "177-deprecated/rls-permissions#access-is-denied-by-default", "title": "RLS Permissions (Deprecated)", "searchTitle": "Access is Denied by Default", "sectionTitle": "Access is Denied by Default", @@ -1535,7 +1577,7 @@ "kind": "section" }, { - "id": "174-deprecated/rls-permissions#permission-evaluation", + "id": "178-deprecated/rls-permissions#permission-evaluation", "title": "RLS Permissions (Deprecated)", "searchTitle": "Permission Evaluation", "sectionTitle": "Permission Evaluation", @@ -1545,7 +1587,7 @@ "kind": "section" }, { - "id": "175-deprecated/rls-permissions#permission-deployment", + "id": "179-deprecated/rls-permissions#permission-deployment", "title": "RLS Permissions (Deprecated)", "searchTitle": "Permission Deployment", "sectionTitle": "Permission Deployment", @@ -1555,7 +1597,7 @@ "kind": "section" }, { - "id": "176-deprecated/rls-permissions#rules", + "id": "180-deprecated/rls-permissions#rules", "title": "RLS Permissions (Deprecated)", "searchTitle": "Rules", "sectionTitle": "Rules", @@ -1565,7 +1607,7 @@ "kind": "section" }, { - "id": "177-deprecated/rls-permissions#select-permissions", + "id": "181-deprecated/rls-permissions#select-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Select Permissions", "sectionTitle": "Select Permissions", @@ -1575,7 +1617,7 @@ "kind": "section" }, { - "id": "178-deprecated/rls-permissions#insert-permissions", + "id": "182-deprecated/rls-permissions#insert-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Insert Permissions", "sectionTitle": "Insert Permissions", @@ -1585,7 +1627,7 @@ "kind": "section" }, { - "id": "179-deprecated/rls-permissions#update-permissions", + "id": "183-deprecated/rls-permissions#update-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Update Permissions", "sectionTitle": "Update Permissions", @@ -1595,7 +1637,7 @@ "kind": "section" }, { - "id": "180-deprecated/rls-permissions#delete-permissions", + "id": "184-deprecated/rls-permissions#delete-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Delete Permissions", "sectionTitle": "Delete Permissions", @@ -1605,7 +1647,7 @@ "kind": "section" }, { - "id": "181-deprecated/rls-permissions#permissions-based-on-auth-data", + "id": "185-deprecated/rls-permissions#permissions-based-on-auth-data", "title": "RLS Permissions (Deprecated)", "searchTitle": "Permissions Based on Auth Data", "sectionTitle": "Permissions Based on Auth Data", @@ -1615,7 +1657,7 @@ "kind": "section" }, { - "id": "182-deprecated/rls-permissions#debugging", + "id": "186-deprecated/rls-permissions#debugging", "title": "RLS Permissions (Deprecated)", "searchTitle": "Debugging", "sectionTitle": "Debugging", @@ -1625,7 +1667,7 @@ "kind": "section" }, { - "id": "183-deprecated/rls-permissions#read-permissions", + "id": "187-deprecated/rls-permissions#read-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Read Permissions", "sectionTitle": "Read Permissions", @@ -1635,7 +1677,7 @@ "kind": "section" }, { - "id": "184-deprecated/rls-permissions#write-permissions", + "id": "188-deprecated/rls-permissions#write-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Write Permissions", "sectionTitle": "Write Permissions", @@ -1715,7 +1757,7 @@ "kind": "page" }, { - "id": "185-install#integrate-zero", + "id": "189-install#integrate-zero", "title": "Install Zero", "searchTitle": "Integrate Zero", "sectionTitle": "Integrate Zero", @@ -1725,7 +1767,7 @@ "kind": "section" }, { - "id": "186-install#set-up-your-database", + "id": "190-install#set-up-your-database", "title": "Install Zero", "searchTitle": "Set Up Your Database", "sectionTitle": "Set Up Your Database", @@ -1735,7 +1777,7 @@ "kind": "section" }, { - "id": "187-install#install-zero", + "id": "191-install#install-zero", "title": "Install Zero", "searchTitle": "Install Zero", "sectionTitle": "Install Zero", @@ -1745,7 +1787,7 @@ "kind": "section" }, { - "id": "188-install#set-up-your-zero-schema", + "id": "192-install#set-up-your-zero-schema", "title": "Install Zero", "searchTitle": "Set Up Your Zero Schema", "sectionTitle": "Set Up Your Zero Schema", @@ -1755,7 +1797,7 @@ "kind": "section" }, { - "id": "189-install#set-up-the-zero-client", + "id": "193-install#set-up-the-zero-client", "title": "Install Zero", "searchTitle": "Set Up the Zero Client", "sectionTitle": "Set Up the Zero Client", @@ -1765,7 +1807,7 @@ "kind": "section" }, { - "id": "190-install#sync-data", + "id": "194-install#sync-data", "title": "Install Zero", "searchTitle": "Sync Data", "sectionTitle": "Sync Data", @@ -1775,7 +1817,7 @@ "kind": "section" }, { - "id": "191-install#define-query", + "id": "195-install#define-query", "title": "Install Zero", "searchTitle": "Define Query", "sectionTitle": "Define Query", @@ -1785,7 +1827,7 @@ "kind": "section" }, { - "id": "192-install#add-query-endpoint", + "id": "196-install#add-query-endpoint", "title": "Install Zero", "searchTitle": "Add Query Endpoint", "sectionTitle": "Add Query Endpoint", @@ -1795,7 +1837,7 @@ "kind": "section" }, { - "id": "193-install#invoke-query", + "id": "197-install#invoke-query", "title": "Install Zero", "searchTitle": "Invoke Query", "sectionTitle": "Invoke Query", @@ -1805,7 +1847,7 @@ "kind": "section" }, { - "id": "194-install#more-about-queries", + "id": "198-install#more-about-queries", "title": "Install Zero", "searchTitle": "More about Queries", "sectionTitle": "More about Queries", @@ -1815,7 +1857,7 @@ "kind": "section" }, { - "id": "195-install#mutate-data", + "id": "199-install#mutate-data", "title": "Install Zero", "searchTitle": "Mutate Data", "sectionTitle": "Mutate Data", @@ -1825,7 +1867,7 @@ "kind": "section" }, { - "id": "196-install#define-mutators", + "id": "200-install#define-mutators", "title": "Install Zero", "searchTitle": "Define Mutators", "sectionTitle": "Define Mutators", @@ -1835,7 +1877,7 @@ "kind": "section" }, { - "id": "197-install#add-mutate-endpoint", + "id": "201-install#add-mutate-endpoint", "title": "Install Zero", "searchTitle": "Add Mutate Endpoint", "sectionTitle": "Add Mutate Endpoint", @@ -1845,7 +1887,7 @@ "kind": "section" }, { - "id": "198-install#invoke-mutators", + "id": "202-install#invoke-mutators", "title": "Install Zero", "searchTitle": "Invoke Mutators", "sectionTitle": "Invoke Mutators", @@ -1855,7 +1897,7 @@ "kind": "section" }, { - "id": "199-install#more-about-mutators", + "id": "203-install#more-about-mutators", "title": "Install Zero", "searchTitle": "More about Mutators", "sectionTitle": "More about Mutators", @@ -1878,7 +1920,7 @@ "title": "Mutators", "searchTitle": "Mutators", "url": "/docs/mutators", - "content": "Mutators are how you write data with Zero. Here's a simple example: // src/mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' export const mutators = defineMutators({ updateIssue: defineMutator( z.object({ id: z.string(), title: z.string() }), async ({tx, args: {id, title}}) => { if (title.length > 100) { throw new Error(`Title is too long`) } await tx.mutate.issue.update({ id, title }) } ) }) Architecture A copy of each mutator exists on both the client and on your server: Often the implementations will be the same, and you can just share their code. This is easy with full-stack frameworks like TanStack Start or Next.js. But the implementations don't have to be the same, or even compute the same result. For example, the server can add extra checks to enforce permissions, or send notifications or interact with other systems. Life of a Mutation When a mutator is invoked, it initially runs on the client, against the client-side datastore. Any changes are immediately applied to open queries and the user sees the changes. In the background, Zero sends a mutation (a record of the mutator having run with certain arguments) to your server's push endpoint. Your push endpoint runs the push protocol, executing the server-side mutator in a transaction against your database and recording the fact that the mutation ran. The @rocicorp/zero package contains utilities to make it easy to implement this endpoint in TypeScript. The changes to the database are then replicated to zero-cache using logical replication. zero-cache calculates the updates to active queries and sends rows that have changed to each client. It also sends information about the mutations that have been applied to the database. Clients receive row updates and apply them to their local cache. Any pending mutations which have been applied to the server have their local effects rolled back. Client-side queries are updated and the user sees the changes. Defining Mutators Basics Create a mutator using defineMutator. The only required argument is a MutatorFn, which must be async: import {defineMutator} from '@rocicorp/zero' const myMutator = defineMutator(async () => { // ... }) Mutators almost always complete in the same frame on the client, within milliseconds. The reason they are marked async is because on the server, reading from the tx object goes over the network to Postgres. Writing Data The MutatorFn receives a tx parameter which can be used to write data with a CRUD-style API. Each table in your Zero schema has a corresponding field on tx.mutate: const myMutator = defineMutator(async ({tx}) => { // This is here because there's a `user` table in your schema. await tx.mutate.user.insert(...) }) Mutators almost always run in the same frame on the client, against local data. The reason mutators are marked async is because on the server, reading from the tx object goes over the network to Postgres. Also, in edge cases on the client, reads and writes can go to local storage (IndexedDB or SQLite). Insert Create new records with insert: tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: 'js' }) If the Zero primary key already exists, insert will succeed without changing the row - use upsert to update an existing row. Optional fields can be set to null to explicitly set the new field to null. They can also be set to undefined to take the default value (which is often null but can also be some generated value server-side): // Sets language to `null` specifically tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: null }) // Sets language to the default server-side value. // Could be null, or some generated or constant default value too. tx.mutate.user.insert({ id: 'user-123', username: 'sam' }) // Same as above tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: undefined }) Upsert Create new records or update existing ones with upsert: tx.mutate.user.upsert({ id: samID, username: 'sam', language: 'ts' }) upsert supports the same null / undefined semantics for optional fields that insert does (see above). Update Update an existing record. Does nothing if the specified record (by PK) does not exist. You can pass a partial object, leaving fields out that you don’t want to change. For example here we leave the username the same: // Leaves username field to previous value. tx.mutate.user.update({ id: samID, language: 'golang' }) // Same as above tx.mutate.user.update({ id: samID, username: undefined, language: 'haskell' }) // Reset language field to `null` tx.mutate.user.update({ id: samID, language: null }) Delete Delete an existing record. Does nothing if specified record does not exist. tx.mutate.user.delete({ id: samID }) Arguments The MutatorFn can take a single args parameter. To enable this, pass a validator to defineMutator: import {defineMutator} from '@rocicorp/zero' const initStats = defineMutator( z.object({issueCount: z.number()}), async ({tx, args: {issueCount}}) => { if (issueCount < 0) { throw new Error(`issueCount cannot be negative`) } await tx.mutate.stats.insert({ id: 'global', issueCount }) } ) We use Zod in these examples, but you can use any validation library that implements Standard Schema. It's most common for mutators to be a pure function of the database state plus arguments. But it's not required. Impure mutators can be useful, e.g., to consult some external system on the server for authorization or validation. Reading Data You can read data within a mutator by passing ZQL to tx.run: const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { const issue = await tx.run( zql.issue.where('id', id).one() ) if (issue?.status === 'closed') { throw new Error(`Cannot update closed issue`) } await tx.mutate.issue.update({ id, title }) } ) You have the full power of ZQL at your disposal, including relationships, filters, ordering, and limits. Reads and writes within a mutator are transactional, meaning that the datastore is guaranteed to not change while your mutator is running. And if the mutator throws, the entire mutation is rolled back. Unlike zero.run(), there is no type parameter that can be used to wait for server results inside mutators. This is because waiting for server results in mutators makes no sense – it would defeat the purpose of running optimistically to begin with. When a mutator runs on the client (tx.location === \"client\"), ZQL reads only return data already cached on the client. When mutators run on the server (tx.location === \"server\"), ZQL reads always return all data. Context Mutator parameters are supplied by the client application and passed to the server automatically by Zero. This makes them unsuitable for credentials, since the user could modify them. For this reason, Zero mutators also support the concept of a context object. Access your context with the ctx parameter to your mutator: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, ctx: {userID}, args: {id, title}}) => { // Note: User cannot control ctx.userID, so this // enforces authorship of created issue. await tx.mutate.issue.insert({ id, title, authorID: userID }) } ) If you don't want to register your Context and Schema types globally, you can use defineMutatorWithType and defineMutatorsWithType: import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {DrizzleTransaction} from '@rocicorp/zero/server/adapters/drizzle' import type {drizzleClient} from 'db-provider.ts' const defineMutator = defineMutatorWithType< Schema, ZeroContext, DrizzleTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {KyselyTransaction} from '@rocicorp/zero/server/adapters/kysely' import type {Database} from 'db-provider.ts' const defineMutator = defineMutatorWithType< Schema, ZeroContext, KyselyTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {PrismaTransaction} from '@rocicorp/zero/server/adapters/prisma' import type {PrismaClient} from '@prisma/client' const defineMutator = defineMutatorWithType< Schema, ZeroContext, PrismaTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {NodePgTransaction} from '@rocicorp/zero/server/adapters/pg' const defineMutator = defineMutatorWithType< Schema, ZeroContext, NodePgTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {PostgresJsTransaction} from '@rocicorp/zero/server/adapters/postgresjs' const defineMutator = defineMutatorWithType< Schema, ZeroContext, PostgresJsTransaction >() const defineMutators = defineMutatorsWithType() Mutator Registries The result of defineMutator is a MutatorDefinition. By itself this isn't super useful. You need to register it using defineMutators: export const mutators = defineMutators({ issue: { update: updateIssue } }) Typically these are done together in one step: export const mutators = defineMutators({ issue: { update: defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.update({ id, title }) } ) } }) The result of defineMutators is called a MutatorRegistry. Each field in the registry is a callable Mutator that you can use to perform mutations: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: 'issue-123', title: 'New title' }) ) Mutator Names Each Mutator has a mutatorName which is computed by defineMutators. When you run a mutator, Zero sends this name along with the arguments to your server to execute the server-side mutation. console.log(mutators.issue.update.mutatorName) // \"issue.update\" mutators.ts By convention, mutators are listed in a central mutators.ts file. This allows them to be easily used on both the client and server: import {defineMutators, defineMutator} from '@rocicorp/zero' import {zql} from './schema.ts' import {z} from 'zod' export const mutators = defineMutators({ posts: { create: defineMutator( z.object({ id: z.string(), title: z.string() }), async ({ tx, context: {userID}, args: {id, title} }) => { await tx.mutate.post.insert({ id, title, authorID: userID }) } ), update: defineMutator( z.object({ id: z.string(), title: z.string().optional() }), async ({ tx, context: {userID}, args: {id, title} }) => { const prev = await tx.run( zql.post.where('id', id).one() ) if (prev?.authorID !== userID) { throw new Error(`Access denied`) } await tx.mutate.post.update({ id, title, authorID: userID }) } ) } }) You can use as many levels of nesting as you want to organize your mutators. As your application grows, you can move mutators to different files to keep them organized: // posts.ts export const postMutators = { create: defineMutator( z.object({ id: z.string(), title: z.string(), }), async ({tx, context: {userID}, args: {id, title}}) => { await tx.mutate.post.insert({ id, title, authorID: userID, }) }, ), } // user.ts export const userMutators = { updateRole: defineMutator( z.object({ role: z.string(), }), async ({tx, ctx: {userID}, args: {role}}) => { await tx.mutate.user.update({ id: userID, role, }) }, ), } // mutators.ts import {postMutators} from 'zero/mutators/posts.ts' import {userMutators} from 'zero/mutators/users.ts' export const mutators = defineMutators{{ posts: postMutators, users: userMutators, }) defineMutators establishes the full name for each mutator (i.e., posts.create, users.updateRole), which is later sent to the server. So this should only be used once at the top level of your mutators.ts file. Registration Before you can use your mutators, you need to register them with Zero: import {ZeroProvider} from '@rocicorp/zero/react' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } return ( )import {ZeroProvider} from '@rocicorp/zero/solid' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } return ( )import {Zero} from '@rocicorp/zero' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } const zero = new Zero(opts) Mutators need to be registered with Zero because Zero calls them during sync for conflict resolution. If you invoke a mutator that is not registered, Zero will throw an error. Server Setup In order for mutations to sync, you must provide an implementation of the mutate endpoint on your server. zero-cache calls this endpoint to process each mutation. Registering the Endpoint Use ZERO_MUTATE_URL to tell zero-cache where to find your mutate implementation: export ZERO_MUTATE_URL=\"http://localhost:3000/api/zero/mutate\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleMutateRequest and mustGetMutator functions to implement the endpoint. Plug in whatever dbProvider you set up (see server-zql or the install guide). // src/routes/api/zero/mutate.ts import {createFileRoute} from '@tanstack/react-router' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request, userID: null }) return Response.json(result) } } } })// app/api/zero/mutate/route.ts import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(request: Request) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request, userID: null }) return Response.json(result) }// src/routes/api/zero/mutate.ts import type {APIEvent} from '@solidjs/start/server' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(event: APIEvent) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {Hono} from 'hono' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from './db-provider.ts' const app = new Hono() app.post('/api/zero/mutate', async c => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request: c.req.raw, userID: null }) return c.json(result) }) Zero includes several built-in database adapters. You can also easily create your own. See ZQL on the Server for more information. handleMutateRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetMutator looks up the mutator in the registry and throws an error if not found. The mutator.fn function is your mutator implementation wrapped in the validator you provided. These examples have only public mutators, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the mutate handler. See Authentication. Handling Errors The handleMutateRequest function skips any mutations that throw: const result = await handleMutateRequest({ dbProvider, handler: transact => transact(async (tx, name, args) => { // The mutation is skipped and the next mutation runs as normal. // The optimistic mutation on the client will be reverted. throw new Error('bonk') }), request: c.req.raw, userID: null }) handleMutateRequest catches such errors and turns them into a structured response that gets sent back to the client. You can recover the errors and show UI if you want. It is also of course possible for the entire push endpoint to return an HTTP error, or to not reply at all: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async () => { throw new Error('zonk') // will trigger resend } } } })export async function POST() { throw new Error('zonk') // will trigger resend }export async function POST() { throw new Error('zonk') // will trigger resend }app.post('/api/zero/mutate', async c => { // This will cause the client to resend all queued mutations. throw new Error('zonk') }) Responses other than 200, 401, or 403 enter the error state. zero-cache will retry on 5xx up to four times before returning an error. If Zero receives HTTP 401 or 403, the client will enter the needs auth state and require a manual reconnect. Use zero.connection.connect() for cookie auth or zero.connection.connect({auth: newToken}) for token auth, then Zero will retry all queued mutations. If you want a different behavior, it is possible to implement the mutate endpoint yourself and handle errors differently. Custom Mutate URL By default, Zero sends mutations to the URL specified in the ZERO_MUTATE_URL parameter. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in the ZERO_MUTATE_URL parameter: export ZERO_MUTATE_URL=\"https://api.example.com/mutate,https://api.staging.example.com/mutate\" Then choose one of those URLs by passing it to mutateURL on the Zero constructor: const opts: ZeroOptions = { // ... mutateURL: 'https://api.staging.example.com/mutate' } URL Patterns The strings listed in ZERO_MUTATE_URL can also be URLPatterns: export ZERO_MUTATE_URL=\"https://mybranch-*.preview.myapp.com/mutate\" For more information, see the URLPattern section of the Queries docs. It works the same way for mutations. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Server-Specific Code To implement server-specific code, just run different mutators in your mutate endpoint. Server authority to the rescue! defineMutators accepts a baseMutators parameter that makes this easy. The returned mutator registry will contain all the mutators from baseMutators, plus any new ones you define or override: // server-mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' import {mutators as sharedMutators} from 'mutators.ts' export const serverMutators = defineMutators( sharedMutators, { posts: { // Overrides the shared mutator definition with same name. update: defineMutator( z.object({ id: z.string(), title: z.string().optional(), priority: z.number().optional() }), async ({ tx, ctx: {userID}, args: {id, title, priority} }) => { // Run the shared mutator first. await sharedMutators.posts.update.fn({ tx, ctx, args }) // Record a history of this operation happening in an audit log table. await tx.mutate.auditLog.insert({ issueId: id, action: 'update-title', timestamp: Date.getTime() }) } ) } } ) For simple things, we also expose a location field on the transaction object that you can use to branch your code: const myMutator = defineMutator(async ({tx}) => { if (tx.location === 'client') { // Client-side code } else { // Server-side code } }) Running Mutators Once you have registered your mutators, you can invoke them with zero.mutate: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: crypto.randomUUID(), title: 'New title' }) ) Client-generated random IDs from crypto.randomUUID(), uuid, ulid, or nanoid work much better with sync engines like Zero. See IDs for more details. Waiting for Results We typically recommend that you \"fire and forget\" mutators. Optimistic mutations make sense when the common case is that a mutation succeeds. If a mutation frequently fails, then showing the user an optimistic result isn't very useful, because it will likely be wrong. That said there are cases where it is nice to know when a write succeeded on either the client or server. One example is if you need to read a row directly after writing it. Zero's local writes are very fast (almost always < 1 frame), but because Zero is backed by IndexedDB, writes are still technically asynchronous and reads directly after a write may not return the new data. You can use the .client promise in this case to wait for a write to complete on the client side: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) // issue-123 not guaranteed to be present here. read1 may be undefined. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await client write – almost always less than 1 frame, and same // macrotask, so no browser paint will occur here. const res = await write.client if (res.type === 'error') { console.error('Mutator failed on client', res.error) } // issue-123 definitely can be read now. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) You can also await .server for the server result: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) const clientRes = await write.client if (clientRes.type === 'error') { throw new Error(`Mutator failed on client`, { cause: clientRes.error }) } // optimistic write guaranteed to be present here, but not // server write. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await the server result/acknowledgment. This requires a round trip. const serverRes = await write.server if (serverRes.type === 'error') { throw new Error(`Mutator failed on server`, { cause: serverRes.error }) } // The server acknowledged the mutation, but its Postgres changes // may not have replicated to this client yet. This read can still // reflect optimistic rather than authoritative state. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) If the client-side mutator fails, .server also resolves to an error result. Awaiting .server therefore covers both client- and server-side failures. There is not yet a way to return data from mutators in the success case. Let us know if you need this. Permissions Because mutators are just normal TypeScript functions that run server-side, there is no need for a special permissions system. You can implement whatever permission checks you want using plain TypeScript code. See Permissions for more information. Dropping Down to Raw SQL The ServerTransaction interface has a dbTransaction property that exposes the underlying database connection. This allows you to run raw SQL queries directly against the database. This is useful for complex queries, or for using Postgres features that Zero doesn't support yet: const markAllAsRead = defineMutator( z.object({ userId: z.string() }), async ({tx, args: {userId}}) => { // shared stuff ... if (tx.location === 'server') { // `tx` is now narrowed to `ServerTransaction`. // Do special server-only stuff with raw SQL. await tx.dbTransaction.query( ` UPDATE notification SET read = true WHERE user_id = $1 `, [userId] ) } } ) See ZQL on the Server for more information. Notifications and Async Work The best way to handle notifications and async work is a transactional outbox. This ensures that notifications actually do eventually get sent, without holding open database transactions to talk over the network. This can be implemented very easily in Zero by writing notifications to an outbox table as part of your mutator, then processing that table periodically with a background job. However sometimes it's still nice to do a quick and dirty async send as part of a mutation, for example early on in development, or to record metrics. For this, the createMutators pattern is useful: // server-mutators.ts import {defineMutator} from '@rocicorp/zero' import z from 'zod' import {zql} from 'schema.ts' import {mutators as clientMutators} from 'mutators.ts' // Instead of defining server mutators as a constant, // define them as a function of a list of async tasks. export function createMutators( asyncTasks: Array<() => Promise> ) { return defineMutators(clientMutators, { issue: { update: defineMutator( z.object({ id: z.string(), title: z.string() }), async (tx, {id, title}) => { await tx.mutate.issue.update({id, title}) asyncTasks.push(() => sendEmailToSubscribers(id)) } ) } }) } Then in your mutate handler: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ tx, args }) }), request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled( asyncTasks.map(task => task()) ) return Response.json(result) } } } })export async function POST(request: Request) { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({tx, args}) }), request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return Response.json(result) }export async function POST(event: APIEvent) { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({tx, args}) }), request: event.request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return Response.json(result) }app.post('/api/zero/mutate', async c => { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ tx, args }) }), request: c.req.raw, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return c.json(result) }) Custom Mutate Implementation You can manually implement the mutate endpoint in any programming language. This will be documented in the future, but you can refer to the handleMutateRequest source code for an example for now.", + "content": "Mutators are how you write data with Zero. Here's a simple example: // src/mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' export const mutators = defineMutators({ updateIssue: defineMutator( z.object({ id: z.string(), title: z.string() }), async ({tx, args: {id, title}}) => { if (title.length > 100) { throw new Error(`Title is too long`) } await tx.mutate.issue.update({ id, title }) } ) }) Architecture A copy of each mutator exists on both the client and on your server: Often the implementations will be the same, and you can just share their code. This is easy with full-stack frameworks like TanStack Start or Next.js. But the implementations don't have to be the same, or even compute the same result. For example, the server can add extra checks to enforce permissions, or send notifications or interact with other systems. Life of a Mutation When a mutator is invoked, it initially runs on the client, against the client-side datastore. Any changes are immediately applied to open queries and the user sees the changes. In the background, Zero sends a mutation (a record of the mutator having run with certain arguments) to your server's push endpoint. Your push endpoint runs the push protocol, executing the server-side mutator in a transaction against your database and recording the fact that the mutation ran. The @rocicorp/zero package contains utilities to make it easy to implement this endpoint in TypeScript. The changes to the database are then replicated to zero-cache using logical replication. zero-cache calculates the updates to active queries and sends rows that have changed to each client. It also sends information about the mutations that have been applied to the database. Clients receive row updates and apply them to their local cache. Any pending mutations which have been applied to the server have their local effects rolled back. Client-side queries are updated and the user sees the changes. Defining Mutators Basics Create a mutator using defineMutator. The only required argument is a MutatorFn, which must be async: import {defineMutator} from '@rocicorp/zero' const myMutator = defineMutator(async () => { // ... }) Mutators almost always complete in the same frame on the client, within milliseconds. The reason they are marked async is because on the server, reading from the tx object goes over the network to Postgres. Writing Data The MutatorFn receives a tx parameter which can be used to write data with a CRUD-style API. Each table in your Zero schema has a corresponding field on tx.mutate: const myMutator = defineMutator(async ({tx}) => { // This is here because there's a `user` table in your schema. await tx.mutate.user.insert(...) }) Mutators almost always run in the same frame on the client, against local data. The reason mutators are marked async is because on the server, reading from the tx object goes over the network to Postgres. Also, in edge cases on the client, reads and writes can go to local storage (IndexedDB or SQLite). Insert Create new records with insert: tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: 'js' }) If the Zero primary key already exists, insert will succeed without changing the row - use upsert to update an existing row. Optional fields can be set to null to explicitly set the new field to null. They can also be set to undefined to take the default value (which is often null but can also be some generated value server-side): // Sets language to `null` specifically tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: null }) // Sets language to the default server-side value. // Could be null, or some generated or constant default value too. tx.mutate.user.insert({ id: 'user-123', username: 'sam' }) // Same as above tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: undefined }) Upsert Create new records or update existing ones with upsert: tx.mutate.user.upsert({ id: samID, username: 'sam', language: 'ts' }) upsert supports the same null / undefined semantics for optional fields that insert does (see above). Update Update an existing record. Does nothing if the specified record (by PK) does not exist. You can pass a partial object, leaving fields out that you don’t want to change. For example here we leave the username the same: // Leaves username field to previous value. tx.mutate.user.update({ id: samID, language: 'golang' }) // Same as above tx.mutate.user.update({ id: samID, username: undefined, language: 'haskell' }) // Reset language field to `null` tx.mutate.user.update({ id: samID, language: null }) Delete Delete an existing record. Does nothing if specified record does not exist. tx.mutate.user.delete({ id: samID }) Arguments The MutatorFn can take a single args parameter. To enable this, pass a validator to defineMutator: import {defineMutator} from '@rocicorp/zero' const initStats = defineMutator( z.object({issueCount: z.number()}), async ({tx, args: {issueCount}}) => { if (issueCount < 0) { throw new Error(`issueCount cannot be negative`) } await tx.mutate.stats.insert({ id: 'global', issueCount }) } ) We use Zod in these examples, but you can use any validation library that implements Standard Schema. It's most common for mutators to be a pure function of the database state plus arguments. But it's not required. Impure mutators can be useful, e.g., to consult some external system on the server for authorization or validation. Reading Data You can read data within a mutator by passing ZQL to tx.run: const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { const issue = await tx.run( zql.issue.where('id', id).one() ) if (issue?.status === 'closed') { throw new Error(`Cannot update closed issue`) } await tx.mutate.issue.update({ id, title }) } ) You have the full power of ZQL at your disposal, including relationships, filters, ordering, and limits. Reads and writes within a mutator are transactional, meaning that the datastore is guaranteed to not change while your mutator is running. And if the mutator throws, the entire mutation is rolled back. Unlike zero.run(), there is no type parameter that can be used to wait for server results inside mutators. This is because waiting for server results in mutators makes no sense – it would defeat the purpose of running optimistically to begin with. When a mutator runs on the client (tx.location === \"client\"), ZQL reads only return data already cached on the client. When mutators run on the server (tx.location === \"server\"), ZQL reads always return all data. Context Mutator parameters are supplied by the client application and passed to the server automatically by Zero. This makes them unsuitable for credentials, since the user could modify them. For this reason, Zero mutators also support the concept of a context object. Access your context with the ctx parameter to your mutator: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, ctx: {userID}, args: {id, title}}) => { // Note: User cannot control ctx.userID, so this // enforces authorship of created issue. await tx.mutate.issue.insert({ id, title, authorID: userID }) } ) If you don't want to register your Context and Schema types globally, you can use defineMutatorWithType and defineMutatorsWithType: import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {DrizzleTransaction} from '@rocicorp/zero/server/adapters/drizzle' import type {drizzleClient} from 'db-provider.ts' const defineMutator = defineMutatorWithType< Schema, ZeroContext, DrizzleTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {KyselyTransaction} from '@rocicorp/zero/server/adapters/kysely' import type {Database} from 'db-provider.ts' const defineMutator = defineMutatorWithType< Schema, ZeroContext, KyselyTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {PrismaTransaction} from '@rocicorp/zero/server/adapters/prisma' import type {PrismaClient} from '@prisma/client' const defineMutator = defineMutatorWithType< Schema, ZeroContext, PrismaTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {NodePgTransaction} from '@rocicorp/zero/server/adapters/pg' const defineMutator = defineMutatorWithType< Schema, ZeroContext, NodePgTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {PostgresJsTransaction} from '@rocicorp/zero/server/adapters/postgresjs' const defineMutator = defineMutatorWithType< Schema, ZeroContext, PostgresJsTransaction >() const defineMutators = defineMutatorsWithType() Mutator Registries The result of defineMutator is a MutatorDefinition. By itself this isn't super useful. You need to register it using defineMutators: export const mutators = defineMutators({ issue: { update: updateIssue } }) Typically these are done together in one step: export const mutators = defineMutators({ issue: { update: defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.update({ id, title }) } ) } }) The result of defineMutators is called a MutatorRegistry. Each field in the registry is a callable Mutator that you can use to perform mutations: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: 'issue-123', title: 'New title' }) ) Mutator Names Each Mutator has a mutatorName which is computed by defineMutators. When you run a mutator, Zero sends this name along with the arguments to your server to execute the server-side mutation. console.log(mutators.issue.update.mutatorName) // \"issue.update\" mutators.ts By convention, mutators are listed in a central mutators.ts file. This allows them to be easily used on both the client and server: import {defineMutators, defineMutator} from '@rocicorp/zero' import {zql} from './schema.ts' import {z} from 'zod' export const mutators = defineMutators({ posts: { create: defineMutator( z.object({ id: z.string(), title: z.string() }), async ({ tx, context: {userID}, args: {id, title} }) => { await tx.mutate.post.insert({ id, title, authorID: userID }) } ), update: defineMutator( z.object({ id: z.string(), title: z.string().optional() }), async ({ tx, context: {userID}, args: {id, title} }) => { const prev = await tx.run( zql.post.where('id', id).one() ) if (prev?.authorID !== userID) { throw new Error(`Access denied`) } await tx.mutate.post.update({ id, title, authorID: userID }) } ) } }) You can use as many levels of nesting as you want to organize your mutators. As your application grows, you can move mutators to different files to keep them organized: // posts.ts export const postMutators = { create: defineMutator( z.object({ id: z.string(), title: z.string(), }), async ({tx, context: {userID}, args: {id, title}}) => { await tx.mutate.post.insert({ id, title, authorID: userID, }) }, ), } // user.ts export const userMutators = { updateRole: defineMutator( z.object({ role: z.string(), }), async ({tx, ctx: {userID}, args: {role}}) => { await tx.mutate.user.update({ id: userID, role, }) }, ), } // mutators.ts import {postMutators} from 'zero/mutators/posts.ts' import {userMutators} from 'zero/mutators/users.ts' export const mutators = defineMutators{{ posts: postMutators, users: userMutators, }) defineMutators establishes the full name for each mutator (i.e., posts.create, users.updateRole), which is later sent to the server. So this should only be used once at the top level of your mutators.ts file. Registration Before you can use your mutators, you need to register them with Zero: import {ZeroProvider} from '@rocicorp/zero/react' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } return ( )import {ZeroProvider} from '@rocicorp/zero/solid' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } return ( )import {Zero} from '@rocicorp/zero' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } const zero = new Zero(opts) Mutators need to be registered with Zero because Zero calls them during sync for conflict resolution. If you invoke a mutator that is not registered, Zero will throw an error. Server Setup In order for mutations to sync, you must provide an implementation of the mutate endpoint on your server. zero-cache calls this endpoint to process each mutation. Registering the Endpoint Use ZERO_MUTATE_URL to tell zero-cache where to find your mutate implementation: export ZERO_MUTATE_URL=\"http://localhost:3000/api/zero/mutate\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleMutateRequest and mustGetMutator functions to implement the endpoint. Plug in whatever dbProvider you set up (see server-zql or the install guide). // src/routes/api/zero/mutate.ts import {createFileRoute} from '@tanstack/react-router' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request, userID: null }) return Response.json(result) } } } })// app/api/zero/mutate/route.ts import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(request: Request) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request, userID: null }) return Response.json(result) }// src/routes/api/zero/mutate.ts import type {APIEvent} from '@solidjs/start/server' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(event: APIEvent) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {Hono} from 'hono' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from './db-provider.ts' const app = new Hono() app.post('/api/zero/mutate', async c => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request: c.req.raw, userID: null }) return c.json(result) }) Zero includes several built-in database adapters. You can also easily create your own. See ZQL on the Server for more information. handleMutateRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetMutator looks up the mutator in the registry and throws an error if not found. The mutator.fn function is your mutator implementation wrapped in the validator you provided. These examples have only public mutators, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the mutate handler. See Authentication. Handling Errors The handleMutateRequest function skips any mutations that throw: const result = await handleMutateRequest({ dbProvider, handler: transact => transact(async (tx, name, args) => { // The mutation is skipped and the next mutation runs as normal. // The optimistic mutation on the client will be reverted. throw new Error('bonk') }), request: c.req.raw, userID: null }) handleMutateRequest catches such errors and turns them into a structured response that gets sent back to the client. You can recover the errors and show UI if you want. It is also of course possible for the entire push endpoint to return an HTTP error, or to not reply at all: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async () => { throw new Error('zonk') // will trigger resend } } } })export async function POST() { throw new Error('zonk') // will trigger resend }export async function POST() { throw new Error('zonk') // will trigger resend }app.post('/api/zero/mutate', async c => { // This will cause the client to resend all queued mutations. throw new Error('zonk') }) Responses other than 200, 401, or 403 enter the error state. zero-cache will retry on 5xx up to four times before returning an error. If Zero receives HTTP 401 or 403, the client will enter the needs auth state and require a manual reconnect. Use zero.connection.connect() for cookie auth or zero.connection.connect({auth: newToken}) for token auth, then Zero will retry all queued mutations. If you want a different behavior, it is possible to implement the mutate endpoint yourself and handle errors differently. Retrying Mutators Some errors go away if you try again. For example, at the repeatable read or serializable isolation level, Postgres can abort a transaction that conflicts with a concurrent one. The error is a serialization failure, code 40001. Call tx.retryOn() in a server mutator to have Zero run the mutator again, in a fresh transaction, when it fails with an error you choose: function isSerializationFailure(error: unknown) { return ( error instanceof Error && 'code' in error && error.code === '40001' ) } const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { if (tx.location === 'server') { tx.retryOn(isSerializationFailure) } await tx.mutate.issue.update({id, title}) } ) retryOn() takes a predicate. It gets the error and the attempt number: 1 when deciding the first re-run, 2 for the second, and so on. Return true to run the mutator again. If it returns anything else or throws, Zero records the error and skips the mutation, as with any other error. The predicate sees errors the mutator throws, and also errors Postgres raises at COMMIT, which the mutator can't catch. Everything the failed run wrote is rolled back, and the re-run reads fresh data. The error is passed as it was thrown. node-postgres and postgres.js errors have the Postgres error code in code. Other libraries can wrap the error, so check what yours throws. The predicate can also return a promise. Zero waits for it, so you can use it to back off before the next run: tx.retryOn(async (error, attempt) => { if (!isSerializationFailure(error)) { return false } // Wait 50ms, then 100ms, then 150ms, and so on. await new Promise(resolve => setTimeout(resolve, attempt * 50) ) return true }) A few things to know: retryOn() covers only the current run. The re-run calls it again because it runs the mutator again. Call it before anything that can fail. You can call retryOn() more than once. Zero runs the mutator again if any predicate returns true. Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run fails, Zero records the error. The predicate runs after the transaction has ended, so it can't use tx. Reading or writing through tx there throws. Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. retryOn() is only available on the server, when tx.location === 'server'. The built-in adapters and ZQLDatabase support retryOn(). If you pass your own Database to handleMutateRequest, it must pass the addRetryPredicate it receives in TransactionProviderInput on to the transaction it creates with CRUDMutatorFactory.createTransaction(). Otherwise tx.retryOn() throws. Custom Mutate URL By default, Zero sends mutations to the URL specified in the ZERO_MUTATE_URL parameter. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in the ZERO_MUTATE_URL parameter: export ZERO_MUTATE_URL=\"https://api.example.com/mutate,https://api.staging.example.com/mutate\" Then choose one of those URLs by passing it to mutateURL on the Zero constructor: const opts: ZeroOptions = { // ... mutateURL: 'https://api.staging.example.com/mutate' } URL Patterns The strings listed in ZERO_MUTATE_URL can also be URLPatterns: export ZERO_MUTATE_URL=\"https://mybranch-*.preview.myapp.com/mutate\" For more information, see the URLPattern section of the Queries docs. It works the same way for mutations. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Server-Specific Code To implement server-specific code, just run different mutators in your mutate endpoint. Server authority to the rescue! defineMutators accepts a baseMutators parameter that makes this easy. The returned mutator registry will contain all the mutators from baseMutators, plus any new ones you define or override: // server-mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' import {mutators as sharedMutators} from 'mutators.ts' export const serverMutators = defineMutators( sharedMutators, { posts: { // Overrides the shared mutator definition with same name. update: defineMutator( z.object({ id: z.string(), title: z.string().optional(), priority: z.number().optional() }), async ({ tx, ctx: {userID}, args: {id, title, priority} }) => { // Run the shared mutator first. await sharedMutators.posts.update.fn({ tx, ctx, args }) // Record a history of this operation happening in an audit log table. await tx.mutate.auditLog.insert({ issueId: id, action: 'update-title', timestamp: Date.getTime() }) } ) } } ) For simple things, we also expose a location field on the transaction object that you can use to branch your code: const myMutator = defineMutator(async ({tx}) => { if (tx.location === 'client') { // Client-side code } else { // Server-side code } }) Running Mutators Once you have registered your mutators, you can invoke them with zero.mutate: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: crypto.randomUUID(), title: 'New title' }) ) Client-generated random IDs from crypto.randomUUID(), uuid, ulid, or nanoid work much better with sync engines like Zero. See IDs for more details. Waiting for Results We typically recommend that you \"fire and forget\" mutators. Optimistic mutations make sense when the common case is that a mutation succeeds. If a mutation frequently fails, then showing the user an optimistic result isn't very useful, because it will likely be wrong. That said there are cases where it is nice to know when a write succeeded on either the client or server. One example is if you need to read a row directly after writing it. Zero's local writes are very fast (almost always < 1 frame), but because Zero is backed by IndexedDB, writes are still technically asynchronous and reads directly after a write may not return the new data. You can use the .client promise in this case to wait for a write to complete on the client side: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) // issue-123 not guaranteed to be present here. read1 may be undefined. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await client write – almost always less than 1 frame, and same // macrotask, so no browser paint will occur here. const res = await write.client if (res.type === 'error') { console.error('Mutator failed on client', res.error) } // issue-123 definitely can be read now. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) You can also await .server for the server result: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) const clientRes = await write.client if (clientRes.type === 'error') { throw new Error(`Mutator failed on client`, { cause: clientRes.error }) } // optimistic write guaranteed to be present here, but not // server write. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await the server result/acknowledgment. This requires a round trip. const serverRes = await write.server if (serverRes.type === 'error') { throw new Error(`Mutator failed on server`, { cause: serverRes.error }) } // The server acknowledged the mutation, but its Postgres changes // may not have replicated to this client yet. This read can still // reflect optimistic rather than authoritative state. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) If the client-side mutator fails, .server also resolves to an error result. Awaiting .server therefore covers both client- and server-side failures. Returning Data A mutator can return a JSON value. The caller gets it in the data field of the success result. The client and server runs can return different values. For example, the server can return a value that only the database knows: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.insert({id, title}) if (tx.location === 'client') { // The client doesn't know the short ID yet. return {shortID: null} } // Postgres assigns shortID when the row is inserted. const issue = await tx.run( zql.issue.where('id', id).one() ) return {shortID: issue?.shortID ?? null} } ) The data of the .client result is what the client run returned. The data of the .server result is what the server run returned: const write = zero.mutate( mutators.issue.create({ id: crypto.randomUUID(), title: 'New title' }) ) const serverRes = await write.server if (serverRes.type === 'success') { // `data` is a plain JSON value, so check its shape. const {shortID} = z .object({shortID: z.number().nullable()}) .parse(serverRes.data) console.log('Short ID:', shortID) } A few things to know: data is typed as ReadonlyJSONValue | undefined, not as the mutator's return type. Check its shape before you use it. If the mutator returns nothing, data is undefined. The server value only reaches the client if your mutate handler returns the mutator's result, as return mutator.fn({args, tx}) does in the examples above. Zero saves the server's return value in your database along with the mutation's result, so the client still gets it if the response from your mutate endpoint is lost. A mutator that returns nothing doesn't write this. Permissions Because mutators are just normal TypeScript functions that run server-side, there is no need for a special permissions system. You can implement whatever permission checks you want using plain TypeScript code. See Permissions for more information. Dropping Down to Raw SQL The ServerTransaction interface has a dbTransaction property that exposes the underlying database connection. This allows you to run raw SQL queries directly against the database. This is useful for complex queries, or for using Postgres features that Zero doesn't support yet: const markAllAsRead = defineMutator( z.object({ userId: z.string() }), async ({tx, args: {userId}}) => { // shared stuff ... if (tx.location === 'server') { // `tx` is now narrowed to `ServerTransaction`. // Do special server-only stuff with raw SQL. await tx.dbTransaction.query( ` UPDATE notification SET read = true WHERE user_id = $1 `, [userId] ) } } ) See ZQL on the Server for more information. Notifications and Async Work The best way to handle notifications and async work is a transactional outbox. This ensures that notifications actually do eventually get sent, without holding open database transactions to talk over the network. This can be implemented very easily in Zero by writing notifications to an outbox table as part of your mutator, then processing that table periodically with a background job. However sometimes it's still nice to do a quick and dirty async send as part of a mutation, for example early on in development, or to record metrics. For this, the createMutators pattern is useful: // server-mutators.ts import {defineMutator} from '@rocicorp/zero' import z from 'zod' import {zql} from 'schema.ts' import {mutators as clientMutators} from 'mutators.ts' // Instead of defining server mutators as a constant, // define them as a function of a list of async tasks. export function createMutators( asyncTasks: Array<() => Promise> ) { return defineMutators(clientMutators, { issue: { update: defineMutator( z.object({ id: z.string(), title: z.string() }), async (tx, {id, title}) => { await tx.mutate.issue.update({id, title}) asyncTasks.push(() => sendEmailToSubscribers(id)) } ) } }) } Then in your mutate handler: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ tx, args }) }), request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled( asyncTasks.map(task => task()) ) return Response.json(result) } } } })export async function POST(request: Request) { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({tx, args}) }), request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return Response.json(result) }export async function POST(event: APIEvent) { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({tx, args}) }), request: event.request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return Response.json(result) }app.post('/api/zero/mutate', async c => { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ tx, args }) }), request: c.req.raw, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return c.json(result) }) Custom Mutate Implementation You can manually implement the mutate endpoint in any programming language. This will be documented in the future, but you can refer to the handleMutateRequest source code for an example for now.", "headings": [ { "text": "Architecture", @@ -1960,6 +2002,10 @@ "text": "Handling Errors", "id": "handling-errors" }, + { + "text": "Retrying Mutators", + "id": "retrying-mutators" + }, { "text": "Custom Mutate URL", "id": "custom-mutate-url" @@ -1980,6 +2026,10 @@ "text": "Waiting for Results", "id": "waiting-for-results" }, + { + "text": "Returning Data", + "id": "returning-data" + }, { "text": "Permissions", "id": "permissions" @@ -2000,7 +2050,7 @@ "kind": "page" }, { - "id": "200-mutators#architecture", + "id": "204-mutators#architecture", "title": "Mutators", "searchTitle": "Architecture", "sectionTitle": "Architecture", @@ -2010,7 +2060,7 @@ "kind": "section" }, { - "id": "201-mutators#life-of-a-mutation", + "id": "205-mutators#life-of-a-mutation", "title": "Mutators", "searchTitle": "Life of a Mutation", "sectionTitle": "Life of a Mutation", @@ -2020,7 +2070,7 @@ "kind": "section" }, { - "id": "202-mutators#defining-mutators", + "id": "206-mutators#defining-mutators", "title": "Mutators", "searchTitle": "Defining Mutators", "sectionTitle": "Defining Mutators", @@ -2030,7 +2080,7 @@ "kind": "section" }, { - "id": "203-mutators#basics", + "id": "207-mutators#basics", "title": "Mutators", "searchTitle": "Basics", "sectionTitle": "Basics", @@ -2040,7 +2090,7 @@ "kind": "section" }, { - "id": "204-mutators#writing-data", + "id": "208-mutators#writing-data", "title": "Mutators", "searchTitle": "Writing Data", "sectionTitle": "Writing Data", @@ -2050,7 +2100,7 @@ "kind": "section" }, { - "id": "205-mutators#insert", + "id": "209-mutators#insert", "title": "Mutators", "searchTitle": "Insert", "sectionTitle": "Insert", @@ -2060,7 +2110,7 @@ "kind": "section" }, { - "id": "206-mutators#upsert", + "id": "210-mutators#upsert", "title": "Mutators", "searchTitle": "Upsert", "sectionTitle": "Upsert", @@ -2070,7 +2120,7 @@ "kind": "section" }, { - "id": "207-mutators#update", + "id": "211-mutators#update", "title": "Mutators", "searchTitle": "Update", "sectionTitle": "Update", @@ -2080,7 +2130,7 @@ "kind": "section" }, { - "id": "208-mutators#delete", + "id": "212-mutators#delete", "title": "Mutators", "searchTitle": "Delete", "sectionTitle": "Delete", @@ -2090,7 +2140,7 @@ "kind": "section" }, { - "id": "209-mutators#arguments", + "id": "213-mutators#arguments", "title": "Mutators", "searchTitle": "Arguments", "sectionTitle": "Arguments", @@ -2100,7 +2150,7 @@ "kind": "section" }, { - "id": "210-mutators#reading-data", + "id": "214-mutators#reading-data", "title": "Mutators", "searchTitle": "Reading Data", "sectionTitle": "Reading Data", @@ -2110,7 +2160,7 @@ "kind": "section" }, { - "id": "211-mutators#context", + "id": "215-mutators#context", "title": "Mutators", "searchTitle": "Context", "sectionTitle": "Context", @@ -2120,7 +2170,7 @@ "kind": "section" }, { - "id": "212-mutators#mutator-registries", + "id": "216-mutators#mutator-registries", "title": "Mutators", "searchTitle": "Mutator Registries", "sectionTitle": "Mutator Registries", @@ -2130,7 +2180,7 @@ "kind": "section" }, { - "id": "213-mutators#mutator-names", + "id": "217-mutators#mutator-names", "title": "Mutators", "searchTitle": "Mutator Names", "sectionTitle": "Mutator Names", @@ -2140,7 +2190,7 @@ "kind": "section" }, { - "id": "214-mutators#mutatorsts", + "id": "218-mutators#mutatorsts", "title": "Mutators", "searchTitle": "mutators.ts", "sectionTitle": "mutators.ts", @@ -2150,7 +2200,7 @@ "kind": "section" }, { - "id": "215-mutators#registration", + "id": "219-mutators#registration", "title": "Mutators", "searchTitle": "Registration", "sectionTitle": "Registration", @@ -2160,17 +2210,17 @@ "kind": "section" }, { - "id": "216-mutators#server-setup", + "id": "220-mutators#server-setup", "title": "Mutators", "searchTitle": "Server Setup", "sectionTitle": "Server Setup", "sectionId": "server-setup", "url": "/docs/mutators", - "content": "In order for mutations to sync, you must provide an implementation of the mutate endpoint on your server. zero-cache calls this endpoint to process each mutation. Registering the Endpoint Use ZERO_MUTATE_URL to tell zero-cache where to find your mutate implementation: export ZERO_MUTATE_URL=\"http://localhost:3000/api/zero/mutate\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleMutateRequest and mustGetMutator functions to implement the endpoint. Plug in whatever dbProvider you set up (see server-zql or the install guide). // src/routes/api/zero/mutate.ts import {createFileRoute} from '@tanstack/react-router' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request, userID: null }) return Response.json(result) } } } })// app/api/zero/mutate/route.ts import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(request: Request) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request, userID: null }) return Response.json(result) }// src/routes/api/zero/mutate.ts import type {APIEvent} from '@solidjs/start/server' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(event: APIEvent) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {Hono} from 'hono' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from './db-provider.ts' const app = new Hono() app.post('/api/zero/mutate', async c => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request: c.req.raw, userID: null }) return c.json(result) }) Zero includes several built-in database adapters. You can also easily create your own. See ZQL on the Server for more information. handleMutateRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetMutator looks up the mutator in the registry and throws an error if not found. The mutator.fn function is your mutator implementation wrapped in the validator you provided. These examples have only public mutators, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the mutate handler. See Authentication. Handling Errors The handleMutateRequest function skips any mutations that throw: const result = await handleMutateRequest({ dbProvider, handler: transact => transact(async (tx, name, args) => { // The mutation is skipped and the next mutation runs as normal. // The optimistic mutation on the client will be reverted. throw new Error('bonk') }), request: c.req.raw, userID: null }) handleMutateRequest catches such errors and turns them into a structured response that gets sent back to the client. You can recover the errors and show UI if you want. It is also of course possible for the entire push endpoint to return an HTTP error, or to not reply at all: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async () => { throw new Error('zonk') // will trigger resend } } } })export async function POST() { throw new Error('zonk') // will trigger resend }export async function POST() { throw new Error('zonk') // will trigger resend }app.post('/api/zero/mutate', async c => { // This will cause the client to resend all queued mutations. throw new Error('zonk') }) Responses other than 200, 401, or 403 enter the error state. zero-cache will retry on 5xx up to four times before returning an error. If Zero receives HTTP 401 or 403, the client will enter the needs auth state and require a manual reconnect. Use zero.connection.connect() for cookie auth or zero.connection.connect({auth: newToken}) for token auth, then Zero will retry all queued mutations. If you want a different behavior, it is possible to implement the mutate endpoint yourself and handle errors differently. Custom Mutate URL By default, Zero sends mutations to the URL specified in the ZERO_MUTATE_URL parameter. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in the ZERO_MUTATE_URL parameter: export ZERO_MUTATE_URL=\"https://api.example.com/mutate,https://api.staging.example.com/mutate\" Then choose one of those URLs by passing it to mutateURL on the Zero constructor: const opts: ZeroOptions = { // ... mutateURL: 'https://api.staging.example.com/mutate' } URL Patterns The strings listed in ZERO_MUTATE_URL can also be URLPatterns: export ZERO_MUTATE_URL=\"https://mybranch-*.preview.myapp.com/mutate\" For more information, see the URLPattern section of the Queries docs. It works the same way for mutations. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Server-Specific Code To implement server-specific code, just run different mutators in your mutate endpoint. Server authority to the rescue! defineMutators accepts a baseMutators parameter that makes this easy. The returned mutator registry will contain all the mutators from baseMutators, plus any new ones you define or override: // server-mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' import {mutators as sharedMutators} from 'mutators.ts' export const serverMutators = defineMutators( sharedMutators, { posts: { // Overrides the shared mutator definition with same name. update: defineMutator( z.object({ id: z.string(), title: z.string().optional(), priority: z.number().optional() }), async ({ tx, ctx: {userID}, args: {id, title, priority} }) => { // Run the shared mutator first. await sharedMutators.posts.update.fn({ tx, ctx, args }) // Record a history of this operation happening in an audit log table. await tx.mutate.auditLog.insert({ issueId: id, action: 'update-title', timestamp: Date.getTime() }) } ) } } ) For simple things, we also expose a location field on the transaction object that you can use to branch your code: const myMutator = defineMutator(async ({tx}) => { if (tx.location === 'client') { // Client-side code } else { // Server-side code } })", + "content": "In order for mutations to sync, you must provide an implementation of the mutate endpoint on your server. zero-cache calls this endpoint to process each mutation. Registering the Endpoint Use ZERO_MUTATE_URL to tell zero-cache where to find your mutate implementation: export ZERO_MUTATE_URL=\"http://localhost:3000/api/zero/mutate\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleMutateRequest and mustGetMutator functions to implement the endpoint. Plug in whatever dbProvider you set up (see server-zql or the install guide). // src/routes/api/zero/mutate.ts import {createFileRoute} from '@tanstack/react-router' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request, userID: null }) return Response.json(result) } } } })// app/api/zero/mutate/route.ts import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(request: Request) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request, userID: null }) return Response.json(result) }// src/routes/api/zero/mutate.ts import type {APIEvent} from '@solidjs/start/server' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(event: APIEvent) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {Hono} from 'hono' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from './db-provider.ts' const app = new Hono() app.post('/api/zero/mutate', async c => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request: c.req.raw, userID: null }) return c.json(result) }) Zero includes several built-in database adapters. You can also easily create your own. See ZQL on the Server for more information. handleMutateRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetMutator looks up the mutator in the registry and throws an error if not found. The mutator.fn function is your mutator implementation wrapped in the validator you provided. These examples have only public mutators, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the mutate handler. See Authentication. Handling Errors The handleMutateRequest function skips any mutations that throw: const result = await handleMutateRequest({ dbProvider, handler: transact => transact(async (tx, name, args) => { // The mutation is skipped and the next mutation runs as normal. // The optimistic mutation on the client will be reverted. throw new Error('bonk') }), request: c.req.raw, userID: null }) handleMutateRequest catches such errors and turns them into a structured response that gets sent back to the client. You can recover the errors and show UI if you want. It is also of course possible for the entire push endpoint to return an HTTP error, or to not reply at all: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async () => { throw new Error('zonk') // will trigger resend } } } })export async function POST() { throw new Error('zonk') // will trigger resend }export async function POST() { throw new Error('zonk') // will trigger resend }app.post('/api/zero/mutate', async c => { // This will cause the client to resend all queued mutations. throw new Error('zonk') }) Responses other than 200, 401, or 403 enter the error state. zero-cache will retry on 5xx up to four times before returning an error. If Zero receives HTTP 401 or 403, the client will enter the needs auth state and require a manual reconnect. Use zero.connection.connect() for cookie auth or zero.connection.connect({auth: newToken}) for token auth, then Zero will retry all queued mutations. If you want a different behavior, it is possible to implement the mutate endpoint yourself and handle errors differently. Retrying Mutators Some errors go away if you try again. For example, at the repeatable read or serializable isolation level, Postgres can abort a transaction that conflicts with a concurrent one. The error is a serialization failure, code 40001. Call tx.retryOn() in a server mutator to have Zero run the mutator again, in a fresh transaction, when it fails with an error you choose: function isSerializationFailure(error: unknown) { return ( error instanceof Error && 'code' in error && error.code === '40001' ) } const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { if (tx.location === 'server') { tx.retryOn(isSerializationFailure) } await tx.mutate.issue.update({id, title}) } ) retryOn() takes a predicate. It gets the error and the attempt number: 1 when deciding the first re-run, 2 for the second, and so on. Return true to run the mutator again. If it returns anything else or throws, Zero records the error and skips the mutation, as with any other error. The predicate sees errors the mutator throws, and also errors Postgres raises at COMMIT, which the mutator can't catch. Everything the failed run wrote is rolled back, and the re-run reads fresh data. The error is passed as it was thrown. node-postgres and postgres.js errors have the Postgres error code in code. Other libraries can wrap the error, so check what yours throws. The predicate can also return a promise. Zero waits for it, so you can use it to back off before the next run: tx.retryOn(async (error, attempt) => { if (!isSerializationFailure(error)) { return false } // Wait 50ms, then 100ms, then 150ms, and so on. await new Promise(resolve => setTimeout(resolve, attempt * 50) ) return true }) A few things to know: retryOn() covers only the current run. The re-run calls it again because it runs the mutator again. Call it before anything that can fail. You can call retryOn() more than once. Zero runs the mutator again if any predicate returns true. Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run fails, Zero records the error. The predicate runs after the transaction has ended, so it can't use tx. Reading or writing through tx there throws. Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. retryOn() is only available on the server, when tx.location === 'server'. The built-in adapters and ZQLDatabase support retryOn(). If you pass your own Database to handleMutateRequest, it must pass the addRetryPredicate it receives in TransactionProviderInput on to the transaction it creates with CRUDMutatorFactory.createTransaction(). Otherwise tx.retryOn() throws. Custom Mutate URL By default, Zero sends mutations to the URL specified in the ZERO_MUTATE_URL parameter. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in the ZERO_MUTATE_URL parameter: export ZERO_MUTATE_URL=\"https://api.example.com/mutate,https://api.staging.example.com/mutate\" Then choose one of those URLs by passing it to mutateURL on the Zero constructor: const opts: ZeroOptions = { // ... mutateURL: 'https://api.staging.example.com/mutate' } URL Patterns The strings listed in ZERO_MUTATE_URL can also be URLPatterns: export ZERO_MUTATE_URL=\"https://mybranch-*.preview.myapp.com/mutate\" For more information, see the URLPattern section of the Queries docs. It works the same way for mutations. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Server-Specific Code To implement server-specific code, just run different mutators in your mutate endpoint. Server authority to the rescue! defineMutators accepts a baseMutators parameter that makes this easy. The returned mutator registry will contain all the mutators from baseMutators, plus any new ones you define or override: // server-mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' import {mutators as sharedMutators} from 'mutators.ts' export const serverMutators = defineMutators( sharedMutators, { posts: { // Overrides the shared mutator definition with same name. update: defineMutator( z.object({ id: z.string(), title: z.string().optional(), priority: z.number().optional() }), async ({ tx, ctx: {userID}, args: {id, title, priority} }) => { // Run the shared mutator first. await sharedMutators.posts.update.fn({ tx, ctx, args }) // Record a history of this operation happening in an audit log table. await tx.mutate.auditLog.insert({ issueId: id, action: 'update-title', timestamp: Date.getTime() }) } ) } } ) For simple things, we also expose a location field on the transaction object that you can use to branch your code: const myMutator = defineMutator(async ({tx}) => { if (tx.location === 'client') { // Client-side code } else { // Server-side code } })", "kind": "section" }, { - "id": "217-mutators#registering-the-endpoint", + "id": "221-mutators#registering-the-endpoint", "title": "Mutators", "searchTitle": "Registering the Endpoint", "sectionTitle": "Registering the Endpoint", @@ -2180,7 +2230,7 @@ "kind": "section" }, { - "id": "218-mutators#implementing-the-endpoint", + "id": "222-mutators#implementing-the-endpoint", "title": "Mutators", "searchTitle": "Implementing the Endpoint", "sectionTitle": "Implementing the Endpoint", @@ -2190,7 +2240,7 @@ "kind": "section" }, { - "id": "219-mutators#handling-errors", + "id": "223-mutators#handling-errors", "title": "Mutators", "searchTitle": "Handling Errors", "sectionTitle": "Handling Errors", @@ -2200,7 +2250,17 @@ "kind": "section" }, { - "id": "220-mutators#custom-mutate-url", + "id": "224-mutators#retrying-mutators", + "title": "Mutators", + "searchTitle": "Retrying Mutators", + "sectionTitle": "Retrying Mutators", + "sectionId": "retrying-mutators", + "url": "/docs/mutators", + "content": "Some errors go away if you try again. For example, at the repeatable read or serializable isolation level, Postgres can abort a transaction that conflicts with a concurrent one. The error is a serialization failure, code 40001. Call tx.retryOn() in a server mutator to have Zero run the mutator again, in a fresh transaction, when it fails with an error you choose: function isSerializationFailure(error: unknown) { return ( error instanceof Error && 'code' in error && error.code === '40001' ) } const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { if (tx.location === 'server') { tx.retryOn(isSerializationFailure) } await tx.mutate.issue.update({id, title}) } ) retryOn() takes a predicate. It gets the error and the attempt number: 1 when deciding the first re-run, 2 for the second, and so on. Return true to run the mutator again. If it returns anything else or throws, Zero records the error and skips the mutation, as with any other error. The predicate sees errors the mutator throws, and also errors Postgres raises at COMMIT, which the mutator can't catch. Everything the failed run wrote is rolled back, and the re-run reads fresh data. The error is passed as it was thrown. node-postgres and postgres.js errors have the Postgres error code in code. Other libraries can wrap the error, so check what yours throws. The predicate can also return a promise. Zero waits for it, so you can use it to back off before the next run: tx.retryOn(async (error, attempt) => { if (!isSerializationFailure(error)) { return false } // Wait 50ms, then 100ms, then 150ms, and so on. await new Promise(resolve => setTimeout(resolve, attempt * 50) ) return true }) A few things to know: retryOn() covers only the current run. The re-run calls it again because it runs the mutator again. Call it before anything that can fail. You can call retryOn() more than once. Zero runs the mutator again if any predicate returns true. Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run fails, Zero records the error. The predicate runs after the transaction has ended, so it can't use tx. Reading or writing through tx there throws. Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. retryOn() is only available on the server, when tx.location === 'server'. The built-in adapters and ZQLDatabase support retryOn(). If you pass your own Database to handleMutateRequest, it must pass the addRetryPredicate it receives in TransactionProviderInput on to the transaction it creates with CRUDMutatorFactory.createTransaction(). Otherwise tx.retryOn() throws.", + "kind": "section" + }, + { + "id": "225-mutators#custom-mutate-url", "title": "Mutators", "searchTitle": "Custom Mutate URL", "sectionTitle": "Custom Mutate URL", @@ -2210,7 +2270,7 @@ "kind": "section" }, { - "id": "221-mutators#url-patterns", + "id": "226-mutators#url-patterns", "title": "Mutators", "searchTitle": "URL Patterns", "sectionTitle": "URL Patterns", @@ -2220,7 +2280,7 @@ "kind": "section" }, { - "id": "222-mutators#server-specific-code", + "id": "227-mutators#server-specific-code", "title": "Mutators", "searchTitle": "Server-Specific Code", "sectionTitle": "Server-Specific Code", @@ -2230,27 +2290,37 @@ "kind": "section" }, { - "id": "223-mutators#running-mutators", + "id": "228-mutators#running-mutators", "title": "Mutators", "searchTitle": "Running Mutators", "sectionTitle": "Running Mutators", "sectionId": "running-mutators", "url": "/docs/mutators", - "content": "Once you have registered your mutators, you can invoke them with zero.mutate: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: crypto.randomUUID(), title: 'New title' }) ) Client-generated random IDs from crypto.randomUUID(), uuid, ulid, or nanoid work much better with sync engines like Zero. See IDs for more details. Waiting for Results We typically recommend that you \"fire and forget\" mutators. Optimistic mutations make sense when the common case is that a mutation succeeds. If a mutation frequently fails, then showing the user an optimistic result isn't very useful, because it will likely be wrong. That said there are cases where it is nice to know when a write succeeded on either the client or server. One example is if you need to read a row directly after writing it. Zero's local writes are very fast (almost always < 1 frame), but because Zero is backed by IndexedDB, writes are still technically asynchronous and reads directly after a write may not return the new data. You can use the .client promise in this case to wait for a write to complete on the client side: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) // issue-123 not guaranteed to be present here. read1 may be undefined. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await client write – almost always less than 1 frame, and same // macrotask, so no browser paint will occur here. const res = await write.client if (res.type === 'error') { console.error('Mutator failed on client', res.error) } // issue-123 definitely can be read now. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) You can also await .server for the server result: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) const clientRes = await write.client if (clientRes.type === 'error') { throw new Error(`Mutator failed on client`, { cause: clientRes.error }) } // optimistic write guaranteed to be present here, but not // server write. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await the server result/acknowledgment. This requires a round trip. const serverRes = await write.server if (serverRes.type === 'error') { throw new Error(`Mutator failed on server`, { cause: serverRes.error }) } // The server acknowledged the mutation, but its Postgres changes // may not have replicated to this client yet. This read can still // reflect optimistic rather than authoritative state. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) If the client-side mutator fails, .server also resolves to an error result. Awaiting .server therefore covers both client- and server-side failures. There is not yet a way to return data from mutators in the success case. Let us know if you need this.", + "content": "Once you have registered your mutators, you can invoke them with zero.mutate: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: crypto.randomUUID(), title: 'New title' }) ) Client-generated random IDs from crypto.randomUUID(), uuid, ulid, or nanoid work much better with sync engines like Zero. See IDs for more details. Waiting for Results We typically recommend that you \"fire and forget\" mutators. Optimistic mutations make sense when the common case is that a mutation succeeds. If a mutation frequently fails, then showing the user an optimistic result isn't very useful, because it will likely be wrong. That said there are cases where it is nice to know when a write succeeded on either the client or server. One example is if you need to read a row directly after writing it. Zero's local writes are very fast (almost always < 1 frame), but because Zero is backed by IndexedDB, writes are still technically asynchronous and reads directly after a write may not return the new data. You can use the .client promise in this case to wait for a write to complete on the client side: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) // issue-123 not guaranteed to be present here. read1 may be undefined. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await client write – almost always less than 1 frame, and same // macrotask, so no browser paint will occur here. const res = await write.client if (res.type === 'error') { console.error('Mutator failed on client', res.error) } // issue-123 definitely can be read now. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) You can also await .server for the server result: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) const clientRes = await write.client if (clientRes.type === 'error') { throw new Error(`Mutator failed on client`, { cause: clientRes.error }) } // optimistic write guaranteed to be present here, but not // server write. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await the server result/acknowledgment. This requires a round trip. const serverRes = await write.server if (serverRes.type === 'error') { throw new Error(`Mutator failed on server`, { cause: serverRes.error }) } // The server acknowledged the mutation, but its Postgres changes // may not have replicated to this client yet. This read can still // reflect optimistic rather than authoritative state. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) If the client-side mutator fails, .server also resolves to an error result. Awaiting .server therefore covers both client- and server-side failures. Returning Data A mutator can return a JSON value. The caller gets it in the data field of the success result. The client and server runs can return different values. For example, the server can return a value that only the database knows: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.insert({id, title}) if (tx.location === 'client') { // The client doesn't know the short ID yet. return {shortID: null} } // Postgres assigns shortID when the row is inserted. const issue = await tx.run( zql.issue.where('id', id).one() ) return {shortID: issue?.shortID ?? null} } ) The data of the .client result is what the client run returned. The data of the .server result is what the server run returned: const write = zero.mutate( mutators.issue.create({ id: crypto.randomUUID(), title: 'New title' }) ) const serverRes = await write.server if (serverRes.type === 'success') { // `data` is a plain JSON value, so check its shape. const {shortID} = z .object({shortID: z.number().nullable()}) .parse(serverRes.data) console.log('Short ID:', shortID) } A few things to know: data is typed as ReadonlyJSONValue | undefined, not as the mutator's return type. Check its shape before you use it. If the mutator returns nothing, data is undefined. The server value only reaches the client if your mutate handler returns the mutator's result, as return mutator.fn({args, tx}) does in the examples above. Zero saves the server's return value in your database along with the mutation's result, so the client still gets it if the response from your mutate endpoint is lost. A mutator that returns nothing doesn't write this.", "kind": "section" }, { - "id": "224-mutators#waiting-for-results", + "id": "229-mutators#waiting-for-results", "title": "Mutators", "searchTitle": "Waiting for Results", "sectionTitle": "Waiting for Results", "sectionId": "waiting-for-results", "url": "/docs/mutators", - "content": "We typically recommend that you \"fire and forget\" mutators. Optimistic mutations make sense when the common case is that a mutation succeeds. If a mutation frequently fails, then showing the user an optimistic result isn't very useful, because it will likely be wrong. That said there are cases where it is nice to know when a write succeeded on either the client or server. One example is if you need to read a row directly after writing it. Zero's local writes are very fast (almost always < 1 frame), but because Zero is backed by IndexedDB, writes are still technically asynchronous and reads directly after a write may not return the new data. You can use the .client promise in this case to wait for a write to complete on the client side: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) // issue-123 not guaranteed to be present here. read1 may be undefined. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await client write – almost always less than 1 frame, and same // macrotask, so no browser paint will occur here. const res = await write.client if (res.type === 'error') { console.error('Mutator failed on client', res.error) } // issue-123 definitely can be read now. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) You can also await .server for the server result: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) const clientRes = await write.client if (clientRes.type === 'error') { throw new Error(`Mutator failed on client`, { cause: clientRes.error }) } // optimistic write guaranteed to be present here, but not // server write. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await the server result/acknowledgment. This requires a round trip. const serverRes = await write.server if (serverRes.type === 'error') { throw new Error(`Mutator failed on server`, { cause: serverRes.error }) } // The server acknowledged the mutation, but its Postgres changes // may not have replicated to this client yet. This read can still // reflect optimistic rather than authoritative state. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) If the client-side mutator fails, .server also resolves to an error result. Awaiting .server therefore covers both client- and server-side failures. There is not yet a way to return data from mutators in the success case. Let us know if you need this.", + "content": "We typically recommend that you \"fire and forget\" mutators. Optimistic mutations make sense when the common case is that a mutation succeeds. If a mutation frequently fails, then showing the user an optimistic result isn't very useful, because it will likely be wrong. That said there are cases where it is nice to know when a write succeeded on either the client or server. One example is if you need to read a row directly after writing it. Zero's local writes are very fast (almost always < 1 frame), but because Zero is backed by IndexedDB, writes are still technically asynchronous and reads directly after a write may not return the new data. You can use the .client promise in this case to wait for a write to complete on the client side: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) // issue-123 not guaranteed to be present here. read1 may be undefined. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await client write – almost always less than 1 frame, and same // macrotask, so no browser paint will occur here. const res = await write.client if (res.type === 'error') { console.error('Mutator failed on client', res.error) } // issue-123 definitely can be read now. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) You can also await .server for the server result: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) const clientRes = await write.client if (clientRes.type === 'error') { throw new Error(`Mutator failed on client`, { cause: clientRes.error }) } // optimistic write guaranteed to be present here, but not // server write. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await the server result/acknowledgment. This requires a round trip. const serverRes = await write.server if (serverRes.type === 'error') { throw new Error(`Mutator failed on server`, { cause: serverRes.error }) } // The server acknowledged the mutation, but its Postgres changes // may not have replicated to this client yet. This read can still // reflect optimistic rather than authoritative state. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) If the client-side mutator fails, .server also resolves to an error result. Awaiting .server therefore covers both client- and server-side failures.", "kind": "section" }, { - "id": "225-mutators#permissions", + "id": "230-mutators#returning-data", + "title": "Mutators", + "searchTitle": "Returning Data", + "sectionTitle": "Returning Data", + "sectionId": "returning-data", + "url": "/docs/mutators", + "content": "A mutator can return a JSON value. The caller gets it in the data field of the success result. The client and server runs can return different values. For example, the server can return a value that only the database knows: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.insert({id, title}) if (tx.location === 'client') { // The client doesn't know the short ID yet. return {shortID: null} } // Postgres assigns shortID when the row is inserted. const issue = await tx.run( zql.issue.where('id', id).one() ) return {shortID: issue?.shortID ?? null} } ) The data of the .client result is what the client run returned. The data of the .server result is what the server run returned: const write = zero.mutate( mutators.issue.create({ id: crypto.randomUUID(), title: 'New title' }) ) const serverRes = await write.server if (serverRes.type === 'success') { // `data` is a plain JSON value, so check its shape. const {shortID} = z .object({shortID: z.number().nullable()}) .parse(serverRes.data) console.log('Short ID:', shortID) } A few things to know: data is typed as ReadonlyJSONValue | undefined, not as the mutator's return type. Check its shape before you use it. If the mutator returns nothing, data is undefined. The server value only reaches the client if your mutate handler returns the mutator's result, as return mutator.fn({args, tx}) does in the examples above. Zero saves the server's return value in your database along with the mutation's result, so the client still gets it if the response from your mutate endpoint is lost. A mutator that returns nothing doesn't write this.", + "kind": "section" + }, + { + "id": "231-mutators#permissions", "title": "Mutators", "searchTitle": "Permissions", "sectionTitle": "Permissions", @@ -2260,7 +2330,7 @@ "kind": "section" }, { - "id": "226-mutators#dropping-down-to-raw-sql", + "id": "232-mutators#dropping-down-to-raw-sql", "title": "Mutators", "searchTitle": "Dropping Down to Raw SQL", "sectionTitle": "Dropping Down to Raw SQL", @@ -2270,7 +2340,7 @@ "kind": "section" }, { - "id": "227-mutators#notifications-and-async-work", + "id": "233-mutators#notifications-and-async-work", "title": "Mutators", "searchTitle": "Notifications and Async Work", "sectionTitle": "Notifications and Async Work", @@ -2280,7 +2350,7 @@ "kind": "section" }, { - "id": "228-mutators#custom-mutate-implementation", + "id": "234-mutators#custom-mutate-implementation", "title": "Mutators", "searchTitle": "Custom Mutate Implementation", "sectionTitle": "Custom Mutate Implementation", @@ -2304,7 +2374,7 @@ "kind": "page" }, { - "id": "229-open-source#business-model", + "id": "235-open-source#business-model", "title": "Zero is Open Source Software", "searchTitle": "Business Model", "sectionTitle": "Business Model", @@ -2356,7 +2426,7 @@ "kind": "page" }, { - "id": "230-otel#grafana-cloud-walkthrough", + "id": "236-otel#grafana-cloud-walkthrough", "title": "OpenTelemetry", "searchTitle": "Grafana Cloud Walkthrough", "sectionTitle": "Grafana Cloud Walkthrough", @@ -2366,7 +2436,7 @@ "kind": "section" }, { - "id": "231-otel#distributed-tracing", + "id": "237-otel#distributed-tracing", "title": "OpenTelemetry", "searchTitle": "Distributed Tracing", "sectionTitle": "Distributed Tracing", @@ -2376,7 +2446,7 @@ "kind": "section" }, { - "id": "232-otel#metrics-reference", + "id": "238-otel#metrics-reference", "title": "OpenTelemetry", "searchTitle": "Metrics Reference", "sectionTitle": "Metrics Reference", @@ -2386,7 +2456,7 @@ "kind": "section" }, { - "id": "233-otel#zeroserver", + "id": "239-otel#zeroserver", "title": "OpenTelemetry", "searchTitle": "zero.server", "sectionTitle": "zero.server", @@ -2396,7 +2466,7 @@ "kind": "section" }, { - "id": "234-otel#zeroreplica", + "id": "240-otel#zeroreplica", "title": "OpenTelemetry", "searchTitle": "zero.replica", "sectionTitle": "zero.replica", @@ -2406,7 +2476,7 @@ "kind": "section" }, { - "id": "235-otel#zeroreplication", + "id": "241-otel#zeroreplication", "title": "OpenTelemetry", "searchTitle": "zero.replication", "sectionTitle": "zero.replication", @@ -2416,7 +2486,7 @@ "kind": "section" }, { - "id": "236-otel#zerosync", + "id": "242-otel#zerosync", "title": "OpenTelemetry", "searchTitle": "zero.sync", "sectionTitle": "zero.sync", @@ -2426,7 +2496,7 @@ "kind": "section" }, { - "id": "237-otel#zeromutation", + "id": "243-otel#zeromutation", "title": "OpenTelemetry", "searchTitle": "zero.mutation", "sectionTitle": "zero.mutation", @@ -2486,7 +2556,7 @@ "kind": "page" }, { - "id": "238-postgres-support#object-names", + "id": "244-postgres-support#object-names", "title": "Supported Postgres Features", "searchTitle": "Object Names", "sectionTitle": "Object Names", @@ -2496,7 +2566,7 @@ "kind": "section" }, { - "id": "239-postgres-support#object-types", + "id": "245-postgres-support#object-types", "title": "Supported Postgres Features", "searchTitle": "Object Types", "sectionTitle": "Object Types", @@ -2506,7 +2576,7 @@ "kind": "section" }, { - "id": "240-postgres-support#column-types", + "id": "246-postgres-support#column-types", "title": "Supported Postgres Features", "searchTitle": "Column Types", "sectionTitle": "Column Types", @@ -2516,7 +2586,7 @@ "kind": "section" }, { - "id": "241-postgres-support#column-defaults", + "id": "247-postgres-support#column-defaults", "title": "Supported Postgres Features", "searchTitle": "Column Defaults", "sectionTitle": "Column Defaults", @@ -2526,7 +2596,7 @@ "kind": "section" }, { - "id": "242-postgres-support#ids", + "id": "248-postgres-support#ids", "title": "Supported Postgres Features", "searchTitle": "IDs", "sectionTitle": "IDs", @@ -2536,7 +2606,7 @@ "kind": "section" }, { - "id": "243-postgres-support#primary-keys", + "id": "249-postgres-support#primary-keys", "title": "Supported Postgres Features", "searchTitle": "Primary Keys", "sectionTitle": "Primary Keys", @@ -2546,7 +2616,7 @@ "kind": "section" }, { - "id": "244-postgres-support#limiting-replication", + "id": "250-postgres-support#limiting-replication", "title": "Supported Postgres Features", "searchTitle": "Limiting Replication", "sectionTitle": "Limiting Replication", @@ -2556,7 +2626,7 @@ "kind": "section" }, { - "id": "245-postgres-support#zero-cache-replication", + "id": "251-postgres-support#zero-cache-replication", "title": "Supported Postgres Features", "searchTitle": "zero-cache replication", "sectionTitle": "zero-cache replication", @@ -2566,7 +2636,7 @@ "kind": "section" }, { - "id": "246-postgres-support#browser-client-replication", + "id": "252-postgres-support#browser-client-replication", "title": "Supported Postgres Features", "searchTitle": "Browser client replication", "sectionTitle": "Browser client replication", @@ -2576,7 +2646,7 @@ "kind": "section" }, { - "id": "247-postgres-support#schema-changes", + "id": "253-postgres-support#schema-changes", "title": "Supported Postgres Features", "searchTitle": "Schema changes", "sectionTitle": "Schema changes", @@ -2612,7 +2682,7 @@ "kind": "page" }, { - "id": "248-previews#overview", + "id": "254-previews#overview", "title": "Previews", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -2622,7 +2692,7 @@ "kind": "section" }, { - "id": "249-previews#configure-allowed-endpoint-patterns", + "id": "255-previews#configure-allowed-endpoint-patterns", "title": "Previews", "searchTitle": "Configure Allowed Endpoint Patterns", "sectionTitle": "Configure Allowed Endpoint Patterns", @@ -2632,7 +2702,7 @@ "kind": "section" }, { - "id": "250-previews#choose-endpoint-urls-in-the-client", + "id": "256-previews#choose-endpoint-urls-in-the-client", "title": "Previews", "searchTitle": "Choose Endpoint URLs in the Client", "sectionTitle": "Choose Endpoint URLs in the Client", @@ -2642,7 +2712,7 @@ "kind": "section" }, { - "id": "251-previews#schema-changes-in-previews", + "id": "257-previews#schema-changes-in-previews", "title": "Previews", "searchTitle": "Schema Changes in Previews", "sectionTitle": "Schema Changes in Previews", @@ -2656,7 +2726,7 @@ "title": "Queries", "searchTitle": "Queries", "url": "/docs/queries", - "content": "Queries are how you read and sync data with Zero. Here's a simple example: // src/queries.ts import {defineQueries, defineQuery} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' export const queries = defineQueries({ postsByAuthor: defineQuery( z.object({authorID: z.string()}), ({args: {authorID}}) => zql.post.where('authorID', authorID) ) }) Architecture A copy of each query exists on both the client and on your server: Often the implementations will be the same, and you can just share their code. This is easy with full-stack frameworks like TanStack Start or Next.js. But the implementations don't have to be the same, or even compute the same result. For example, the server can add extra filters to enforce permissions that the client query does not. Life of a Query When a query is invoked, it initially runs on the client, against the client-side datastore. Any matching data is returned immediately and the user sees instant results. In the background, the name and arguments for the query are sent to zero-cache. Zero-cache calls the queries endpoint on your server to get the ZQL for the query. Your server looks up its implementation of the query, invokes it, and returns the resulting ZQL expression to zero-cache. Zero-cache then runs this ZQL against the server-side data. The initial server result is sent back to the client and the client query updates in response. zero-cache receives updates from Postgres via logical replication. It updates affected queries and sends row changes back to the client, which updates the client query, and the user sees the changes. Defining Queries Basics Create a query using defineQuery. The only required argument is a QueryFn, which must return a ZQL expression: import {zql} from 'schema.ts' const allPostsQueryDef = defineQuery(() => zql.post) Arguments The QueryFn can take a single args parameter. To enable this, pass a validator to defineQuery: import {zql} from 'schema.ts' const postsByAuthor = defineQuery( z.object({authorID: z.string().optional()}), ({args: {authorID}}) => { let q = zql.post if (authorID !== undefined) { q = q.where('authorID', authorID) } return q } ) We use Zod in these examples, but you can use any validation library that implements Standard Schema. Zero queries run on both the client and on your server. In the server case, the parameters come from the client and are untrusted. The validator ensures the data passed to your query is of the expected type. Query Registries The result of defineQuery is a QueryDefinition. By itself this isn't super useful. You need to register it using defineQueries: export const queries = defineQueries({ posts: { all: allPostsQueryDef } }) Typically these are done together in one step: export const queries = defineQueries({ posts: { all: defineQuery(() => zql.post) } }) The result of defineQueries is called a QueryRegistry. Each field in the registry is a callable Query that you can use to read data: import {zero} from 'zero.ts' import {queries} from 'queries.ts' const allPosts = await zero.run(queries.posts.all()) Query Names Each Query has a queryName which is computed by defineQueries. This name is later sent to your server to identify the query to run: console.log(queries.posts.all.queryName) // \"posts.all\" Context Query parameters are supplied by the client application and passed to the server automatically by Zero. This makes them unsuitable for credentials, since the user could modify them. For this reason, Zero queries also support the concept of a context object. Access your context with the ctx parameter to your query: const myPostsQuery = defineQuery(({ctx: {userID}}) => { // User cannot control context.userID, so this safely // restricts the query to the user's own posts. return zql.post.where('authorID', userID) }) If you don't want to register your Context and Schema types globally, you can use defineQueryWithType and defineQueriesWithType: import { defineQueriesWithType, defineQueryWithType } from '@rocicorp/zero' import type {Schema} from 'schema.ts' import type {ZeroContext} from 'context.ts' const defineQuery = defineQueryWithType< Schema, ZeroContext >() const defineQueries = defineQueriesWithType() queries.ts By convention, all queries for an application are listed in a central queries.ts file. This allows them to be easily used on both the client and server: import {defineQueries, defineQuery} from '@rocicorp/zero' import {z} from 'zod' import {zql} from './schema.ts' export const queries = defineQueries({ posts: { get: defineQuery(z.string(), id => zql.post.where('id', id) ), byAuthor: defineQuery( z.object({ authorID: z.string(), includeDrafts: z.boolean().optional() }), ({args: {authorID, includeDrafts}}) => { let q = zql.post.where('authorID', authorID) if (!includeDrafts) { q = q.where('isDraft', false) } return q } ) } }) You can use as many levels of nesting as you want to organize your queries. As your application grows, you can move queries to different files to keep them organized: // posts.ts export const postQueries = { get: defineQuery(z.string(), id => zql.post.where('id', id) ) // ... } // users.ts export const userQueries = { byRole: defineQuery(z.string(), role => zql.user.where('role', role) ) // ... } // queries.ts import {postQueries} from './posts.ts' import {userQueries} from './users.ts' export const queries = defineQueries({ posts: postQueries, users: userQueries }) Because defineQueries establishes the full name for each query (i.e., posts.get, users.byRole), it should only be used once at the top level of your queries.ts file. Server Setup In order for queries to sync, you must provide an implementation of the query endpoint on your server. zero-cache calls this endpoint to resolve each query to ZQL that it can run. Registering the Endpoint Use ZERO_QUERY_URL to tell zero-cache where to find your query implementation: export ZERO_QUERY_URL=\"http://localhost:3000/api/zero/query\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleQueryRequest and mustGetQuery functions to implement the endpoint. // src/routes/api/zero/query.ts import {createFileRoute} from '@tanstack/react-router' import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export const Route = createFileRoute('/api/zero/query')({ server: { handlers: { POST: async ({request}) => { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request, userID: null }) return Response.json(result) } } } })// app/api/zero/query/route.ts import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export async function POST(request: Request) { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request, userID: null }) return Response.json(result) }// src/routes/api/zero/query.ts import type {APIEvent} from '@solidjs/start/server' import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export async function POST(event: APIEvent) { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' app.post('/api/zero/query', async c => { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request: c.req.raw, userID: null }) return c.json(result) }) handleQueryRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetQuery looks up the query in the registry and throws an error if not found. The query.fn function is your query implementation wrapped in the validator you provided. These examples have only public queries, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the query handler. See Authentication. Custom Query URL By default, Zero sends queries to the URL specified in the ZERO_QUERY_URL parameter in the zero-cache config. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in ZERO_QUERY_URL: ZERO_QUERY_URL='https://api.example.com/query,https://api.staging.example.com/query' Then choose one of those URLs by passing it to queryURL on the Zero constructor: const zero = new Zero({ schema, queries, queryURL: 'https://api.staging.example.com/query' }) URL Patterns The strings listed in ZERO_QUERY_URL can also be URLPatterns: ZERO_QUERY_URL=\"https://mybranch-*.preview.myapp.com/query\" This queries URL will allow clients to choose URLs like: https://mybranch-aaa.preview.myapp.com/query ✅ https://mybranch-bbb.preview.myapp.com/query ✅ But rejects URLs like: https://preview.myapp.com/query ❌ (missing subdomain) https://malicious.com/query ❌ (different domain) https://mybranch-123.preview.myapp.com/query/extra ❌ (extra path) https://mybranch-123.preview.myapp.com/other ❌ (different path) Because URLPattern is a web standard, you can test them right in your browser: For more information, see the URLPattern docs. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Running Queries Reactively The most common way to use queries is with the useQuery reactive hooks from the React or SolidJS bindings (or the equivalent low-level API): import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(queries.posts.get('user123')) return posts.map(post => (
{post.title}
)) }import {useQuery} from '@rocicorp/zero/solid' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(() => queries.posts.get('user123') ) return ( {post =>
{post.title}
}
) }import {queries} from 'zero/queries.ts' import {zero} from 'zero.ts' const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) for (let post of postsView.data) { console.log(post.title) } // updates as the underlying data changes postsView.addListener(posts => { console.log('posts', posts) }) These functions allow you to automatically re-render UI when a query changes. Conditionally Sometimes the inputs needed to construct a query are not available on the first render. For example, auth state or a route param might still be loading after a page refresh. Both React and Solid support conditional queries by passing undefined until the query can be constructed: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function Username({userID}: {userID: string | undefined}) { const [user] = useQuery( userID ? queries.users.getUser({ userID }) : undefined ) return user ?
{user.username}
: null }import {useQuery} from '@rocicorp/zero/solid' import {Show} from 'solid-js' import {queries} from 'zero/queries.ts' function Username(props: {userID: string | undefined}) { const [user] = useQuery(() => props.userID ? queries.users.getUser({ userID: props.userID }) : undefined ) return ( {user =>
{user().username}
}
) } Once You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} ) For Preloading Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) ) Missing Data Because Zero returns local results immediately and server results asynchronously, displaying \"not found\" / 404 UI can be slightly tricky. If you just use a simple existence check, you will often see the 404 UI flicker while the server result loads: const [issue] = useQuery(queries.issues.get('some-id')) // ❌ This causes flickering of the UI if (!issue) { return
404 Not Found
} else { return
{issue.title}
}const [issue] = useQuery(() => queries.issues.get('some-id') ) return ( {resolved => ( 404 Not Found} >
{resolved.title}
)}
)const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) postsView.addListener(posts => { // ❌ This is updated as data comes in console.log('posts', posts) }) To do this correctly, only display the \"not found\" UI when the result type is complete. This way the 404 page is slow but pages with data are still just as fast: const [issue, issueResult] = useQuery( queries.issues.get('some-id') ) if (!issue && issueResult.type === 'complete') { return
404 Not Found
} if (!issue) { return null } return
{issue.title}
const [issue, issueResult] = useQuery(() => queries.issues.get('some-id') ) return ( {resolved =>
{resolved.title}
}
404 Not Found
)const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) postsView.addListener((posts, resultType) => { if (resultType === 'complete') { console.log('posts', posts) } }) Partial Data Zero immediately returns the data for a query it has on the client, then falls back to the server for any missing data. Sometimes it's useful to know the difference between these two types of results. To do so, use the result from useQuery: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) if (issuesResult.type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) if (issuesResult().type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if (resultType === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') } }) The possible values of result.type are currently complete and unknown. The complete value is currently only returned when Zero has received the server result. In the future, Zero will be able to return this result type when it knows that all possible data for this query is already available locally. Additionally, we plan to add a prefix result for when the data is known to be a prefix of the complete result. See Consistency for more information. Handling Errors If the queries endpoint throws an application or parse error, zero-cache will report it to the client using the type and error fields on the query details object: const [posts, postsResult] = useQuery( queries.posts.byAuthorID('user123') ) if (postsResult.type === 'error') { return (
Error loading posts: {postsResult.error.message}
) }const [posts, postsResult] = useQuery(() => queries.posts.byAuthorID('user123') ) return (
Error loading posts: {postsResult().error.message}
)// Materialize a view of a query const postsView = queries.posts .byAuthorID('user123') .materialize() postsView.addListener((posts, resultType, error) => { if (resultType === 'error') { console.error('Error loading posts', error) } }) See Connection Status for how HTTP or network errors from the queries endpoint are handled. Granular Updates You can use the materialize() method to create a view that you can listen to for changes. However, this will only tell you when the view has changed and give you the complete new result. It won't tell you what changed. To know what changed, you can create your own custom View implementation: // Inside the View class // Instead of storing the change, we invoke some callback push(change: Change): void { switch (change.type) { case 'add': this.#onAdd?.(change) break case 'remove': this.#onRemove?.(change) break case 'edit': this.#onEdit?.(change) break case 'child': this.#onChild?.(change) break default: throw new Error(`Unknown change type: ${change['type']}`) } } For examples, see the View implementations in zero-vue or zero-solid. Query Caching Queries can be either active or cached. An active query is one that is currently being used by the application. Cached queries are not currently in use, but continue syncing in case they are needed again soon. Queries are deactivated according to how they were created: For useQuery(), the UI unmounts the component (which calls destroy() under the covers). For preload(), the UI calls cleanup() on the return value of preload(). For run(), queries are automatically deactivated immediately after the result is returned. For materialize() queries, the UI calls destroy() on the view. Additionally when a Zero instance closes, all active queries are automatically deactivated. This also happens when the containing page or script is unloaded. TTLs Each query has a ttl that controls how long it stays cached. If the user closes all tabs for your app, Zero stops running and the time that elapses doesn't count toward any TTLs. You do not need to account for such time when choosing a TTL – you only need to account for time your app is running without a query. TTL Defaults In most cases, the default TTL should work well: preload() queries default to ttl:'none', meaning they are not cached at all, and will stop syncing immediately when deactivated. But because preload() queries are typically registered at app startup and never shutdown, and because the ttl clock only ticks while Zero is running, this means that preload queries never get unregistered. Other queries have a default ttl of 5m (five minutes). Setting Different TTLs You can override the default TTL with the ttl parameter: const [user] = useQuery( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero.preload(queries.posts.byAuthorID('user123'), { ttl: '5m' })const [user] = useQuery( () => queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero().preload(queries.posts.byAuthorID('user123'), { ttl: '5m' })// run() const user = await zero.run( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // materialize() const view = zero.materialize( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero.preload(queries.posts.byAuthorID('user123'), { ttl: '5m' }) TTLs up to 10m (ten minutes) are currently supported. The following formats are allowed: Why Zero TTLs are Short Zero queries are not free. Just as in any database, queries consume resources on both the client and server. Memory is used to keep metadata about the query, and disk storage is used to keep the query's current state. We do drop this state after we haven't heard from a client for awhile, but this is only a partial improvement. If the client returns, we have to re-run the query to get the latest data. This means that we do not actually want to keep queries active unless there is a good chance they will be needed again soon. The default Zero TTL values might initially seem too short, but they are designed to work well with the way Zero's TTL clock works and strike a good balance between keeping queries alive long enough to be useful, while not keeping them alive so long that they consume resources unnecessarily. Local-Only Queries It can sometimes be useful to run queries only on the client. For example, to implement typeahead search, it really doesn't make sense to register a query with the server for every single keystroke. Zero doesn't yet have a way to run named queries local-only, but you can run ZQL expressions locally by passing them anywhere a query is supported. For example, to subscribe to a local-only query: // Queries the already synced data for issues, // without syncing more data. const [issues] = useQuery( zql.issue.orderBy('created', 'desc').limit(10) )// Queries the already synced data for issues, // without syncing more data. const [issues] = useQuery(() => zql.issue.orderBy('created', 'desc').limit(10) )// Queries the already synced data for issues, // without syncing more data. const view = z.materialize( zql.issue.orderBy('created', 'desc').limit(10) ) view.addListener(issues => { console.log('issues', issues) }) Custom Server Implementation It is possible to implement the ZERO_QUERY_URL endpoint without using Zero's TypeScript libraries, or even in a different language entirely. The endpoint receives a POST request with a JSON body of the form: type QueriesRequestBody = { id: string name: string args: readonly ReadonlyJSONValue[] }[] And responds with: type QueriesResponseBody = ( | { id: string name: string // See https://github.com/rocicorp/mono/blob/main/packages/zero-protocol/src/ast.ts ast: AST } | { error: 'app' id: string name: string details: ReadonlyJSONValue } | { error: 'zero' id: string name: string details: ReadonlyJSONValue } | { error: 'http' id: string name: string status: number details: ReadonlyJSONValue } )[] Consistency Zero always syncs a consistent partial replica of the backend database to the client. This avoids many common consistency issues that come up in classic web applications. But there are still some consistency issues to be aware of when using Zero. For example, imagine that you have a bug database w/ 10k issues. You preload the first 1k issues sorted by created. The user then does a query of issues assigned to themselves, sorted by created. Among the 1k issues that were preloaded imagine 100 are found that match the query. Since the data we preloaded is in the same order as this query, we are guaranteed that any local results found will be a prefix of the server results. The UX that result is nice: the user will see initial results to the query instantly. If more results are found server-side, those results are guaranteed to sort below the local results. There's no shuffling of results when the server response comes in. Now imagine that the user switches the sort to ‘sort by modified’. This new query will run locally, and will again find some local matches. But it is now unlikely that the local results found are a prefix of the server results. When the server result comes in, the user will probably see the results shuffle around. To avoid this annoying effect, what you should do in this example is also preload the first 1k issues sorted by modified desc. In general for any query shape you intend to do, you should preload the first n results for that query shape with no filters, in each sort you intend to use. Zero syncs the union of all active queries' results. You don't have to worry about syncing many sorts of the same query when it's likely the results will overlap heavily. In the future, we will be implementing a consistency model that fixes these issues automatically. We will prevent Zero from returning local data when that data is not known to be a prefix of the server result. Once the consistency model is implemented, preloading can be thought of as purely a performance thing, and not required to avoid unsightly flickering.", + "content": "Queries are how you read and sync data with Zero. Here's a simple example: // src/queries.ts import {defineQueries, defineQuery} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' export const queries = defineQueries({ postsByAuthor: defineQuery( z.object({authorID: z.string()}), ({args: {authorID}}) => zql.post.where('authorID', authorID) ) }) Architecture A copy of each query exists on both the client and on your server: Often the implementations will be the same, and you can just share their code. This is easy with full-stack frameworks like TanStack Start or Next.js. But the implementations don't have to be the same, or even compute the same result. For example, the server can add extra filters to enforce permissions that the client query does not. Life of a Query When a query is invoked, it initially runs on the client, against the client-side datastore. Any matching data is returned immediately and the user sees instant results. In the background, the name and arguments for the query are sent to zero-cache. Zero-cache calls the queries endpoint on your server to get the ZQL for the query. Your server looks up its implementation of the query, invokes it, and returns the resulting ZQL expression to zero-cache. Zero-cache then runs this ZQL against the server-side data. The initial server result is sent back to the client and the client query updates in response. zero-cache receives updates from Postgres via logical replication. It updates affected queries and sends row changes back to the client, which updates the client query, and the user sees the changes. Defining Queries Basics Create a query using defineQuery. The only required argument is a QueryFn, which must return a ZQL expression: import {zql} from 'schema.ts' const allPostsQueryDef = defineQuery(() => zql.post) Arguments The QueryFn can take a single args parameter. To enable this, pass a validator to defineQuery: import {zql} from 'schema.ts' const postsByAuthor = defineQuery( z.object({authorID: z.string().optional()}), ({args: {authorID}}) => { let q = zql.post if (authorID !== undefined) { q = q.where('authorID', authorID) } return q } ) We use Zod in these examples, but you can use any validation library that implements Standard Schema. Zero queries run on both the client and on your server. In the server case, the parameters come from the client and are untrusted. The validator ensures the data passed to your query is of the expected type. Query Registries The result of defineQuery is a QueryDefinition. By itself this isn't super useful. You need to register it using defineQueries: export const queries = defineQueries({ posts: { all: allPostsQueryDef } }) Typically these are done together in one step: export const queries = defineQueries({ posts: { all: defineQuery(() => zql.post) } }) The result of defineQueries is called a QueryRegistry. Each field in the registry is a callable Query that you can use to read data: import {zero} from 'zero.ts' import {queries} from 'queries.ts' const allPosts = await zero.run(queries.posts.all()) Query Names Each Query has a queryName which is computed by defineQueries. This name is later sent to your server to identify the query to run: console.log(queries.posts.all.queryName) // \"posts.all\" Context Query parameters are supplied by the client application and passed to the server automatically by Zero. This makes them unsuitable for credentials, since the user could modify them. For this reason, Zero queries also support the concept of a context object. Access your context with the ctx parameter to your query: const myPostsQuery = defineQuery(({ctx: {userID}}) => { // User cannot control context.userID, so this safely // restricts the query to the user's own posts. return zql.post.where('authorID', userID) }) If you don't want to register your Context and Schema types globally, you can use defineQueryWithType and defineQueriesWithType: import { defineQueriesWithType, defineQueryWithType } from '@rocicorp/zero' import type {Schema} from 'schema.ts' import type {ZeroContext} from 'context.ts' const defineQuery = defineQueryWithType< Schema, ZeroContext >() const defineQueries = defineQueriesWithType() queries.ts By convention, all queries for an application are listed in a central queries.ts file. This allows them to be easily used on both the client and server: import {defineQueries, defineQuery} from '@rocicorp/zero' import {z} from 'zod' import {zql} from './schema.ts' export const queries = defineQueries({ posts: { get: defineQuery(z.string(), id => zql.post.where('id', id) ), byAuthor: defineQuery( z.object({ authorID: z.string(), includeDrafts: z.boolean().optional() }), ({args: {authorID, includeDrafts}}) => { let q = zql.post.where('authorID', authorID) if (!includeDrafts) { q = q.where('isDraft', false) } return q } ) } }) You can use as many levels of nesting as you want to organize your queries. As your application grows, you can move queries to different files to keep them organized: // posts.ts export const postQueries = { get: defineQuery(z.string(), id => zql.post.where('id', id) ) // ... } // users.ts export const userQueries = { byRole: defineQuery(z.string(), role => zql.user.where('role', role) ) // ... } // queries.ts import {postQueries} from './posts.ts' import {userQueries} from './users.ts' export const queries = defineQueries({ posts: postQueries, users: userQueries }) Because defineQueries establishes the full name for each query (i.e., posts.get, users.byRole), it should only be used once at the top level of your queries.ts file. Server Setup In order for queries to sync, you must provide an implementation of the query endpoint on your server. zero-cache calls this endpoint to resolve each query to ZQL that it can run. Registering the Endpoint Use ZERO_QUERY_URL to tell zero-cache where to find your query implementation: export ZERO_QUERY_URL=\"http://localhost:3000/api/zero/query\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleQueryRequest and mustGetQuery functions to implement the endpoint. // src/routes/api/zero/query.ts import {createFileRoute} from '@tanstack/react-router' import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export const Route = createFileRoute('/api/zero/query')({ server: { handlers: { POST: async ({request}) => { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request, userID: null }) return Response.json(result) } } } })// app/api/zero/query/route.ts import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export async function POST(request: Request) { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request, userID: null }) return Response.json(result) }// src/routes/api/zero/query.ts import type {APIEvent} from '@solidjs/start/server' import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export async function POST(event: APIEvent) { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' app.post('/api/zero/query', async c => { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request: c.req.raw, userID: null }) return c.json(result) }) handleQueryRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetQuery looks up the query in the registry and throws an error if not found. The query.fn function is your query implementation wrapped in the validator you provided. These examples have only public queries, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the query handler. See Authentication. Custom Query URL By default, Zero sends queries to the URL specified in the ZERO_QUERY_URL parameter in the zero-cache config. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in ZERO_QUERY_URL: ZERO_QUERY_URL='https://api.example.com/query,https://api.staging.example.com/query' Then choose one of those URLs by passing it to queryURL on the Zero constructor: const zero = new Zero({ schema, queries, queryURL: 'https://api.staging.example.com/query' }) URL Patterns The strings listed in ZERO_QUERY_URL can also be URLPatterns: ZERO_QUERY_URL=\"https://mybranch-*.preview.myapp.com/query\" This queries URL will allow clients to choose URLs like: https://mybranch-aaa.preview.myapp.com/query ✅ https://mybranch-bbb.preview.myapp.com/query ✅ But rejects URLs like: https://preview.myapp.com/query ❌ (missing subdomain) https://malicious.com/query ❌ (different domain) https://mybranch-123.preview.myapp.com/query/extra ❌ (extra path) https://mybranch-123.preview.myapp.com/other ❌ (different path) Because URLPattern is a web standard, you can test them right in your browser: For more information, see the URLPattern docs. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Running Queries Reactively The most common way to use queries is with the useQuery reactive hooks from the React or SolidJS bindings (or the equivalent low-level API): import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(queries.posts.get('user123')) return posts.map(post => (
{post.title}
)) }import {useQuery} from '@rocicorp/zero/solid' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(() => queries.posts.get('user123') ) return ( {post =>
{post.title}
}
) }import {queries} from 'zero/queries.ts' import {zero} from 'zero.ts' const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) for (let post of postsView.data) { console.log(post.title) } // updates as the underlying data changes postsView.addListener(posts => { console.log('posts', posts) }) These functions allow you to automatically re-render UI when a query changes. Conditionally Sometimes the inputs needed to construct a query are not available on the first render. For example, auth state or a route param might still be loading after a page refresh. Both React and Solid support conditional queries by passing undefined until the query can be constructed: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function Username({userID}: {userID: string | undefined}) { const [user] = useQuery( userID ? queries.users.getUser({ userID }) : undefined ) return user ?
{user.username}
: null }import {useQuery} from '@rocicorp/zero/solid' import {Show} from 'solid-js' import {queries} from 'zero/queries.ts' function Username(props: {userID: string | undefined}) { const [user] = useQuery(() => props.userID ? queries.users.getUser({ userID: props.userID }) : undefined ) return ( {user =>
{user().username}
}
) } Once You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} ) To also accept a result the server confirmed during a previous session, pass {type: 'cached'}. It resolves as soon as the result is cached or complete, so it doesn't wait for the server when the client already stores a confirmed result: const results = await zero.run( queries.issues.byPriority('high'), {type: 'cached'} ) For Preloading Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) ) preload() returns cleanup, which ends the preload (Zero keeps its rows for the query's TTL), and two promises: complete resolves once the server has returned the result, and cached resolves once the result is cached or complete. Missing Data Because Zero returns local results immediately and server results asynchronously, displaying \"not found\" / 404 UI can be slightly tricky. If you just use a simple existence check, you will often see the 404 UI flicker while the server result loads: const [issue] = useQuery(queries.issues.get('some-id')) // ❌ This causes flickering of the UI if (!issue) { return
404 Not Found
} else { return
{issue.title}
}const [issue] = useQuery(() => queries.issues.get('some-id') ) return ( {resolved => ( 404 Not Found} >
{resolved.title}
)}
)const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) postsView.addListener(posts => { // ❌ This is updated as data comes in console.log('posts', posts) }) To do this correctly, only display the \"not found\" UI when the result type is complete. This way the 404 page is slow but pages with data are still just as fast: const [issue, issueResult] = useQuery( queries.issues.get('some-id') ) if (!issue && issueResult.type === 'complete') { return
404 Not Found
} if (!issue) { return null } return
{issue.title}
const [issue, issueResult] = useQuery(() => queries.issues.get('some-id') ) return ( {resolved =>
{resolved.title}
}
404 Not Found
)const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) postsView.addListener((posts, resultType) => { if (resultType === 'complete') { console.log('posts', posts) } }) Partial Data Zero immediately returns the data for a query it has on the client, then falls back to the server for any missing data. Sometimes it's useful to know the difference between these two types of results. To do so, use the result from useQuery: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) if (issuesResult.type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) if (issuesResult().type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if (resultType === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') } }) The possible values of result.type are: unknown: the data Zero has on the client. Some of it may be missing. cached: the client stores a result that the server confirmed during a previous session. See Cached Results. complete: the server has returned the result during this session. error: the query failed. See Handling Errors. The complete value is currently only returned when Zero has received the server result. In the future, Zero will be able to return this result type when it knows that all possible data for this query is already available locally. Additionally, we plan to add a prefix result for when the data is known to be a prefix of the complete result. See Consistency for more information. Cached Results When Zero stores data on the client (in IndexedDB, or SQLite on React Native), it also remembers which query results the server confirmed. If the client still holds such a result when the app starts again, the query starts as cached instead of unknown, and becomes complete once the server returns the result in this session. If Zero evicts the stored result before then, the query goes back to unknown. A cached result was complete when the server sent it, but it may be out of date. Use it where a previous session's answer is good enough, for example to show stored data or an empty state without waiting for the server, which also works while offline. Wait for complete where the data must be current: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) const confirmed = issuesResult.type === 'cached' || issuesResult.type === 'complete' if (issues.length === 0 && confirmed) { return
Your inbox is empty
}const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) const confirmed = () => issuesResult().type === 'cached' || issuesResult().type === 'complete' return ( 0 || !confirmed()} fallback={
Your inbox is empty
} > {/* ... */}
)const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if ( issues.length === 0 && (resultType === 'cached' || resultType === 'complete') ) { console.log('Inbox is empty') } }) zero.run(query, {type: 'cached'}) and the cached promise from zero.preload() resolve on a cached or complete result. {type: 'complete'} and the complete promise still wait for the server. Handling Errors If the queries endpoint throws an application or parse error, zero-cache will report it to the client using the type and error fields on the query details object: const [posts, postsResult] = useQuery( queries.posts.byAuthorID('user123') ) if (postsResult.type === 'error') { return (
Error loading posts: {postsResult.error.message}
) }const [posts, postsResult] = useQuery(() => queries.posts.byAuthorID('user123') ) return (
Error loading posts: {postsResult().error.message}
)// Materialize a view of a query const postsView = queries.posts .byAuthorID('user123') .materialize() postsView.addListener((posts, resultType, error) => { if (resultType === 'error') { console.error('Error loading posts', error) } }) See Connection Status for how HTTP or network errors from the queries endpoint are handled. Granular Updates You can use the materialize() method to create a view that you can listen to for changes. However, this will only tell you when the view has changed and give you the complete new result. It won't tell you what changed. To know what changed, you can create your own custom View implementation: // Inside the View class // Instead of storing the change, we invoke some callback push(change: Change): void { switch (change.type) { case 'add': this.#onAdd?.(change) break case 'remove': this.#onRemove?.(change) break case 'edit': this.#onEdit?.(change) break case 'child': this.#onChild?.(change) break default: throw new Error(`Unknown change type: ${change['type']}`) } } For examples, see the View implementations in zero-vue or zero-solid. Query Caching Queries can be either active or cached. An active query is one that is currently being used by the application. Cached queries are not currently in use, but continue syncing in case they are needed again soon. Queries are deactivated according to how they were created: For useQuery(), the UI unmounts the component (which calls destroy() under the covers). For preload(), the UI calls cleanup() on the return value of preload(). For run(), queries are automatically deactivated immediately after the result is returned. For materialize() queries, the UI calls destroy() on the view. Additionally when a Zero instance closes, all active queries are automatically deactivated. This also happens when the containing page or script is unloaded. TTLs Each query has a ttl that controls how long it stays cached. If the user closes all tabs for your app, Zero stops running and the time that elapses doesn't count toward any TTLs. You do not need to account for such time when choosing a TTL – you only need to account for time your app is running without a query. TTL Defaults In most cases, the default TTL should work well: preload() queries default to ttl:'none', meaning they are not cached at all, and will stop syncing immediately when deactivated. But because preload() queries are typically registered at app startup and never shutdown, and because the ttl clock only ticks while Zero is running, this means that preload queries never get unregistered. Other queries have a default ttl of 5m (five minutes). Setting Different TTLs You can override the default TTL with the ttl parameter: const [user] = useQuery( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero.preload(queries.posts.byAuthorID('user123'), { ttl: '5m' })const [user] = useQuery( () => queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero().preload(queries.posts.byAuthorID('user123'), { ttl: '5m' })// run() const user = await zero.run( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // materialize() const view = zero.materialize( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero.preload(queries.posts.byAuthorID('user123'), { ttl: '5m' }) TTLs up to 10m (ten minutes) are currently supported. The following formats are allowed: Why Zero TTLs are Short Zero queries are not free. Just as in any database, queries consume resources on both the client and server. Memory is used to keep metadata about the query, and disk storage is used to keep the query's current state. We do drop this state after we haven't heard from a client for awhile, but this is only a partial improvement. If the client returns, we have to re-run the query to get the latest data. This means that we do not actually want to keep queries active unless there is a good chance they will be needed again soon. The default Zero TTL values might initially seem too short, but they are designed to work well with the way Zero's TTL clock works and strike a good balance between keeping queries alive long enough to be useful, while not keeping them alive so long that they consume resources unnecessarily. Local-Only Queries It can sometimes be useful to run queries only on the client. For example, to implement typeahead search, it really doesn't make sense to register a query with the server for every single keystroke. Zero doesn't yet have a way to run named queries local-only, but you can run ZQL expressions locally by passing them anywhere a query is supported. For example, to subscribe to a local-only query: // Queries the already synced data for issues, // without syncing more data. const [issues] = useQuery( zql.issue.orderBy('created', 'desc').limit(10) )// Queries the already synced data for issues, // without syncing more data. const [issues] = useQuery(() => zql.issue.orderBy('created', 'desc').limit(10) )// Queries the already synced data for issues, // without syncing more data. const view = z.materialize( zql.issue.orderBy('created', 'desc').limit(10) ) view.addListener(issues => { console.log('issues', issues) }) Custom Server Implementation It is possible to implement the ZERO_QUERY_URL endpoint without using Zero's TypeScript libraries, or even in a different language entirely. The endpoint receives a POST request with a JSON body of the form: type QueriesRequestBody = { id: string name: string args: readonly ReadonlyJSONValue[] }[] And responds with: type QueriesResponseBody = ( | { id: string name: string // See https://github.com/rocicorp/mono/blob/main/packages/zero-protocol/src/ast.ts ast: AST } | { error: 'app' id: string name: string details: ReadonlyJSONValue } | { error: 'zero' id: string name: string details: ReadonlyJSONValue } | { error: 'http' id: string name: string status: number details: ReadonlyJSONValue } )[] Consistency Zero always syncs a consistent partial replica of the backend database to the client. This avoids many common consistency issues that come up in classic web applications. But there are still some consistency issues to be aware of when using Zero. For example, imagine that you have a bug database w/ 10k issues. You preload the first 1k issues sorted by created. The user then does a query of issues assigned to themselves, sorted by created. Among the 1k issues that were preloaded imagine 100 are found that match the query. Since the data we preloaded is in the same order as this query, we are guaranteed that any local results found will be a prefix of the server results. The UX that result is nice: the user will see initial results to the query instantly. If more results are found server-side, those results are guaranteed to sort below the local results. There's no shuffling of results when the server response comes in. Now imagine that the user switches the sort to ‘sort by modified’. This new query will run locally, and will again find some local matches. But it is now unlikely that the local results found are a prefix of the server results. When the server result comes in, the user will probably see the results shuffle around. To avoid this annoying effect, what you should do in this example is also preload the first 1k issues sorted by modified desc. In general for any query shape you intend to do, you should preload the first n results for that query shape with no filters, in each sort you intend to use. Zero syncs the union of all active queries' results. You don't have to worry about syncing many sorts of the same query when it's likely the results will overlap heavily. In the future, we will be implementing a consistency model that fixes these issues automatically. We will prevent Zero from returning local data when that data is not known to be a prefix of the server result. Once the consistency model is implemented, preloading can be thought of as purely a performance thing, and not required to avoid unsightly flickering.", "headings": [ { "text": "Architecture", @@ -2742,6 +2812,10 @@ "text": "Partial Data", "id": "partial-data" }, + { + "text": "Cached Results", + "id": "cached-results" + }, { "text": "Handling Errors", "id": "handling-errors" @@ -2786,7 +2860,7 @@ "kind": "page" }, { - "id": "252-queries#architecture", + "id": "258-queries#architecture", "title": "Queries", "searchTitle": "Architecture", "sectionTitle": "Architecture", @@ -2796,7 +2870,7 @@ "kind": "section" }, { - "id": "253-queries#life-of-a-query", + "id": "259-queries#life-of-a-query", "title": "Queries", "searchTitle": "Life of a Query", "sectionTitle": "Life of a Query", @@ -2806,7 +2880,7 @@ "kind": "section" }, { - "id": "254-queries#defining-queries", + "id": "260-queries#defining-queries", "title": "Queries", "searchTitle": "Defining Queries", "sectionTitle": "Defining Queries", @@ -2816,7 +2890,7 @@ "kind": "section" }, { - "id": "255-queries#basics", + "id": "261-queries#basics", "title": "Queries", "searchTitle": "Basics", "sectionTitle": "Basics", @@ -2826,7 +2900,7 @@ "kind": "section" }, { - "id": "256-queries#arguments", + "id": "262-queries#arguments", "title": "Queries", "searchTitle": "Arguments", "sectionTitle": "Arguments", @@ -2836,7 +2910,7 @@ "kind": "section" }, { - "id": "257-queries#query-registries", + "id": "263-queries#query-registries", "title": "Queries", "searchTitle": "Query Registries", "sectionTitle": "Query Registries", @@ -2846,7 +2920,7 @@ "kind": "section" }, { - "id": "258-queries#query-names", + "id": "264-queries#query-names", "title": "Queries", "searchTitle": "Query Names", "sectionTitle": "Query Names", @@ -2856,7 +2930,7 @@ "kind": "section" }, { - "id": "259-queries#context", + "id": "265-queries#context", "title": "Queries", "searchTitle": "Context", "sectionTitle": "Context", @@ -2866,7 +2940,7 @@ "kind": "section" }, { - "id": "260-queries#queriests", + "id": "266-queries#queriests", "title": "Queries", "searchTitle": "queries.ts", "sectionTitle": "queries.ts", @@ -2876,7 +2950,7 @@ "kind": "section" }, { - "id": "261-queries#server-setup", + "id": "267-queries#server-setup", "title": "Queries", "searchTitle": "Server Setup", "sectionTitle": "Server Setup", @@ -2886,7 +2960,7 @@ "kind": "section" }, { - "id": "262-queries#registering-the-endpoint", + "id": "268-queries#registering-the-endpoint", "title": "Queries", "searchTitle": "Registering the Endpoint", "sectionTitle": "Registering the Endpoint", @@ -2896,7 +2970,7 @@ "kind": "section" }, { - "id": "263-queries#implementing-the-endpoint", + "id": "269-queries#implementing-the-endpoint", "title": "Queries", "searchTitle": "Implementing the Endpoint", "sectionTitle": "Implementing the Endpoint", @@ -2906,7 +2980,7 @@ "kind": "section" }, { - "id": "264-queries#custom-query-url", + "id": "270-queries#custom-query-url", "title": "Queries", "searchTitle": "Custom Query URL", "sectionTitle": "Custom Query URL", @@ -2916,7 +2990,7 @@ "kind": "section" }, { - "id": "265-queries#url-patterns", + "id": "271-queries#url-patterns", "title": "Queries", "searchTitle": "URL Patterns", "sectionTitle": "URL Patterns", @@ -2926,17 +3000,17 @@ "kind": "section" }, { - "id": "266-queries#running-queries", + "id": "272-queries#running-queries", "title": "Queries", "searchTitle": "Running Queries", "sectionTitle": "Running Queries", "sectionId": "running-queries", "url": "/docs/queries", - "content": "Reactively The most common way to use queries is with the useQuery reactive hooks from the React or SolidJS bindings (or the equivalent low-level API): import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(queries.posts.get('user123')) return posts.map(post => (
{post.title}
)) }import {useQuery} from '@rocicorp/zero/solid' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(() => queries.posts.get('user123') ) return ( {post =>
{post.title}
}
) }import {queries} from 'zero/queries.ts' import {zero} from 'zero.ts' const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) for (let post of postsView.data) { console.log(post.title) } // updates as the underlying data changes postsView.addListener(posts => { console.log('posts', posts) }) These functions allow you to automatically re-render UI when a query changes. Conditionally Sometimes the inputs needed to construct a query are not available on the first render. For example, auth state or a route param might still be loading after a page refresh. Both React and Solid support conditional queries by passing undefined until the query can be constructed: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function Username({userID}: {userID: string | undefined}) { const [user] = useQuery( userID ? queries.users.getUser({ userID }) : undefined ) return user ?
{user.username}
: null }import {useQuery} from '@rocicorp/zero/solid' import {Show} from 'solid-js' import {queries} from 'zero/queries.ts' function Username(props: {userID: string | undefined}) { const [user] = useQuery(() => props.userID ? queries.users.getUser({ userID: props.userID }) : undefined ) return ( {user =>
{user().username}
}
) } Once You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} ) For Preloading Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) )", + "content": "Reactively The most common way to use queries is with the useQuery reactive hooks from the React or SolidJS bindings (or the equivalent low-level API): import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(queries.posts.get('user123')) return posts.map(post => (
{post.title}
)) }import {useQuery} from '@rocicorp/zero/solid' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(() => queries.posts.get('user123') ) return ( {post =>
{post.title}
}
) }import {queries} from 'zero/queries.ts' import {zero} from 'zero.ts' const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) for (let post of postsView.data) { console.log(post.title) } // updates as the underlying data changes postsView.addListener(posts => { console.log('posts', posts) }) These functions allow you to automatically re-render UI when a query changes. Conditionally Sometimes the inputs needed to construct a query are not available on the first render. For example, auth state or a route param might still be loading after a page refresh. Both React and Solid support conditional queries by passing undefined until the query can be constructed: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function Username({userID}: {userID: string | undefined}) { const [user] = useQuery( userID ? queries.users.getUser({ userID }) : undefined ) return user ?
{user.username}
: null }import {useQuery} from '@rocicorp/zero/solid' import {Show} from 'solid-js' import {queries} from 'zero/queries.ts' function Username(props: {userID: string | undefined}) { const [user] = useQuery(() => props.userID ? queries.users.getUser({ userID: props.userID }) : undefined ) return ( {user =>
{user().username}
}
) } Once You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} ) To also accept a result the server confirmed during a previous session, pass {type: 'cached'}. It resolves as soon as the result is cached or complete, so it doesn't wait for the server when the client already stores a confirmed result: const results = await zero.run( queries.issues.byPriority('high'), {type: 'cached'} ) For Preloading Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) ) preload() returns cleanup, which ends the preload (Zero keeps its rows for the query's TTL), and two promises: complete resolves once the server has returned the result, and cached resolves once the result is cached or complete.", "kind": "section" }, { - "id": "267-queries#reactively", + "id": "273-queries#reactively", "title": "Queries", "searchTitle": "Reactively", "sectionTitle": "Reactively", @@ -2946,7 +3020,7 @@ "kind": "section" }, { - "id": "268-queries#conditionally", + "id": "274-queries#conditionally", "title": "Queries", "searchTitle": "Conditionally", "sectionTitle": "Conditionally", @@ -2956,27 +3030,27 @@ "kind": "section" }, { - "id": "269-queries#once", + "id": "275-queries#once", "title": "Queries", "searchTitle": "Once", "sectionTitle": "Once", "sectionId": "once", "url": "/docs/queries", - "content": "You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} )", + "content": "You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} ) To also accept a result the server confirmed during a previous session, pass {type: 'cached'}. It resolves as soon as the result is cached or complete, so it doesn't wait for the server when the client already stores a confirmed result: const results = await zero.run( queries.issues.byPriority('high'), {type: 'cached'} )", "kind": "section" }, { - "id": "270-queries#for-preloading", + "id": "276-queries#for-preloading", "title": "Queries", "searchTitle": "For Preloading", "sectionTitle": "For Preloading", "sectionId": "for-preloading", "url": "/docs/queries", - "content": "Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) )", + "content": "Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) ) preload() returns cleanup, which ends the preload (Zero keeps its rows for the query's TTL), and two promises: complete resolves once the server has returned the result, and cached resolves once the result is cached or complete.", "kind": "section" }, { - "id": "271-queries#missing-data", + "id": "277-queries#missing-data", "title": "Queries", "searchTitle": "Missing Data", "sectionTitle": "Missing Data", @@ -2986,17 +3060,27 @@ "kind": "section" }, { - "id": "272-queries#partial-data", + "id": "278-queries#partial-data", "title": "Queries", "searchTitle": "Partial Data", "sectionTitle": "Partial Data", "sectionId": "partial-data", "url": "/docs/queries", - "content": "Zero immediately returns the data for a query it has on the client, then falls back to the server for any missing data. Sometimes it's useful to know the difference between these two types of results. To do so, use the result from useQuery: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) if (issuesResult.type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) if (issuesResult().type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if (resultType === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') } }) The possible values of result.type are currently complete and unknown. The complete value is currently only returned when Zero has received the server result. In the future, Zero will be able to return this result type when it knows that all possible data for this query is already available locally. Additionally, we plan to add a prefix result for when the data is known to be a prefix of the complete result. See Consistency for more information.", + "content": "Zero immediately returns the data for a query it has on the client, then falls back to the server for any missing data. Sometimes it's useful to know the difference between these two types of results. To do so, use the result from useQuery: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) if (issuesResult.type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) if (issuesResult().type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if (resultType === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') } }) The possible values of result.type are: unknown: the data Zero has on the client. Some of it may be missing. cached: the client stores a result that the server confirmed during a previous session. See Cached Results. complete: the server has returned the result during this session. error: the query failed. See Handling Errors. The complete value is currently only returned when Zero has received the server result. In the future, Zero will be able to return this result type when it knows that all possible data for this query is already available locally. Additionally, we plan to add a prefix result for when the data is known to be a prefix of the complete result. See Consistency for more information. Cached Results When Zero stores data on the client (in IndexedDB, or SQLite on React Native), it also remembers which query results the server confirmed. If the client still holds such a result when the app starts again, the query starts as cached instead of unknown, and becomes complete once the server returns the result in this session. If Zero evicts the stored result before then, the query goes back to unknown. A cached result was complete when the server sent it, but it may be out of date. Use it where a previous session's answer is good enough, for example to show stored data or an empty state without waiting for the server, which also works while offline. Wait for complete where the data must be current: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) const confirmed = issuesResult.type === 'cached' || issuesResult.type === 'complete' if (issues.length === 0 && confirmed) { return
Your inbox is empty
}const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) const confirmed = () => issuesResult().type === 'cached' || issuesResult().type === 'complete' return ( 0 || !confirmed()} fallback={
Your inbox is empty
} > {/* ... */}
)const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if ( issues.length === 0 && (resultType === 'cached' || resultType === 'complete') ) { console.log('Inbox is empty') } }) zero.run(query, {type: 'cached'}) and the cached promise from zero.preload() resolve on a cached or complete result. {type: 'complete'} and the complete promise still wait for the server.", "kind": "section" }, { - "id": "273-queries#handling-errors", + "id": "279-queries#cached-results", + "title": "Queries", + "searchTitle": "Cached Results", + "sectionTitle": "Cached Results", + "sectionId": "cached-results", + "url": "/docs/queries", + "content": "When Zero stores data on the client (in IndexedDB, or SQLite on React Native), it also remembers which query results the server confirmed. If the client still holds such a result when the app starts again, the query starts as cached instead of unknown, and becomes complete once the server returns the result in this session. If Zero evicts the stored result before then, the query goes back to unknown. A cached result was complete when the server sent it, but it may be out of date. Use it where a previous session's answer is good enough, for example to show stored data or an empty state without waiting for the server, which also works while offline. Wait for complete where the data must be current: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) const confirmed = issuesResult.type === 'cached' || issuesResult.type === 'complete' if (issues.length === 0 && confirmed) { return
Your inbox is empty
}const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) const confirmed = () => issuesResult().type === 'cached' || issuesResult().type === 'complete' return ( 0 || !confirmed()} fallback={
Your inbox is empty
} > {/* ... */}
)const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if ( issues.length === 0 && (resultType === 'cached' || resultType === 'complete') ) { console.log('Inbox is empty') } }) zero.run(query, {type: 'cached'}) and the cached promise from zero.preload() resolve on a cached or complete result. {type: 'complete'} and the complete promise still wait for the server.", + "kind": "section" + }, + { + "id": "280-queries#handling-errors", "title": "Queries", "searchTitle": "Handling Errors", "sectionTitle": "Handling Errors", @@ -3006,7 +3090,7 @@ "kind": "section" }, { - "id": "274-queries#granular-updates", + "id": "281-queries#granular-updates", "title": "Queries", "searchTitle": "Granular Updates", "sectionTitle": "Granular Updates", @@ -3016,7 +3100,7 @@ "kind": "section" }, { - "id": "275-queries#query-caching", + "id": "282-queries#query-caching", "title": "Queries", "searchTitle": "Query Caching", "sectionTitle": "Query Caching", @@ -3026,7 +3110,7 @@ "kind": "section" }, { - "id": "276-queries#ttls", + "id": "283-queries#ttls", "title": "Queries", "searchTitle": "TTLs", "sectionTitle": "TTLs", @@ -3036,7 +3120,7 @@ "kind": "section" }, { - "id": "277-queries#ttl-defaults", + "id": "284-queries#ttl-defaults", "title": "Queries", "searchTitle": "TTL Defaults", "sectionTitle": "TTL Defaults", @@ -3046,7 +3130,7 @@ "kind": "section" }, { - "id": "278-queries#setting-different-ttls", + "id": "285-queries#setting-different-ttls", "title": "Queries", "searchTitle": "Setting Different TTLs", "sectionTitle": "Setting Different TTLs", @@ -3056,7 +3140,7 @@ "kind": "section" }, { - "id": "279-queries#why-zero-ttls-are-short", + "id": "286-queries#why-zero-ttls-are-short", "title": "Queries", "searchTitle": "Why Zero TTLs are Short", "sectionTitle": "Why Zero TTLs are Short", @@ -3066,7 +3150,7 @@ "kind": "section" }, { - "id": "280-queries#local-only-queries", + "id": "287-queries#local-only-queries", "title": "Queries", "searchTitle": "Local-Only Queries", "sectionTitle": "Local-Only Queries", @@ -3076,7 +3160,7 @@ "kind": "section" }, { - "id": "281-queries#custom-server-implementation", + "id": "288-queries#custom-server-implementation", "title": "Queries", "searchTitle": "Custom Server Implementation", "sectionTitle": "Custom Server Implementation", @@ -3086,7 +3170,7 @@ "kind": "section" }, { - "id": "282-queries#consistency", + "id": "289-queries#consistency", "title": "Queries", "searchTitle": "Consistency", "sectionTitle": "Consistency", @@ -3118,7 +3202,7 @@ "kind": "page" }, { - "id": "283-quickstart#hello-zero-solid", + "id": "290-quickstart#hello-zero-solid", "title": "Quickstart", "searchTitle": "hello-zero-solid", "sectionTitle": "hello-zero-solid", @@ -3128,7 +3212,7 @@ "kind": "section" }, { - "id": "284-quickstart#hello-zero-cf", + "id": "291-quickstart#hello-zero-cf", "title": "Quickstart", "searchTitle": "hello-zero-cf", "sectionTitle": "hello-zero-cf", @@ -3138,7 +3222,7 @@ "kind": "section" }, { - "id": "285-quickstart#hello-zero", + "id": "292-quickstart#hello-zero", "title": "Quickstart", "searchTitle": "hello-zero", "sectionTitle": "hello-zero", @@ -3152,7 +3236,7 @@ "title": "React Native", "searchTitle": "React Native", "url": "/docs/react-native", - "content": "Zero has built-in support for React Native and Expo. Usage is identical to React on the web, except you must provide a kvStore implementation. Choose the storage adapter you prefer: import {ZeroProvider} from '@rocicorp/zero/react' import {expoSQLiteStoreProvider} from '@rocicorp/zero/expo-sqlite' export function RootLayout() { return ( ) }import {ZeroProvider} from '@rocicorp/zero/react' import {opSQLiteStoreProvider} from '@rocicorp/zero/op-sqlite' export default function RootLayout() { return ( ) } For a complete example, see zslack. op-sqlite is much faster than expo-sqlite but does not work with Expo Go. However, it is supported with expo prebuild and development builds.", + "content": "Zero has built-in support for React Native and Expo. Usage is identical to React on the web, except you must provide a kvStore implementation. Choose the storage adapter you prefer: import {ZeroProvider} from '@rocicorp/zero/react' import {expoSQLiteStoreProvider} from '@rocicorp/zero/expo-sqlite' export function RootLayout() { return ( ) }import {ZeroProvider} from '@rocicorp/zero/react' import {opSQLiteStoreProvider} from '@rocicorp/zero/op-sqlite' export default function RootLayout() { return ( ) } For a complete example, see zslack. To handle a full disk or a database that fails to open, see Storage Failures. op-sqlite is much faster than expo-sqlite but does not work with Expo Go. However, it is supported with expo prebuild and development builds.", "headings": [], "kind": "page" }, @@ -3161,7 +3245,7 @@ "title": "React", "searchTitle": "React", "url": "/docs/react", - "content": "Zero has built-in support for React. Here's what basic usage looks like. Setup Use the ZeroProvider component to setup Zero. It takes care of creating and destroying Zero instances reactively: import {createRoot} from 'react-dom/client' import {ZeroProvider} from '@rocicorp/zero/react' import {useSession} from 'my-session-provider' import App from './App.tsx' import {schema} from 'schema.ts' import {mutators} from 'mutators.ts' const cacheURL = import.meta.env.VITE_PUBLIC_ZERO_CACHE_URL! export default function Root() { const session = useSession() const userID = session?.userID const auth = session?.accessToken const context = userID ? {userID} : undefined return ( ) } If you use token auth, pass it with auth={session?.jwt}. If you use cookie auth, omit the parameter entirely. When auth changes from one string to another, ZeroProvider refreshes auth in place with zero.connection.connect({auth}). When auth is added or removed, or userID changes, it recreates the Zero instance. When the user is logged out, omit the userID entirely. You can also pass a Zero instance to the ZeroProvider if you want to control the lifecycle of the Zero instance yourself: // ZeroProvider just sets up the context, it doesn't manage // the lifecycle of the Zero instance. When you pass zero={zero}, ZeroProvider only provides React Context. It does not manage auth updates for that instance, so if you are using token auth, call zero.connection.connect({auth: newToken}) yourself or recreate the instance when the user changes. Usage Use useQuery to run queries: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'queries.ts' function Posts() { const [posts] = useQuery( queries.posts.byStatus({status: 'draft'}) ) return ( <> {posts.map(p => (
{p.title} ({p.comments.length} comments)
))} ) } For conditional queries, such as queries that depend on auth state or route params that may not be loaded yet, see Conditional Queries. Use useZero to get access to the Zero instance, for example to run mutators: import {useZero} from '@rocicorp/zero/react' import {mutators} from 'mutators.ts' function CompleteButton({issueID}: {issueID: string}) { const zero = useZero() const onClick = () => { zero.mutate(mutators.issues.complete({id: issueID})) } return } Suspense The useSuspenseQuery hook is exactly like useQuery, except it supports React Suspense. const [issues] = useSuspenseQuery(issueQuery, { suspendUntil: 'complete' // 'partial' or 'complete' }) Use the suspendUntil parameter to control how long to suspend for. The value complete suspends until authoritative results from the server are received. The partial value suspends until any non-empty data is received, or for a empty result that is complete. Examples See the sample directory for more complete React examples.", + "content": "Zero has built-in support for React. Here's what basic usage looks like. Setup Use the ZeroProvider component to setup Zero. It takes care of creating and destroying Zero instances reactively: import {createRoot} from 'react-dom/client' import {ZeroProvider} from '@rocicorp/zero/react' import {useSession} from 'my-session-provider' import App from './App.tsx' import {schema} from 'schema.ts' import {mutators} from 'mutators.ts' const cacheURL = import.meta.env.VITE_PUBLIC_ZERO_CACHE_URL! export default function Root() { const session = useSession() const userID = session?.userID const auth = session?.accessToken const context = userID ? {userID} : undefined return ( ) } If you use token auth, pass it with auth={session?.jwt}. If you use cookie auth, omit the parameter entirely. When auth changes from one string to another, ZeroProvider refreshes auth in place with zero.connection.connect({auth}). When auth is added or removed, or userID changes, it recreates the Zero instance. When the user is logged out, omit the userID entirely. You can also pass a Zero instance to the ZeroProvider if you want to control the lifecycle of the Zero instance yourself: // ZeroProvider just sets up the context, it doesn't manage // the lifecycle of the Zero instance. When you pass zero={zero}, ZeroProvider only provides React Context. It does not manage auth updates for that instance, so if you are using token auth, call zero.connection.connect({auth: newToken}) yourself or recreate the instance when the user changes. Usage Use useQuery to run queries: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'queries.ts' function Posts() { const [posts] = useQuery( queries.posts.byStatus({status: 'draft'}) ) return ( <> {posts.map(p => (
{p.title} ({p.comments.length} comments)
))} ) } For conditional queries, such as queries that depend on auth state or route params that may not be loaded yet, see Conditional Queries. Use useZero to get access to the Zero instance, for example to run mutators: import {useZero} from '@rocicorp/zero/react' import {mutators} from 'mutators.ts' function CompleteButton({issueID}: {issueID: string}) { const zero = useZero() const onClick = () => { zero.mutate(mutators.issues.complete({id: issueID})) } return } Suspense The useSuspenseQuery hook is exactly like useQuery, except it supports React Suspense. const [issues] = useSuspenseQuery(issueQuery, { suspendUntil: 'complete' // 'partial' or 'complete' }) Use the suspendUntil parameter to control how long to suspend for. The value complete suspends until authoritative results from the server are received. The partial value suspends until any non-empty data is received, or for a empty result that is complete. Query Identity You don't need to memoize queries. Build them inline, where you use them, and pass them straight to useQuery. Zero caches the queries you build with ZQL. Build the same query again and you usually get back the same Query object, even when it has related() or whereExists() callbacks written inline. So rebuilding a query on every render is cheap. You can also use a query as a dependency of useEffect or useMemo, or pass it as a prop to a component wrapped in React.memo: import {useEffect} from 'react' import {useQuery} from '@rocicorp/zero/react' import {zql} from 'schema.ts' function IssueSearch({text}: {text: string}) { // Rebuilt on every render. No useMemo needed. const query = zql.issue .where('title', 'ILIKE', `%${text}%`) .orderBy('created', 'desc') const [issues] = useQuery(query) // Scroll back to the top when the search changes. useEffect(() => { window.scrollTo(0, 0) }, [query]) return issues.map(issue => (
{issue.title}
)) } This example passes ZQL to useQuery directly, so it runs only on the client. Calls to registered queries work a little differently. Each call like queries.posts.byStatus({status}) returns a new object. You can still pass it inline: useQuery sees that it's the same query with the same arguments and reuses its previous work. But don't use the returned object as a dependency. Use its arguments instead. Keep in mind: Identity follows how you build a query. zql.issue.where('status', 'open').where('priority', 'high') and zql.issue.where('priority', 'high').where('status', 'open') return the same rows, but they can be different objects. A different value, such as a different ID in where(), gives a different object. Identity is an optimization, not a promise. Zero can hand back a new object for a query it has built before. When that happens, an effect or memo that depends on the query just runs again, so make sure that's safe. Examples See the sample directory for more complete React examples.", "headings": [ { "text": "Setup", @@ -3175,6 +3259,10 @@ "text": "Suspense", "id": "suspense" }, + { + "text": "Query Identity", + "id": "query-identity" + }, { "text": "Examples", "id": "examples" @@ -3183,7 +3271,7 @@ "kind": "page" }, { - "id": "286-react#setup", + "id": "293-react#setup", "title": "React", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -3193,7 +3281,7 @@ "kind": "section" }, { - "id": "287-react#usage", + "id": "294-react#usage", "title": "React", "searchTitle": "Usage", "sectionTitle": "Usage", @@ -3203,7 +3291,7 @@ "kind": "section" }, { - "id": "288-react#suspense", + "id": "295-react#suspense", "title": "React", "searchTitle": "Suspense", "sectionTitle": "Suspense", @@ -3213,7 +3301,17 @@ "kind": "section" }, { - "id": "289-react#examples", + "id": "296-react#query-identity", + "title": "React", + "searchTitle": "Query Identity", + "sectionTitle": "Query Identity", + "sectionId": "query-identity", + "url": "/docs/react", + "content": "You don't need to memoize queries. Build them inline, where you use them, and pass them straight to useQuery. Zero caches the queries you build with ZQL. Build the same query again and you usually get back the same Query object, even when it has related() or whereExists() callbacks written inline. So rebuilding a query on every render is cheap. You can also use a query as a dependency of useEffect or useMemo, or pass it as a prop to a component wrapped in React.memo: import {useEffect} from 'react' import {useQuery} from '@rocicorp/zero/react' import {zql} from 'schema.ts' function IssueSearch({text}: {text: string}) { // Rebuilt on every render. No useMemo needed. const query = zql.issue .where('title', 'ILIKE', `%${text}%`) .orderBy('created', 'desc') const [issues] = useQuery(query) // Scroll back to the top when the search changes. useEffect(() => { window.scrollTo(0, 0) }, [query]) return issues.map(issue => (
{issue.title}
)) } This example passes ZQL to useQuery directly, so it runs only on the client. Calls to registered queries work a little differently. Each call like queries.posts.byStatus({status}) returns a new object. You can still pass it inline: useQuery sees that it's the same query with the same arguments and reuses its previous work. But don't use the returned object as a dependency. Use its arguments instead. Keep in mind: Identity follows how you build a query. zql.issue.where('status', 'open').where('priority', 'high') and zql.issue.where('priority', 'high').where('status', 'open') return the same rows, but they can be different objects. A different value, such as a different ID in where(), gives a different object. Identity is an optimization, not a promise. Zero can hand back a new object for a query it has built before. When that happens, an effect or memo that depends on the query just runs again, so make sure that's safe.", + "kind": "section" + }, + { + "id": "297-react#examples", "title": "React", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -3245,7 +3343,7 @@ "kind": "page" }, { - "id": "290-release-notes/0.1#breaking-changes", + "id": "298-release-notes/0.1#breaking-changes", "title": "Zero 0.1", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -3255,7 +3353,7 @@ "kind": "section" }, { - "id": "291-release-notes/0.1#features", + "id": "299-release-notes/0.1#features", "title": "Zero 0.1", "searchTitle": "Features", "sectionTitle": "Features", @@ -3265,7 +3363,7 @@ "kind": "section" }, { - "id": "292-release-notes/0.1#source-tree-fixes", + "id": "300-release-notes/0.1#source-tree-fixes", "title": "Zero 0.1", "searchTitle": "Source tree fixes", "sectionTitle": "Source tree fixes", @@ -3301,7 +3399,7 @@ "kind": "page" }, { - "id": "293-release-notes/0.10#install", + "id": "301-release-notes/0.10#install", "title": "Zero 0.10", "searchTitle": "Install", "sectionTitle": "Install", @@ -3311,7 +3409,7 @@ "kind": "section" }, { - "id": "294-release-notes/0.10#features", + "id": "302-release-notes/0.10#features", "title": "Zero 0.10", "searchTitle": "Features", "sectionTitle": "Features", @@ -3321,7 +3419,7 @@ "kind": "section" }, { - "id": "295-release-notes/0.10#fixes", + "id": "303-release-notes/0.10#fixes", "title": "Zero 0.10", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3331,7 +3429,7 @@ "kind": "section" }, { - "id": "296-release-notes/0.10#breaking-changes", + "id": "304-release-notes/0.10#breaking-changes", "title": "Zero 0.10", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3367,7 +3465,7 @@ "kind": "page" }, { - "id": "297-release-notes/0.11#install", + "id": "305-release-notes/0.11#install", "title": "Zero 0.11", "searchTitle": "Install", "sectionTitle": "Install", @@ -3377,7 +3475,7 @@ "kind": "section" }, { - "id": "298-release-notes/0.11#features", + "id": "306-release-notes/0.11#features", "title": "Zero 0.11", "searchTitle": "Features", "sectionTitle": "Features", @@ -3387,7 +3485,7 @@ "kind": "section" }, { - "id": "299-release-notes/0.11#fixes", + "id": "307-release-notes/0.11#fixes", "title": "Zero 0.11", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3397,7 +3495,7 @@ "kind": "section" }, { - "id": "300-release-notes/0.11#breaking-changes", + "id": "308-release-notes/0.11#breaking-changes", "title": "Zero 0.11", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3433,7 +3531,7 @@ "kind": "page" }, { - "id": "301-release-notes/0.12#install", + "id": "309-release-notes/0.12#install", "title": "Zero 0.12", "searchTitle": "Install", "sectionTitle": "Install", @@ -3443,7 +3541,7 @@ "kind": "section" }, { - "id": "302-release-notes/0.12#features", + "id": "310-release-notes/0.12#features", "title": "Zero 0.12", "searchTitle": "Features", "sectionTitle": "Features", @@ -3453,7 +3551,7 @@ "kind": "section" }, { - "id": "303-release-notes/0.12#fixes", + "id": "311-release-notes/0.12#fixes", "title": "Zero 0.12", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3463,7 +3561,7 @@ "kind": "section" }, { - "id": "304-release-notes/0.12#breaking-changes", + "id": "312-release-notes/0.12#breaking-changes", "title": "Zero 0.12", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3499,7 +3597,7 @@ "kind": "page" }, { - "id": "305-release-notes/0.13#install", + "id": "313-release-notes/0.13#install", "title": "Zero 0.13", "searchTitle": "Install", "sectionTitle": "Install", @@ -3509,7 +3607,7 @@ "kind": "section" }, { - "id": "306-release-notes/0.13#features", + "id": "314-release-notes/0.13#features", "title": "Zero 0.13", "searchTitle": "Features", "sectionTitle": "Features", @@ -3519,7 +3617,7 @@ "kind": "section" }, { - "id": "307-release-notes/0.13#fixes", + "id": "315-release-notes/0.13#fixes", "title": "Zero 0.13", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3529,7 +3627,7 @@ "kind": "section" }, { - "id": "308-release-notes/0.13#breaking-changes", + "id": "316-release-notes/0.13#breaking-changes", "title": "Zero 0.13", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3565,7 +3663,7 @@ "kind": "page" }, { - "id": "309-release-notes/0.14#install", + "id": "317-release-notes/0.14#install", "title": "Zero 0.14", "searchTitle": "Install", "sectionTitle": "Install", @@ -3575,7 +3673,7 @@ "kind": "section" }, { - "id": "310-release-notes/0.14#features", + "id": "318-release-notes/0.14#features", "title": "Zero 0.14", "searchTitle": "Features", "sectionTitle": "Features", @@ -3585,7 +3683,7 @@ "kind": "section" }, { - "id": "311-release-notes/0.14#fixes", + "id": "319-release-notes/0.14#fixes", "title": "Zero 0.14", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3595,7 +3693,7 @@ "kind": "section" }, { - "id": "312-release-notes/0.14#breaking-changes", + "id": "320-release-notes/0.14#breaking-changes", "title": "Zero 0.14", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3635,7 +3733,7 @@ "kind": "page" }, { - "id": "313-release-notes/0.15#install", + "id": "321-release-notes/0.15#install", "title": "Zero 0.15", "searchTitle": "Install", "sectionTitle": "Install", @@ -3645,7 +3743,7 @@ "kind": "section" }, { - "id": "314-release-notes/0.15#upgrade-guide", + "id": "322-release-notes/0.15#upgrade-guide", "title": "Zero 0.15", "searchTitle": "Upgrade Guide", "sectionTitle": "Upgrade Guide", @@ -3655,7 +3753,7 @@ "kind": "section" }, { - "id": "315-release-notes/0.15#features", + "id": "323-release-notes/0.15#features", "title": "Zero 0.15", "searchTitle": "Features", "sectionTitle": "Features", @@ -3665,7 +3763,7 @@ "kind": "section" }, { - "id": "316-release-notes/0.15#fixes", + "id": "324-release-notes/0.15#fixes", "title": "Zero 0.15", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3675,7 +3773,7 @@ "kind": "section" }, { - "id": "317-release-notes/0.15#breaking-changes", + "id": "325-release-notes/0.15#breaking-changes", "title": "Zero 0.15", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3715,7 +3813,7 @@ "kind": "page" }, { - "id": "318-release-notes/0.16#install", + "id": "326-release-notes/0.16#install", "title": "Zero 0.16", "searchTitle": "Install", "sectionTitle": "Install", @@ -3725,7 +3823,7 @@ "kind": "section" }, { - "id": "319-release-notes/0.16#upgrading", + "id": "327-release-notes/0.16#upgrading", "title": "Zero 0.16", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -3735,7 +3833,7 @@ "kind": "section" }, { - "id": "320-release-notes/0.16#features", + "id": "328-release-notes/0.16#features", "title": "Zero 0.16", "searchTitle": "Features", "sectionTitle": "Features", @@ -3745,7 +3843,7 @@ "kind": "section" }, { - "id": "321-release-notes/0.16#fixes", + "id": "329-release-notes/0.16#fixes", "title": "Zero 0.16", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3755,7 +3853,7 @@ "kind": "section" }, { - "id": "322-release-notes/0.16#breaking-changes", + "id": "330-release-notes/0.16#breaking-changes", "title": "Zero 0.16", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3795,7 +3893,7 @@ "kind": "page" }, { - "id": "323-release-notes/0.17#install", + "id": "331-release-notes/0.17#install", "title": "Zero 0.17", "searchTitle": "Install", "sectionTitle": "Install", @@ -3805,7 +3903,7 @@ "kind": "section" }, { - "id": "324-release-notes/0.17#upgrading", + "id": "332-release-notes/0.17#upgrading", "title": "Zero 0.17", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -3815,7 +3913,7 @@ "kind": "section" }, { - "id": "325-release-notes/0.17#features", + "id": "333-release-notes/0.17#features", "title": "Zero 0.17", "searchTitle": "Features", "sectionTitle": "Features", @@ -3825,7 +3923,7 @@ "kind": "section" }, { - "id": "326-release-notes/0.17#fixes", + "id": "334-release-notes/0.17#fixes", "title": "Zero 0.17", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3835,7 +3933,7 @@ "kind": "section" }, { - "id": "327-release-notes/0.17#breaking-changes", + "id": "335-release-notes/0.17#breaking-changes", "title": "Zero 0.17", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3875,7 +3973,7 @@ "kind": "page" }, { - "id": "328-release-notes/0.18#install", + "id": "336-release-notes/0.18#install", "title": "Zero 0.18", "searchTitle": "Install", "sectionTitle": "Install", @@ -3885,7 +3983,7 @@ "kind": "section" }, { - "id": "329-release-notes/0.18#upgrading", + "id": "337-release-notes/0.18#upgrading", "title": "Zero 0.18", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -3895,7 +3993,7 @@ "kind": "section" }, { - "id": "330-release-notes/0.18#features", + "id": "338-release-notes/0.18#features", "title": "Zero 0.18", "searchTitle": "Features", "sectionTitle": "Features", @@ -3905,7 +4003,7 @@ "kind": "section" }, { - "id": "331-release-notes/0.18#fixes", + "id": "339-release-notes/0.18#fixes", "title": "Zero 0.18", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3915,7 +4013,7 @@ "kind": "section" }, { - "id": "332-release-notes/0.18#breaking-changes", + "id": "340-release-notes/0.18#breaking-changes", "title": "Zero 0.18", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3955,7 +4053,7 @@ "kind": "page" }, { - "id": "333-release-notes/0.19#install", + "id": "341-release-notes/0.19#install", "title": "Zero 0.19", "searchTitle": "Install", "sectionTitle": "Install", @@ -3965,7 +4063,7 @@ "kind": "section" }, { - "id": "334-release-notes/0.19#upgrading", + "id": "342-release-notes/0.19#upgrading", "title": "Zero 0.19", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -3975,7 +4073,7 @@ "kind": "section" }, { - "id": "335-release-notes/0.19#features", + "id": "343-release-notes/0.19#features", "title": "Zero 0.19", "searchTitle": "Features", "sectionTitle": "Features", @@ -3985,7 +4083,7 @@ "kind": "section" }, { - "id": "336-release-notes/0.19#fixes", + "id": "344-release-notes/0.19#fixes", "title": "Zero 0.19", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3995,7 +4093,7 @@ "kind": "section" }, { - "id": "337-release-notes/0.19#breaking-changes", + "id": "345-release-notes/0.19#breaking-changes", "title": "Zero 0.19", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4039,7 +4137,7 @@ "kind": "page" }, { - "id": "338-release-notes/0.2#breaking-changes", + "id": "346-release-notes/0.2#breaking-changes", "title": "Zero 0.2", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -4049,7 +4147,7 @@ "kind": "section" }, { - "id": "339-release-notes/0.2#features", + "id": "347-release-notes/0.2#features", "title": "Zero 0.2", "searchTitle": "Features", "sectionTitle": "Features", @@ -4059,7 +4157,7 @@ "kind": "section" }, { - "id": "340-release-notes/0.2#fixes", + "id": "348-release-notes/0.2#fixes", "title": "Zero 0.2", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4069,7 +4167,7 @@ "kind": "section" }, { - "id": "341-release-notes/0.2#docs", + "id": "349-release-notes/0.2#docs", "title": "Zero 0.2", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -4079,7 +4177,7 @@ "kind": "section" }, { - "id": "342-release-notes/0.2#source-tree-fixes", + "id": "350-release-notes/0.2#source-tree-fixes", "title": "Zero 0.2", "searchTitle": "Source tree fixes", "sectionTitle": "Source tree fixes", @@ -4089,7 +4187,7 @@ "kind": "section" }, { - "id": "343-release-notes/0.2#zbugs", + "id": "351-release-notes/0.2#zbugs", "title": "Zero 0.2", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -4129,7 +4227,7 @@ "kind": "page" }, { - "id": "344-release-notes/0.20#install", + "id": "352-release-notes/0.20#install", "title": "Zero 0.20", "searchTitle": "Install", "sectionTitle": "Install", @@ -4139,7 +4237,7 @@ "kind": "section" }, { - "id": "345-release-notes/0.20#upgrading", + "id": "353-release-notes/0.20#upgrading", "title": "Zero 0.20", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4149,7 +4247,7 @@ "kind": "section" }, { - "id": "346-release-notes/0.20#features", + "id": "354-release-notes/0.20#features", "title": "Zero 0.20", "searchTitle": "Features", "sectionTitle": "Features", @@ -4159,7 +4257,7 @@ "kind": "section" }, { - "id": "347-release-notes/0.20#fixes", + "id": "355-release-notes/0.20#fixes", "title": "Zero 0.20", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4169,7 +4267,7 @@ "kind": "section" }, { - "id": "348-release-notes/0.20#breaking-changes", + "id": "356-release-notes/0.20#breaking-changes", "title": "Zero 0.20", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4209,7 +4307,7 @@ "kind": "page" }, { - "id": "349-release-notes/0.21#install", + "id": "357-release-notes/0.21#install", "title": "Zero 0.21", "searchTitle": "Install", "sectionTitle": "Install", @@ -4219,7 +4317,7 @@ "kind": "section" }, { - "id": "350-release-notes/0.21#upgrading", + "id": "358-release-notes/0.21#upgrading", "title": "Zero 0.21", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4229,7 +4327,7 @@ "kind": "section" }, { - "id": "351-release-notes/0.21#features", + "id": "359-release-notes/0.21#features", "title": "Zero 0.21", "searchTitle": "Features", "sectionTitle": "Features", @@ -4239,7 +4337,7 @@ "kind": "section" }, { - "id": "352-release-notes/0.21#fixes", + "id": "360-release-notes/0.21#fixes", "title": "Zero 0.21", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4249,7 +4347,7 @@ "kind": "section" }, { - "id": "353-release-notes/0.21#breaking-changes", + "id": "361-release-notes/0.21#breaking-changes", "title": "Zero 0.21", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4301,7 +4399,7 @@ "kind": "page" }, { - "id": "354-release-notes/0.22#install", + "id": "362-release-notes/0.22#install", "title": "Zero 0.22", "searchTitle": "Install", "sectionTitle": "Install", @@ -4311,7 +4409,7 @@ "kind": "section" }, { - "id": "355-release-notes/0.22#upgrading", + "id": "363-release-notes/0.22#upgrading", "title": "Zero 0.22", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4321,7 +4419,7 @@ "kind": "section" }, { - "id": "356-release-notes/0.22#how-ttls-used-to-work", + "id": "364-release-notes/0.22#how-ttls-used-to-work", "title": "Zero 0.22", "searchTitle": "How TTLs Used to Work", "sectionTitle": "How TTLs Used to Work", @@ -4331,7 +4429,7 @@ "kind": "section" }, { - "id": "357-release-notes/0.22#how-ttls-work-now", + "id": "365-release-notes/0.22#how-ttls-work-now", "title": "Zero 0.22", "searchTitle": "How TTLs Work Now", "sectionTitle": "How TTLs Work Now", @@ -4341,7 +4439,7 @@ "kind": "section" }, { - "id": "358-release-notes/0.22#using-new-ttls", + "id": "366-release-notes/0.22#using-new-ttls", "title": "Zero 0.22", "searchTitle": "Using New TTLs", "sectionTitle": "Using New TTLs", @@ -4351,7 +4449,7 @@ "kind": "section" }, { - "id": "359-release-notes/0.22#features", + "id": "367-release-notes/0.22#features", "title": "Zero 0.22", "searchTitle": "Features", "sectionTitle": "Features", @@ -4361,7 +4459,7 @@ "kind": "section" }, { - "id": "360-release-notes/0.22#fixes", + "id": "368-release-notes/0.22#fixes", "title": "Zero 0.22", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4371,7 +4469,7 @@ "kind": "section" }, { - "id": "361-release-notes/0.22#breaking-changes", + "id": "369-release-notes/0.22#breaking-changes", "title": "Zero 0.22", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4415,7 +4513,7 @@ "kind": "page" }, { - "id": "362-release-notes/0.23#install", + "id": "370-release-notes/0.23#install", "title": "Zero 0.23", "searchTitle": "Install", "sectionTitle": "Install", @@ -4425,7 +4523,7 @@ "kind": "section" }, { - "id": "363-release-notes/0.23#upgrading", + "id": "371-release-notes/0.23#upgrading", "title": "Zero 0.23", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4435,7 +4533,7 @@ "kind": "section" }, { - "id": "364-release-notes/0.23#features", + "id": "372-release-notes/0.23#features", "title": "Zero 0.23", "searchTitle": "Features", "sectionTitle": "Features", @@ -4445,7 +4543,7 @@ "kind": "section" }, { - "id": "365-release-notes/0.23#fixes", + "id": "373-release-notes/0.23#fixes", "title": "Zero 0.23", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4455,7 +4553,7 @@ "kind": "section" }, { - "id": "366-release-notes/0.23#zbugs", + "id": "374-release-notes/0.23#zbugs", "title": "Zero 0.23", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -4465,7 +4563,7 @@ "kind": "section" }, { - "id": "367-release-notes/0.23#breaking-changes", + "id": "375-release-notes/0.23#breaking-changes", "title": "Zero 0.23", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4505,7 +4603,7 @@ "kind": "page" }, { - "id": "368-release-notes/0.24#installation", + "id": "376-release-notes/0.24#installation", "title": "Zero 0.24", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -4515,7 +4613,7 @@ "kind": "section" }, { - "id": "369-release-notes/0.24#features", + "id": "377-release-notes/0.24#features", "title": "Zero 0.24", "searchTitle": "Features", "sectionTitle": "Features", @@ -4525,7 +4623,7 @@ "kind": "section" }, { - "id": "370-release-notes/0.24#fixes", + "id": "378-release-notes/0.24#fixes", "title": "Zero 0.24", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4535,7 +4633,7 @@ "kind": "section" }, { - "id": "371-release-notes/0.24#breaking-changes", + "id": "379-release-notes/0.24#breaking-changes", "title": "Zero 0.24", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4545,7 +4643,7 @@ "kind": "section" }, { - "id": "372-release-notes/0.24#example-upgrades", + "id": "380-release-notes/0.24#example-upgrades", "title": "Zero 0.24", "searchTitle": "Example Upgrades", "sectionTitle": "Example Upgrades", @@ -4593,7 +4691,7 @@ "kind": "page" }, { - "id": "373-release-notes/0.25#installation", + "id": "381-release-notes/0.25#installation", "title": "Zero 0.25", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -4603,7 +4701,7 @@ "kind": "section" }, { - "id": "374-release-notes/0.25#overview", + "id": "382-release-notes/0.25#overview", "title": "Zero 0.25", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -4613,7 +4711,7 @@ "kind": "section" }, { - "id": "375-release-notes/0.25#upgrading", + "id": "383-release-notes/0.25#upgrading", "title": "Zero 0.25", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4623,7 +4721,7 @@ "kind": "section" }, { - "id": "376-release-notes/0.25#features", + "id": "384-release-notes/0.25#features", "title": "Zero 0.25", "searchTitle": "Features", "sectionTitle": "Features", @@ -4633,7 +4731,7 @@ "kind": "section" }, { - "id": "377-release-notes/0.25#performance", + "id": "385-release-notes/0.25#performance", "title": "Zero 0.25", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -4643,7 +4741,7 @@ "kind": "section" }, { - "id": "378-release-notes/0.25#fixes", + "id": "386-release-notes/0.25#fixes", "title": "Zero 0.25", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4653,7 +4751,7 @@ "kind": "section" }, { - "id": "379-release-notes/0.25#breaking-changes", + "id": "387-release-notes/0.25#breaking-changes", "title": "Zero 0.25", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4689,7 +4787,7 @@ "kind": "page" }, { - "id": "380-release-notes/0.26#installation", + "id": "388-release-notes/0.26#installation", "title": "Zero 0.26", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -4699,7 +4797,7 @@ "kind": "section" }, { - "id": "381-release-notes/0.26#features", + "id": "389-release-notes/0.26#features", "title": "Zero 0.26", "searchTitle": "Features", "sectionTitle": "Features", @@ -4709,7 +4807,7 @@ "kind": "section" }, { - "id": "382-release-notes/0.26#fixes", + "id": "390-release-notes/0.26#fixes", "title": "Zero 0.26", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4719,7 +4817,7 @@ "kind": "section" }, { - "id": "383-release-notes/0.26#breaking-changes", + "id": "391-release-notes/0.26#breaking-changes", "title": "Zero 0.26", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4763,7 +4861,7 @@ "kind": "page" }, { - "id": "384-release-notes/0.3#install", + "id": "392-release-notes/0.3#install", "title": "Zero 0.3", "searchTitle": "Install", "sectionTitle": "Install", @@ -4773,7 +4871,7 @@ "kind": "section" }, { - "id": "385-release-notes/0.3#breaking-changes", + "id": "393-release-notes/0.3#breaking-changes", "title": "Zero 0.3", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -4783,7 +4881,7 @@ "kind": "section" }, { - "id": "386-release-notes/0.3#features", + "id": "394-release-notes/0.3#features", "title": "Zero 0.3", "searchTitle": "Features", "sectionTitle": "Features", @@ -4793,7 +4891,7 @@ "kind": "section" }, { - "id": "387-release-notes/0.3#fixes", + "id": "395-release-notes/0.3#fixes", "title": "Zero 0.3", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4803,7 +4901,7 @@ "kind": "section" }, { - "id": "388-release-notes/0.3#docs", + "id": "396-release-notes/0.3#docs", "title": "Zero 0.3", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -4813,7 +4911,7 @@ "kind": "section" }, { - "id": "389-release-notes/0.3#zbugs", + "id": "397-release-notes/0.3#zbugs", "title": "Zero 0.3", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -4857,7 +4955,7 @@ "kind": "page" }, { - "id": "390-release-notes/0.4#install", + "id": "398-release-notes/0.4#install", "title": "Zero 0.4", "searchTitle": "Install", "sectionTitle": "Install", @@ -4867,7 +4965,7 @@ "kind": "section" }, { - "id": "391-release-notes/0.4#breaking-changes", + "id": "399-release-notes/0.4#breaking-changes", "title": "Zero 0.4", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -4877,7 +4975,7 @@ "kind": "section" }, { - "id": "392-release-notes/0.4#added-or--and--and-not-to-zql-documentation", + "id": "400-release-notes/0.4#added-or--and--and-not-to-zql-documentation", "title": "Zero 0.4", "searchTitle": "Added or , and , and not to ZQL (documentation).", "sectionTitle": "Added or , and , and not to ZQL (documentation).", @@ -4887,7 +4985,7 @@ "kind": "section" }, { - "id": "393-release-notes/0.4#fixes", + "id": "401-release-notes/0.4#fixes", "title": "Zero 0.4", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4897,7 +4995,7 @@ "kind": "section" }, { - "id": "394-release-notes/0.4#docs", + "id": "402-release-notes/0.4#docs", "title": "Zero 0.4", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -4907,7 +5005,7 @@ "kind": "section" }, { - "id": "395-release-notes/0.4#zbugs", + "id": "403-release-notes/0.4#zbugs", "title": "Zero 0.4", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -4951,7 +5049,7 @@ "kind": "page" }, { - "id": "396-release-notes/0.5#install", + "id": "404-release-notes/0.5#install", "title": "Zero 0.5", "searchTitle": "Install", "sectionTitle": "Install", @@ -4961,7 +5059,7 @@ "kind": "section" }, { - "id": "397-release-notes/0.5#breaking-changes", + "id": "405-release-notes/0.5#breaking-changes", "title": "Zero 0.5", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -4971,7 +5069,7 @@ "kind": "section" }, { - "id": "398-release-notes/0.5#features", + "id": "406-release-notes/0.5#features", "title": "Zero 0.5", "searchTitle": "Features", "sectionTitle": "Features", @@ -4981,7 +5079,7 @@ "kind": "section" }, { - "id": "399-release-notes/0.5#fixes", + "id": "407-release-notes/0.5#fixes", "title": "Zero 0.5", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4991,7 +5089,7 @@ "kind": "section" }, { - "id": "400-release-notes/0.5#docs", + "id": "408-release-notes/0.5#docs", "title": "Zero 0.5", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -5001,7 +5099,7 @@ "kind": "section" }, { - "id": "401-release-notes/0.5#zbugs", + "id": "409-release-notes/0.5#zbugs", "title": "Zero 0.5", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -5045,7 +5143,7 @@ "kind": "page" }, { - "id": "402-release-notes/0.6#install", + "id": "410-release-notes/0.6#install", "title": "Zero 0.6", "searchTitle": "Install", "sectionTitle": "Install", @@ -5055,7 +5153,7 @@ "kind": "section" }, { - "id": "403-release-notes/0.6#upgrade-guide", + "id": "411-release-notes/0.6#upgrade-guide", "title": "Zero 0.6", "searchTitle": "Upgrade Guide", "sectionTitle": "Upgrade Guide", @@ -5065,7 +5163,7 @@ "kind": "section" }, { - "id": "404-release-notes/0.6#breaking-changes", + "id": "412-release-notes/0.6#breaking-changes", "title": "Zero 0.6", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5075,7 +5173,7 @@ "kind": "section" }, { - "id": "405-release-notes/0.6#features", + "id": "413-release-notes/0.6#features", "title": "Zero 0.6", "searchTitle": "Features", "sectionTitle": "Features", @@ -5085,7 +5183,7 @@ "kind": "section" }, { - "id": "406-release-notes/0.6#zbugs", + "id": "414-release-notes/0.6#zbugs", "title": "Zero 0.6", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -5095,7 +5193,7 @@ "kind": "section" }, { - "id": "407-release-notes/0.6#docs", + "id": "415-release-notes/0.6#docs", "title": "Zero 0.6", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -5135,7 +5233,7 @@ "kind": "page" }, { - "id": "408-release-notes/0.7#install", + "id": "416-release-notes/0.7#install", "title": "Zero 0.7", "searchTitle": "Install", "sectionTitle": "Install", @@ -5145,7 +5243,7 @@ "kind": "section" }, { - "id": "409-release-notes/0.7#features", + "id": "417-release-notes/0.7#features", "title": "Zero 0.7", "searchTitle": "Features", "sectionTitle": "Features", @@ -5155,7 +5253,7 @@ "kind": "section" }, { - "id": "410-release-notes/0.7#breaking-changes", + "id": "418-release-notes/0.7#breaking-changes", "title": "Zero 0.7", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5165,7 +5263,7 @@ "kind": "section" }, { - "id": "411-release-notes/0.7#zbugs", + "id": "419-release-notes/0.7#zbugs", "title": "Zero 0.7", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -5175,7 +5273,7 @@ "kind": "section" }, { - "id": "412-release-notes/0.7#docs", + "id": "420-release-notes/0.7#docs", "title": "Zero 0.7", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -5211,7 +5309,7 @@ "kind": "page" }, { - "id": "413-release-notes/0.8#install", + "id": "421-release-notes/0.8#install", "title": "Zero 0.8", "searchTitle": "Install", "sectionTitle": "Install", @@ -5221,7 +5319,7 @@ "kind": "section" }, { - "id": "414-release-notes/0.8#features", + "id": "422-release-notes/0.8#features", "title": "Zero 0.8", "searchTitle": "Features", "sectionTitle": "Features", @@ -5231,7 +5329,7 @@ "kind": "section" }, { - "id": "415-release-notes/0.8#fixes", + "id": "423-release-notes/0.8#fixes", "title": "Zero 0.8", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5241,7 +5339,7 @@ "kind": "section" }, { - "id": "416-release-notes/0.8#breaking-changes", + "id": "424-release-notes/0.8#breaking-changes", "title": "Zero 0.8", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5277,7 +5375,7 @@ "kind": "page" }, { - "id": "417-release-notes/0.9#install", + "id": "425-release-notes/0.9#install", "title": "Zero 0.9", "searchTitle": "Install", "sectionTitle": "Install", @@ -5287,7 +5385,7 @@ "kind": "section" }, { - "id": "418-release-notes/0.9#features", + "id": "426-release-notes/0.9#features", "title": "Zero 0.9", "searchTitle": "Features", "sectionTitle": "Features", @@ -5297,7 +5395,7 @@ "kind": "section" }, { - "id": "419-release-notes/0.9#fixes", + "id": "427-release-notes/0.9#fixes", "title": "Zero 0.9", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5307,7 +5405,7 @@ "kind": "section" }, { - "id": "420-release-notes/0.9#breaking-changes", + "id": "428-release-notes/0.9#breaking-changes", "title": "Zero 0.9", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5347,7 +5445,7 @@ "kind": "page" }, { - "id": "421-release-notes/1.0#installation", + "id": "429-release-notes/1.0#installation", "title": "Zero 1.0", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5357,7 +5455,7 @@ "kind": "section" }, { - "id": "422-release-notes/1.0#overview", + "id": "430-release-notes/1.0#overview", "title": "Zero 1.0", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -5367,7 +5465,7 @@ "kind": "section" }, { - "id": "423-release-notes/1.0#features", + "id": "431-release-notes/1.0#features", "title": "Zero 1.0", "searchTitle": "Features", "sectionTitle": "Features", @@ -5377,7 +5475,7 @@ "kind": "section" }, { - "id": "424-release-notes/1.0#fixes", + "id": "432-release-notes/1.0#fixes", "title": "Zero 1.0", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5387,7 +5485,7 @@ "kind": "section" }, { - "id": "425-release-notes/1.0#breaking-changes", + "id": "433-release-notes/1.0#breaking-changes", "title": "Zero 1.0", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5423,7 +5521,7 @@ "kind": "page" }, { - "id": "426-release-notes/1.1#installation", + "id": "434-release-notes/1.1#installation", "title": "Zero 1.1", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5433,7 +5531,7 @@ "kind": "section" }, { - "id": "427-release-notes/1.1#features", + "id": "435-release-notes/1.1#features", "title": "Zero 1.1", "searchTitle": "Features", "sectionTitle": "Features", @@ -5443,7 +5541,7 @@ "kind": "section" }, { - "id": "428-release-notes/1.1#fixes", + "id": "436-release-notes/1.1#fixes", "title": "Zero 1.1", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5453,7 +5551,7 @@ "kind": "section" }, { - "id": "429-release-notes/1.1#breaking-changes", + "id": "437-release-notes/1.1#breaking-changes", "title": "Zero 1.1", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5505,7 +5603,7 @@ "kind": "page" }, { - "id": "430-release-notes/1.10#installation", + "id": "438-release-notes/1.10#installation", "title": "Zero 1.10", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5515,7 +5613,7 @@ "kind": "section" }, { - "id": "431-release-notes/1.10#overview", + "id": "439-release-notes/1.10#overview", "title": "Zero 1.10", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -5525,7 +5623,7 @@ "kind": "section" }, { - "id": "432-release-notes/1.10#features", + "id": "440-release-notes/1.10#features", "title": "Zero 1.10", "searchTitle": "Features", "sectionTitle": "Features", @@ -5535,7 +5633,7 @@ "kind": "section" }, { - "id": "433-release-notes/1.10#fixes", + "id": "441-release-notes/1.10#fixes", "title": "Zero 1.10", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5545,7 +5643,7 @@ "kind": "section" }, { - "id": "434-release-notes/1.10#queries-and-connections", + "id": "442-release-notes/1.10#queries-and-connections", "title": "Zero 1.10", "searchTitle": "Queries and Connections", "sectionTitle": "Queries and Connections", @@ -5555,7 +5653,7 @@ "kind": "section" }, { - "id": "435-release-notes/1.10#replication-and-backfills", + "id": "443-release-notes/1.10#replication-and-backfills", "title": "Zero 1.10", "searchTitle": "Replication and Backfills", "sectionTitle": "Replication and Backfills", @@ -5565,7 +5663,7 @@ "kind": "section" }, { - "id": "436-release-notes/1.10#backups-and-startup", + "id": "444-release-notes/1.10#backups-and-startup", "title": "Zero 1.10", "searchTitle": "Backups and Startup", "sectionTitle": "Backups and Startup", @@ -5575,7 +5673,7 @@ "kind": "section" }, { - "id": "437-release-notes/1.10#breaking-changes", + "id": "445-release-notes/1.10#breaking-changes", "title": "Zero 1.10", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5585,7 +5683,227 @@ "kind": "section" }, { - "id": "55-release-notes/1.2", + "id": "55-release-notes/1.11", + "title": "Zero 1.11", + "searchTitle": "Zero 1.11", + "url": "/docs/release-notes/1.11", + "content": "Installation npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Overview Zero 1.11 shows cached query results sooner, recovers from client storage failures, and makes Litestream v5 the default for backups. It also speeds up queries on the client and server and fixes a long list of correctness and reliability issues. Features Queries whose results a previous session stored locally now start with the new 'cached' result type, so apps can show stored data right away and still tell it apart from data confirmed by the server. (#6578) (thanks @pawarren!) Building the same query again usually returns the same Query object, so useQuery no longer rebuilds and rehashes its query on every render, and queries built inline can be used as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero now starts in a new 'initializing' connection state while it loads local data. Slow local loading no longer counts against the connect timeout. (#6561) When local storage fails to open, fills up, or reports I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore reports which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Upgrading from 1.10 keeps each client's local data and its pending mutations. Future protocol changes no longer start clients over with an empty local database. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when they fail with an error that tx.retryOn() accepts, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. Replicas restored from another replica's backup continue it incrementally, periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits until replication has caught up before reporting ready. (1c5ddcdb4) Inactive client groups are now kept for 7 days instead of 48 hours, so clients that return within a week avoid a full resync. (#6484) New tools for diagnosing slow queries: zero-cache logs which queries are slow and why, with index suggestions, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) Opt-in limits protect zero-cache from expensive queries: a hydration budget for reconnecting clients and a per-query hydration timeout. (#6477, #6499) New OpenTelemetry metrics for WAL usage, index-build progress in replication status events, and restore progress in a new view-syncer status event. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image shrank from about 1.09 GB to 807 MB. (#6695) Performance Server-Side Incremental Updates When a transaction replicates, zero-cache brings every client group's queries up to date with it. Zero 1.11 does this about 2.5x faster: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU. Client Cold Start Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster. Building and Hashing Queries Components build their queries on every render, and Zero hashes each query to identify it. Building the same query now returns the same object with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size. Slower in Zero 1.11: Client Writes With Many Joined Queries One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped (#6645, #6708). When every change reaches some parent, as here, keeping that index up to date is extra work. Fixes Query Results Queries using or with a flipped exists no longer return wrong rows. (#6636) Joins no longer leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery are now synced, and changes to them propagate. (913c0665f) Catching up a client after a large update no longer fails with Missing row, and a schema change during rehydration no longer fails with table has no column named. (#6662, #6616) Queries whose SQLite plan nests one multi-index OR inside another no longer crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache no longer calls ZERO_QUERY_URL again for unchanged custom queries when it rebuilds its query pipelines. Permission changes still re-transform every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments no longer share one view. A SolidJS view rebuilt after its query changes uses the current TTL. (#6461, #6497) In mutators, a row just inserted without its optional columns now reads back with those columns as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) no longer return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients no longer log ChunkNotFoundError during persist, and a write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database now deletes the right file. (#6656) (thanks @pawarren!) Clients no longer go offline after the app resumes from the background or the computer wakes from sleep, and React Native connect timeouts retry instead of leaving the client stuck in an error state. (#6510, #6485) (thanks @pawarren!) Slow local startup no longer makes a healthy server look unreachable. (#6475) A mutator that throws while being replayed no longer disconnects the client in a loop. The mutation is still sent to the server. (#6555) Client memory no longer grows with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing are logged as warnings or info instead of errors, and fatal errors now include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability Fixed memory and resource leaks in zero-cache around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) Fixed view-syncer crashes and Internal errors when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg no longer crash when Postgres closes a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector no longer runs a view-syncer out of memory on large tables. (#6596) Error messages no longer include row values, query literals or mutation arguments. (#6413) Replication, Backfills, and Backups Replication no longer rolls back or stops after an interrupted stream, and a replication-manager whose slot was invalidated now resyncs instead of crash-looping. (#6562, #6590) Backfills no longer apply rows before replication has caught up to them, resume when a view-syncer moves to another replication-manager, and no longer leak a Postgres connection that holds back vacuum. (#6685, #6684, #6513) Replication slots are more robust: creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag is now measured correctly on Aurora. (e6a2590db) After initial sync, the replication-manager waits for a backup that covers its replica before serving, so restoring view-syncers no longer fall back to Postgres. (#6465) Fewer unnecessary query pipeline resets, and dropping an upstream index now refreshes table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager keeps serving until the new one takes over. (21f349135) Change-database connections no longer leak after a failed catch-up, and a change-database reset no longer hangs. (#6529, #6632) Litestream's upload concurrency now scales with available memory, avoiding out-of-memory crashes in small containers. (240c05730) Configuration values that start with a dash, such as some shard IDs, are now parsed correctly. (81e6ac570) In the zero Docker image, initial sync and backfills no longer leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe) Breaking Changes Legacy queries: zero-cache no longer accepts queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: code that switches over every result type or connection state must handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options, which no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours instead of 48, replication lag is reported every 10 seconds instead of 30, and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 instead of 2. Startup can take longer while zero-cache waits for a backup and for replication to catch up; give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: mutator return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates for tx.retryOn() to work. Using tx after its transaction ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL no longer apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they had not yet sent are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Mutations that were not yet sent are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after an upgrade, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE; use ZERO_CHANGE_STREAMER_URI. (698286b55) Events: a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", + "headings": [ + { + "text": "Installation", + "id": "installation" + }, + { + "text": "Overview", + "id": "overview" + }, + { + "text": "Features", + "id": "features" + }, + { + "text": "Performance", + "id": "performance" + }, + { + "text": "Server-Side Incremental Updates", + "id": "server-side-incremental-updates" + }, + { + "text": "Client Cold Start", + "id": "client-cold-start" + }, + { + "text": "Building and Hashing Queries", + "id": "building-and-hashing-queries" + }, + { + "text": "Slower in Zero 1.11: Client Writes With Many Joined Queries", + "id": "slower-in-zero-111-client-writes-with-many-joined-queries" + }, + { + "text": "Fixes", + "id": "fixes" + }, + { + "text": "Query Results", + "id": "query-results" + }, + { + "text": "Clients and React Native", + "id": "clients-and-react-native" + }, + { + "text": "zero-cache Reliability", + "id": "zero-cache-reliability" + }, + { + "text": "Replication, Backfills, and Backups", + "id": "replication-backfills-and-backups" + }, + { + "text": "Type Checking", + "id": "type-checking" + }, + { + "text": "Breaking Changes", + "id": "breaking-changes" + } + ], + "kind": "page" + }, + { + "id": "446-release-notes/1.11#installation", + "title": "Zero 1.11", + "searchTitle": "Installation", + "sectionTitle": "Installation", + "sectionId": "installation", + "url": "/docs/release-notes/1.11", + "content": "npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first.", + "kind": "section" + }, + { + "id": "447-release-notes/1.11#overview", + "title": "Zero 1.11", + "searchTitle": "Overview", + "sectionTitle": "Overview", + "sectionId": "overview", + "url": "/docs/release-notes/1.11", + "content": "Zero 1.11 shows cached query results sooner, recovers from client storage failures, and makes Litestream v5 the default for backups. It also speeds up queries on the client and server and fixes a long list of correctness and reliability issues.", + "kind": "section" + }, + { + "id": "448-release-notes/1.11#features", + "title": "Zero 1.11", + "searchTitle": "Features", + "sectionTitle": "Features", + "sectionId": "features", + "url": "/docs/release-notes/1.11", + "content": "Queries whose results a previous session stored locally now start with the new 'cached' result type, so apps can show stored data right away and still tell it apart from data confirmed by the server. (#6578) (thanks @pawarren!) Building the same query again usually returns the same Query object, so useQuery no longer rebuilds and rehashes its query on every render, and queries built inline can be used as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero now starts in a new 'initializing' connection state while it loads local data. Slow local loading no longer counts against the connect timeout. (#6561) When local storage fails to open, fills up, or reports I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore reports which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Upgrading from 1.10 keeps each client's local data and its pending mutations. Future protocol changes no longer start clients over with an empty local database. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when they fail with an error that tx.retryOn() accepts, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. Replicas restored from another replica's backup continue it incrementally, periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits until replication has caught up before reporting ready. (1c5ddcdb4) Inactive client groups are now kept for 7 days instead of 48 hours, so clients that return within a week avoid a full resync. (#6484) New tools for diagnosing slow queries: zero-cache logs which queries are slow and why, with index suggestions, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) Opt-in limits protect zero-cache from expensive queries: a hydration budget for reconnecting clients and a per-query hydration timeout. (#6477, #6499) New OpenTelemetry metrics for WAL usage, index-build progress in replication status events, and restore progress in a new view-syncer status event. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image shrank from about 1.09 GB to 807 MB. (#6695)", + "kind": "section" + }, + { + "id": "449-release-notes/1.11#performance", + "title": "Zero 1.11", + "searchTitle": "Performance", + "sectionTitle": "Performance", + "sectionId": "performance", + "url": "/docs/release-notes/1.11", + "content": "Server-Side Incremental Updates When a transaction replicates, zero-cache brings every client group's queries up to date with it. Zero 1.11 does this about 2.5x faster: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU. Client Cold Start Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster. Building and Hashing Queries Components build their queries on every render, and Zero hashes each query to identify it. Building the same query now returns the same object with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size. Slower in Zero 1.11: Client Writes With Many Joined Queries One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped (#6645, #6708). When every change reaches some parent, as here, keeping that index up to date is extra work.", + "kind": "section" + }, + { + "id": "450-release-notes/1.11#server-side-incremental-updates", + "title": "Zero 1.11", + "searchTitle": "Server-Side Incremental Updates", + "sectionTitle": "Server-Side Incremental Updates", + "sectionId": "server-side-incremental-updates", + "url": "/docs/release-notes/1.11", + "content": "When a transaction replicates, zero-cache brings every client group's queries up to date with it. Zero 1.11 does this about 2.5x faster: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU.", + "kind": "section" + }, + { + "id": "451-release-notes/1.11#client-cold-start", + "title": "Zero 1.11", + "searchTitle": "Client Cold Start", + "sectionTitle": "Client Cold Start", + "sectionId": "client-cold-start", + "url": "/docs/release-notes/1.11", + "content": "Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster.", + "kind": "section" + }, + { + "id": "452-release-notes/1.11#building-and-hashing-queries", + "title": "Zero 1.11", + "searchTitle": "Building and Hashing Queries", + "sectionTitle": "Building and Hashing Queries", + "sectionId": "building-and-hashing-queries", + "url": "/docs/release-notes/1.11", + "content": "Components build their queries on every render, and Zero hashes each query to identify it. Building the same query now returns the same object with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size.", + "kind": "section" + }, + { + "id": "453-release-notes/1.11#slower-in-zero-111-client-writes-with-many-joined-queries", + "title": "Zero 1.11", + "searchTitle": "Slower in Zero 1.11: Client Writes With Many Joined Queries", + "sectionTitle": "Slower in Zero 1.11: Client Writes With Many Joined Queries", + "sectionId": "slower-in-zero-111-client-writes-with-many-joined-queries", + "url": "/docs/release-notes/1.11", + "content": "One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped (#6645, #6708). When every change reaches some parent, as here, keeping that index up to date is extra work.", + "kind": "section" + }, + { + "id": "454-release-notes/1.11#fixes", + "title": "Zero 1.11", + "searchTitle": "Fixes", + "sectionTitle": "Fixes", + "sectionId": "fixes", + "url": "/docs/release-notes/1.11", + "content": "Query Results Queries using or with a flipped exists no longer return wrong rows. (#6636) Joins no longer leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery are now synced, and changes to them propagate. (913c0665f) Catching up a client after a large update no longer fails with Missing row, and a schema change during rehydration no longer fails with table has no column named. (#6662, #6616) Queries whose SQLite plan nests one multi-index OR inside another no longer crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache no longer calls ZERO_QUERY_URL again for unchanged custom queries when it rebuilds its query pipelines. Permission changes still re-transform every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments no longer share one view. A SolidJS view rebuilt after its query changes uses the current TTL. (#6461, #6497) In mutators, a row just inserted without its optional columns now reads back with those columns as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) no longer return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients no longer log ChunkNotFoundError during persist, and a write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database now deletes the right file. (#6656) (thanks @pawarren!) Clients no longer go offline after the app resumes from the background or the computer wakes from sleep, and React Native connect timeouts retry instead of leaving the client stuck in an error state. (#6510, #6485) (thanks @pawarren!) Slow local startup no longer makes a healthy server look unreachable. (#6475) A mutator that throws while being replayed no longer disconnects the client in a loop. The mutation is still sent to the server. (#6555) Client memory no longer grows with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing are logged as warnings or info instead of errors, and fatal errors now include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability Fixed memory and resource leaks in zero-cache around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) Fixed view-syncer crashes and Internal errors when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg no longer crash when Postgres closes a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector no longer runs a view-syncer out of memory on large tables. (#6596) Error messages no longer include row values, query literals or mutation arguments. (#6413) Replication, Backfills, and Backups Replication no longer rolls back or stops after an interrupted stream, and a replication-manager whose slot was invalidated now resyncs instead of crash-looping. (#6562, #6590) Backfills no longer apply rows before replication has caught up to them, resume when a view-syncer moves to another replication-manager, and no longer leak a Postgres connection that holds back vacuum. (#6685, #6684, #6513) Replication slots are more robust: creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag is now measured correctly on Aurora. (e6a2590db) After initial sync, the replication-manager waits for a backup that covers its replica before serving, so restoring view-syncers no longer fall back to Postgres. (#6465) Fewer unnecessary query pipeline resets, and dropping an upstream index now refreshes table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager keeps serving until the new one takes over. (21f349135) Change-database connections no longer leak after a failed catch-up, and a change-database reset no longer hangs. (#6529, #6632) Litestream's upload concurrency now scales with available memory, avoiding out-of-memory crashes in small containers. (240c05730) Configuration values that start with a dash, such as some shard IDs, are now parsed correctly. (81e6ac570) In the zero Docker image, initial sync and backfills no longer leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe)", + "kind": "section" + }, + { + "id": "455-release-notes/1.11#query-results", + "title": "Zero 1.11", + "searchTitle": "Query Results", + "sectionTitle": "Query Results", + "sectionId": "query-results", + "url": "/docs/release-notes/1.11", + "content": "Queries using or with a flipped exists no longer return wrong rows. (#6636) Joins no longer leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery are now synced, and changes to them propagate. (913c0665f) Catching up a client after a large update no longer fails with Missing row, and a schema change during rehydration no longer fails with table has no column named. (#6662, #6616) Queries whose SQLite plan nests one multi-index OR inside another no longer crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache no longer calls ZERO_QUERY_URL again for unchanged custom queries when it rebuilds its query pipelines. Permission changes still re-transform every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments no longer share one view. A SolidJS view rebuilt after its query changes uses the current TTL. (#6461, #6497) In mutators, a row just inserted without its optional columns now reads back with those columns as null. (d4816752c)", + "kind": "section" + }, + { + "id": "456-release-notes/1.11#clients-and-react-native", + "title": "Zero 1.11", + "searchTitle": "Clients and React Native", + "sectionTitle": "Clients and React Native", + "sectionId": "clients-and-react-native", + "url": "/docs/release-notes/1.11", + "content": "SQLite-backed stores (expo-sqlite, op-sqlite) no longer return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients no longer log ChunkNotFoundError during persist, and a write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database now deletes the right file. (#6656) (thanks @pawarren!) Clients no longer go offline after the app resumes from the background or the computer wakes from sleep, and React Native connect timeouts retry instead of leaving the client stuck in an error state. (#6510, #6485) (thanks @pawarren!) Slow local startup no longer makes a healthy server look unreachable. (#6475) A mutator that throws while being replayed no longer disconnects the client in a loop. The mutation is still sent to the server. (#6555) Client memory no longer grows with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing are logged as warnings or info instead of errors, and fatal errors now include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!)", + "kind": "section" + }, + { + "id": "457-release-notes/1.11#zero-cache-reliability", + "title": "Zero 1.11", + "searchTitle": "zero-cache Reliability", + "sectionTitle": "zero-cache Reliability", + "sectionId": "zero-cache-reliability", + "url": "/docs/release-notes/1.11", + "content": "Fixed memory and resource leaks in zero-cache around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) Fixed view-syncer crashes and Internal errors when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg no longer crash when Postgres closes a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector no longer runs a view-syncer out of memory on large tables. (#6596) Error messages no longer include row values, query literals or mutation arguments. (#6413)", + "kind": "section" + }, + { + "id": "458-release-notes/1.11#replication-backfills-and-backups", + "title": "Zero 1.11", + "searchTitle": "Replication, Backfills, and Backups", + "sectionTitle": "Replication, Backfills, and Backups", + "sectionId": "replication-backfills-and-backups", + "url": "/docs/release-notes/1.11", + "content": "Replication no longer rolls back or stops after an interrupted stream, and a replication-manager whose slot was invalidated now resyncs instead of crash-looping. (#6562, #6590) Backfills no longer apply rows before replication has caught up to them, resume when a view-syncer moves to another replication-manager, and no longer leak a Postgres connection that holds back vacuum. (#6685, #6684, #6513) Replication slots are more robust: creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag is now measured correctly on Aurora. (e6a2590db) After initial sync, the replication-manager waits for a backup that covers its replica before serving, so restoring view-syncers no longer fall back to Postgres. (#6465) Fewer unnecessary query pipeline resets, and dropping an upstream index now refreshes table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager keeps serving until the new one takes over. (21f349135) Change-database connections no longer leak after a failed catch-up, and a change-database reset no longer hangs. (#6529, #6632) Litestream's upload concurrency now scales with available memory, avoiding out-of-memory crashes in small containers. (240c05730) Configuration values that start with a dash, such as some shard IDs, are now parsed correctly. (81e6ac570) In the zero Docker image, initial sync and backfills no longer leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e)", + "kind": "section" + }, + { + "id": "459-release-notes/1.11#type-checking", + "title": "Zero 1.11", + "searchTitle": "Type Checking", + "sectionTitle": "Type Checking", + "sectionId": "type-checking", + "url": "/docs/release-notes/1.11", + "content": "A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe)", + "kind": "section" + }, + { + "id": "460-release-notes/1.11#breaking-changes", + "title": "Zero 1.11", + "searchTitle": "Breaking Changes", + "sectionTitle": "Breaking Changes", + "sectionId": "breaking-changes", + "url": "/docs/release-notes/1.11", + "content": "Legacy queries: zero-cache no longer accepts queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: code that switches over every result type or connection state must handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options, which no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours instead of 48, replication lag is reported every 10 seconds instead of 30, and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 instead of 2. Startup can take longer while zero-cache waits for a backup and for replication to catch up; give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: mutator return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates for tx.retryOn() to work. Using tx after its transaction ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL no longer apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they had not yet sent are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Mutations that were not yet sent are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after an upgrade, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE; use ZERO_CHANGE_STREAMER_URI. (698286b55) Events: a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", + "kind": "section" + }, + { + "id": "56-release-notes/1.2", "title": "Zero 1.2", "searchTitle": "Zero 1.2", "url": "/docs/release-notes/1.2", @@ -5615,7 +5933,7 @@ "kind": "page" }, { - "id": "438-release-notes/1.2#installation", + "id": "461-release-notes/1.2#installation", "title": "Zero 1.2", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5625,7 +5943,7 @@ "kind": "section" }, { - "id": "439-release-notes/1.2#features", + "id": "462-release-notes/1.2#features", "title": "Zero 1.2", "searchTitle": "Features", "sectionTitle": "Features", @@ -5635,7 +5953,7 @@ "kind": "section" }, { - "id": "440-release-notes/1.2#performance", + "id": "463-release-notes/1.2#performance", "title": "Zero 1.2", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -5645,7 +5963,7 @@ "kind": "section" }, { - "id": "441-release-notes/1.2#fixes", + "id": "464-release-notes/1.2#fixes", "title": "Zero 1.2", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5655,7 +5973,7 @@ "kind": "section" }, { - "id": "442-release-notes/1.2#breaking-changes", + "id": "465-release-notes/1.2#breaking-changes", "title": "Zero 1.2", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5665,7 +5983,7 @@ "kind": "section" }, { - "id": "56-release-notes/1.3", + "id": "57-release-notes/1.3", "title": "Zero 1.3", "searchTitle": "Zero 1.3", "url": "/docs/release-notes/1.3", @@ -5695,7 +6013,7 @@ "kind": "page" }, { - "id": "443-release-notes/1.3#installation", + "id": "466-release-notes/1.3#installation", "title": "Zero 1.3", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5705,7 +6023,7 @@ "kind": "section" }, { - "id": "444-release-notes/1.3#features", + "id": "467-release-notes/1.3#features", "title": "Zero 1.3", "searchTitle": "Features", "sectionTitle": "Features", @@ -5715,7 +6033,7 @@ "kind": "section" }, { - "id": "445-release-notes/1.3#performance", + "id": "468-release-notes/1.3#performance", "title": "Zero 1.3", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -5725,7 +6043,7 @@ "kind": "section" }, { - "id": "446-release-notes/1.3#fixes", + "id": "469-release-notes/1.3#fixes", "title": "Zero 1.3", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5735,7 +6053,7 @@ "kind": "section" }, { - "id": "447-release-notes/1.3#breaking-changes", + "id": "470-release-notes/1.3#breaking-changes", "title": "Zero 1.3", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5745,7 +6063,7 @@ "kind": "section" }, { - "id": "57-release-notes/1.4", + "id": "58-release-notes/1.4", "title": "Zero 1.4", "searchTitle": "Zero 1.4", "url": "/docs/release-notes/1.4", @@ -5783,7 +6101,7 @@ "kind": "page" }, { - "id": "448-release-notes/1.4#installation", + "id": "471-release-notes/1.4#installation", "title": "Zero 1.4", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5793,7 +6111,7 @@ "kind": "section" }, { - "id": "449-release-notes/1.4#upgrading", + "id": "472-release-notes/1.4#upgrading", "title": "Zero 1.4", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -5803,7 +6121,7 @@ "kind": "section" }, { - "id": "450-release-notes/1.4#userid-anon", + "id": "473-release-notes/1.4#userid-anon", "title": "Zero 1.4", "searchTitle": "userID: \"anon\"", "sectionTitle": "userID: \"anon\"", @@ -5813,7 +6131,7 @@ "kind": "section" }, { - "id": "451-release-notes/1.4#features", + "id": "474-release-notes/1.4#features", "title": "Zero 1.4", "searchTitle": "Features", "sectionTitle": "Features", @@ -5823,7 +6141,7 @@ "kind": "section" }, { - "id": "452-release-notes/1.4#performance", + "id": "475-release-notes/1.4#performance", "title": "Zero 1.4", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -5833,7 +6151,7 @@ "kind": "section" }, { - "id": "453-release-notes/1.4#fixes", + "id": "476-release-notes/1.4#fixes", "title": "Zero 1.4", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5843,7 +6161,7 @@ "kind": "section" }, { - "id": "454-release-notes/1.4#breaking-changes", + "id": "477-release-notes/1.4#breaking-changes", "title": "Zero 1.4", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5853,7 +6171,7 @@ "kind": "section" }, { - "id": "58-release-notes/1.5", + "id": "59-release-notes/1.5", "title": "Zero 1.5", "searchTitle": "Zero 1.5", "url": "/docs/release-notes/1.5", @@ -5895,7 +6213,7 @@ "kind": "page" }, { - "id": "455-release-notes/1.5#installation", + "id": "478-release-notes/1.5#installation", "title": "Zero 1.5", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5905,7 +6223,7 @@ "kind": "section" }, { - "id": "456-release-notes/1.5#upgrading", + "id": "479-release-notes/1.5#upgrading", "title": "Zero 1.5", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -5915,7 +6233,7 @@ "kind": "section" }, { - "id": "457-release-notes/1.5#authenticated-client-groups", + "id": "480-release-notes/1.5#authenticated-client-groups", "title": "Zero 1.5", "searchTitle": "Authenticated Client Groups", "sectionTitle": "Authenticated Client Groups", @@ -5925,7 +6243,7 @@ "kind": "section" }, { - "id": "458-release-notes/1.5#deploy-order", + "id": "481-release-notes/1.5#deploy-order", "title": "Zero 1.5", "searchTitle": "Deploy Order", "sectionTitle": "Deploy Order", @@ -5935,7 +6253,7 @@ "kind": "section" }, { - "id": "459-release-notes/1.5#features", + "id": "482-release-notes/1.5#features", "title": "Zero 1.5", "searchTitle": "Features", "sectionTitle": "Features", @@ -5945,7 +6263,7 @@ "kind": "section" }, { - "id": "460-release-notes/1.5#performance", + "id": "483-release-notes/1.5#performance", "title": "Zero 1.5", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -5955,7 +6273,7 @@ "kind": "section" }, { - "id": "461-release-notes/1.5#fixes", + "id": "484-release-notes/1.5#fixes", "title": "Zero 1.5", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5965,7 +6283,7 @@ "kind": "section" }, { - "id": "462-release-notes/1.5#breaking-changes", + "id": "485-release-notes/1.5#breaking-changes", "title": "Zero 1.5", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5975,7 +6293,7 @@ "kind": "section" }, { - "id": "59-release-notes/1.6", + "id": "60-release-notes/1.6", "title": "Zero 1.6", "searchTitle": "Zero 1.6", "url": "/docs/release-notes/1.6", @@ -6013,7 +6331,7 @@ "kind": "page" }, { - "id": "463-release-notes/1.6#installation", + "id": "486-release-notes/1.6#installation", "title": "Zero 1.6", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6023,7 +6341,7 @@ "kind": "section" }, { - "id": "464-release-notes/1.6#upgrading", + "id": "487-release-notes/1.6#upgrading", "title": "Zero 1.6", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -6033,7 +6351,7 @@ "kind": "section" }, { - "id": "465-release-notes/1.6#planetscale-failover", + "id": "488-release-notes/1.6#planetscale-failover", "title": "Zero 1.6", "searchTitle": "PlanetScale Failover", "sectionTitle": "PlanetScale Failover", @@ -6043,7 +6361,7 @@ "kind": "section" }, { - "id": "466-release-notes/1.6#features", + "id": "489-release-notes/1.6#features", "title": "Zero 1.6", "searchTitle": "Features", "sectionTitle": "Features", @@ -6053,7 +6371,7 @@ "kind": "section" }, { - "id": "467-release-notes/1.6#performance", + "id": "490-release-notes/1.6#performance", "title": "Zero 1.6", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6063,7 +6381,7 @@ "kind": "section" }, { - "id": "468-release-notes/1.6#fixes", + "id": "491-release-notes/1.6#fixes", "title": "Zero 1.6", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6073,7 +6391,7 @@ "kind": "section" }, { - "id": "469-release-notes/1.6#breaking-changes", + "id": "492-release-notes/1.6#breaking-changes", "title": "Zero 1.6", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6083,7 +6401,7 @@ "kind": "section" }, { - "id": "60-release-notes/1.7", + "id": "61-release-notes/1.7", "title": "Zero 1.7", "searchTitle": "Zero 1.7", "url": "/docs/release-notes/1.7", @@ -6125,7 +6443,7 @@ "kind": "page" }, { - "id": "470-release-notes/1.7#installation", + "id": "493-release-notes/1.7#installation", "title": "Zero 1.7", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6135,7 +6453,7 @@ "kind": "section" }, { - "id": "471-release-notes/1.7#overview", + "id": "494-release-notes/1.7#overview", "title": "Zero 1.7", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6145,7 +6463,7 @@ "kind": "section" }, { - "id": "472-release-notes/1.7#features", + "id": "495-release-notes/1.7#features", "title": "Zero 1.7", "searchTitle": "Features", "sectionTitle": "Features", @@ -6155,7 +6473,7 @@ "kind": "section" }, { - "id": "473-release-notes/1.7#performance", + "id": "496-release-notes/1.7#performance", "title": "Zero 1.7", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6165,7 +6483,7 @@ "kind": "section" }, { - "id": "474-release-notes/1.7#replication", + "id": "497-release-notes/1.7#replication", "title": "Zero 1.7", "searchTitle": "Replication", "sectionTitle": "Replication", @@ -6175,7 +6493,7 @@ "kind": "section" }, { - "id": "475-release-notes/1.7#flipped-exists-queries", + "id": "498-release-notes/1.7#flipped-exists-queries", "title": "Zero 1.7", "searchTitle": "Flipped Exists Queries", "sectionTitle": "Flipped Exists Queries", @@ -6185,7 +6503,7 @@ "kind": "section" }, { - "id": "476-release-notes/1.7#fixes", + "id": "499-release-notes/1.7#fixes", "title": "Zero 1.7", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6195,7 +6513,7 @@ "kind": "section" }, { - "id": "477-release-notes/1.7#breaking-changes", + "id": "500-release-notes/1.7#breaking-changes", "title": "Zero 1.7", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6205,7 +6523,7 @@ "kind": "section" }, { - "id": "61-release-notes/1.8", + "id": "62-release-notes/1.8", "title": "Zero 1.8", "searchTitle": "Zero 1.8", "url": "/docs/release-notes/1.8", @@ -6251,7 +6569,7 @@ "kind": "page" }, { - "id": "478-release-notes/1.8#installation", + "id": "501-release-notes/1.8#installation", "title": "Zero 1.8", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6261,7 +6579,7 @@ "kind": "section" }, { - "id": "479-release-notes/1.8#overview", + "id": "502-release-notes/1.8#overview", "title": "Zero 1.8", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6271,7 +6589,7 @@ "kind": "section" }, { - "id": "480-release-notes/1.8#features", + "id": "503-release-notes/1.8#features", "title": "Zero 1.8", "searchTitle": "Features", "sectionTitle": "Features", @@ -6281,7 +6599,7 @@ "kind": "section" }, { - "id": "481-release-notes/1.8#performance", + "id": "504-release-notes/1.8#performance", "title": "Zero 1.8", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6291,7 +6609,7 @@ "kind": "section" }, { - "id": "482-release-notes/1.8#replicating-large-transactions", + "id": "505-release-notes/1.8#replicating-large-transactions", "title": "Zero 1.8", "searchTitle": "Replicating Large Transactions", "sectionTitle": "Replicating Large Transactions", @@ -6301,7 +6619,7 @@ "kind": "section" }, { - "id": "483-release-notes/1.8#maintaining-limit-queries", + "id": "506-release-notes/1.8#maintaining-limit-queries", "title": "Zero 1.8", "searchTitle": "Maintaining limit() Queries", "sectionTitle": "Maintaining limit() Queries", @@ -6311,7 +6629,7 @@ "kind": "section" }, { - "id": "484-release-notes/1.8#client-side-hydration", + "id": "507-release-notes/1.8#client-side-hydration", "title": "Zero 1.8", "searchTitle": "Client-Side Hydration", "sectionTitle": "Client-Side Hydration", @@ -6321,7 +6639,7 @@ "kind": "section" }, { - "id": "485-release-notes/1.8#fixes", + "id": "508-release-notes/1.8#fixes", "title": "Zero 1.8", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6331,7 +6649,7 @@ "kind": "section" }, { - "id": "486-release-notes/1.8#breaking-changes", + "id": "509-release-notes/1.8#breaking-changes", "title": "Zero 1.8", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6341,7 +6659,7 @@ "kind": "section" }, { - "id": "62-release-notes/1.9", + "id": "63-release-notes/1.9", "title": "Zero 1.9", "searchTitle": "Zero 1.9", "url": "/docs/release-notes/1.9", @@ -6375,7 +6693,7 @@ "kind": "page" }, { - "id": "487-release-notes/1.9#installation", + "id": "510-release-notes/1.9#installation", "title": "Zero 1.9", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6385,7 +6703,7 @@ "kind": "section" }, { - "id": "488-release-notes/1.9#overview", + "id": "511-release-notes/1.9#overview", "title": "Zero 1.9", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6395,7 +6713,7 @@ "kind": "section" }, { - "id": "489-release-notes/1.9#features", + "id": "512-release-notes/1.9#features", "title": "Zero 1.9", "searchTitle": "Features", "sectionTitle": "Features", @@ -6405,7 +6723,7 @@ "kind": "section" }, { - "id": "490-release-notes/1.9#performance", + "id": "513-release-notes/1.9#performance", "title": "Zero 1.9", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6415,7 +6733,7 @@ "kind": "section" }, { - "id": "491-release-notes/1.9#cold-mutation-latency", + "id": "514-release-notes/1.9#cold-mutation-latency", "title": "Zero 1.9", "searchTitle": "Cold Mutation Latency", "sectionTitle": "Cold Mutation Latency", @@ -6425,7 +6743,7 @@ "kind": "section" }, { - "id": "492-release-notes/1.9#fixes", + "id": "515-release-notes/1.9#fixes", "title": "Zero 1.9", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6435,16 +6753,16 @@ "kind": "section" }, { - "id": "63-release-notes", + "id": "64-release-notes", "title": "Release Notes", "searchTitle": "Release Notes", "url": "/docs/release-notes", - "content": "Zero 1.10: Unique Keys and More Reliable Replication Zero 1.9: Stability and Query Correctness Zero 1.8: Observability and Reliability Zero 1.7: Query Correctness and Performance Zero 1.6: PlanetScale Failover Support Zero 1.5: Schema Change Improvements and Client Group Auth Zero 1.4: Performance and Reliability Improvements Zero 1.3: Faster Initial Sync and Other Perf Improvements Zero 1.2: IVM Performance and Bug Fixes Zero 1.1: Replication Monitoring Zero 1.0: First Stable Release Zero 0.26: Schema Backfill and Scalar Subqueries Zero 0.25: DX Overhaul, Query Planning Zero 0.24: Join Flipping, Cookie Auth, Inspector Updates Zero 0.23: Synced Queries and React Native Support Zero 0.22: Simplified TTLs Zero 0.21: PG arrays, TanStack starter, and more Zero 0.20: Full Supabase support, performance improvements Zero 0.19: Many, many bugfixes and cleanups Zero 0.18: Custom Mutators Zero 0.17: Background Queries Zero 0.16: Lambda-Based Permission Deployment Zero 0.15: Live Permission Updates Zero 0.14: Name Mapping and Multischema Zero 0.13: Multinode and SST Zero 0.12: Circular Relationships Zero 0.11: Windows Zero 0.10: Remove Top-Level Await Zero 0.9: JWK Support Zero 0.8: Schema Autobuild, Result Types, and Enums Zero 0.7: Read Perms and Docker Zero 0.6: Relationship Filters Zero 0.5: JSON Columns Zero 0.4: Compound Filters Zero 0.3: Schema Migrations and Write Perms Zero 0.2: Skip Mode and Computed PKs Zero 0.1: First Release", + "content": "Zero 1.11: Cached Results and Sturdier Client Storage Zero 1.10: Unique Keys and More Reliable Replication Zero 1.9: Stability and Query Correctness Zero 1.8: Observability and Reliability Zero 1.7: Query Correctness and Performance Zero 1.6: PlanetScale Failover Support Zero 1.5: Schema Change Improvements and Client Group Auth Zero 1.4: Performance and Reliability Improvements Zero 1.3: Faster Initial Sync and Other Perf Improvements Zero 1.2: IVM Performance and Bug Fixes Zero 1.1: Replication Monitoring Zero 1.0: First Stable Release Zero 0.26: Schema Backfill and Scalar Subqueries Zero 0.25: DX Overhaul, Query Planning Zero 0.24: Join Flipping, Cookie Auth, Inspector Updates Zero 0.23: Synced Queries and React Native Support Zero 0.22: Simplified TTLs Zero 0.21: PG arrays, TanStack starter, and more Zero 0.20: Full Supabase support, performance improvements Zero 0.19: Many, many bugfixes and cleanups Zero 0.18: Custom Mutators Zero 0.17: Background Queries Zero 0.16: Lambda-Based Permission Deployment Zero 0.15: Live Permission Updates Zero 0.14: Name Mapping and Multischema Zero 0.13: Multinode and SST Zero 0.12: Circular Relationships Zero 0.11: Windows Zero 0.10: Remove Top-Level Await Zero 0.9: JWK Support Zero 0.8: Schema Autobuild, Result Types, and Enums Zero 0.7: Read Perms and Docker Zero 0.6: Relationship Filters Zero 0.5: JSON Columns Zero 0.4: Compound Filters Zero 0.3: Schema Migrations and Write Perms Zero 0.2: Skip Mode and Computed PKs Zero 0.1: First Release", "headings": [], "kind": "page" }, { - "id": "64-reporting-bugs", + "id": "65-reporting-bugs", "title": "Reporting Bugs", "searchTitle": "Reporting Bugs", "url": "/docs/reporting-bugs", @@ -6462,7 +6780,7 @@ "kind": "page" }, { - "id": "493-reporting-bugs#zbugs", + "id": "516-reporting-bugs#zbugs", "title": "Reporting Bugs", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -6472,7 +6790,7 @@ "kind": "section" }, { - "id": "494-reporting-bugs#discord", + "id": "517-reporting-bugs#discord", "title": "Reporting Bugs", "searchTitle": "Discord", "sectionTitle": "Discord", @@ -6482,7 +6800,7 @@ "kind": "section" }, { - "id": "65-rest", + "id": "66-rest", "title": "REST", "searchTitle": "REST", "url": "/docs/rest", @@ -6508,7 +6826,7 @@ "kind": "page" }, { - "id": "495-rest#pattern", + "id": "518-rest#pattern", "title": "REST", "searchTitle": "Pattern", "sectionTitle": "Pattern", @@ -6518,7 +6836,7 @@ "kind": "section" }, { - "id": "496-rest#tanstack-start-example", + "id": "519-rest#tanstack-start-example", "title": "REST", "searchTitle": "TanStack Start Example", "sectionTitle": "TanStack Start Example", @@ -6528,7 +6846,7 @@ "kind": "section" }, { - "id": "497-rest#openapi-generation", + "id": "520-rest#openapi-generation", "title": "REST", "searchTitle": "OpenAPI Generation", "sectionTitle": "OpenAPI Generation", @@ -6538,7 +6856,7 @@ "kind": "section" }, { - "id": "498-rest#full-working-example", + "id": "521-rest#full-working-example", "title": "REST", "searchTitle": "Full Working Example", "sectionTitle": "Full Working Example", @@ -6548,7 +6866,7 @@ "kind": "section" }, { - "id": "66-roadmap", + "id": "67-roadmap", "title": "Roadmap", "searchTitle": "Roadmap", "url": "/docs/roadmap", @@ -6566,7 +6884,7 @@ "kind": "page" }, { - "id": "499-roadmap#q4-2025", + "id": "522-roadmap#q4-2025", "title": "Roadmap", "searchTitle": "Q4 2025", "sectionTitle": "Q4 2025", @@ -6576,7 +6894,7 @@ "kind": "section" }, { - "id": "500-roadmap#beyond", + "id": "523-roadmap#beyond", "title": "Roadmap", "searchTitle": "Beyond", "sectionTitle": "Beyond", @@ -6586,7 +6904,7 @@ "kind": "section" }, { - "id": "67-samples", + "id": "68-samples", "title": "Samples", "searchTitle": "Samples", "url": "/docs/samples", @@ -6612,7 +6930,7 @@ "kind": "page" }, { - "id": "501-samples#gigabugs", + "id": "524-samples#gigabugs", "title": "Samples", "searchTitle": "Gigabugs", "sectionTitle": "Gigabugs", @@ -6622,7 +6940,7 @@ "kind": "section" }, { - "id": "502-samples#ztunes", + "id": "525-samples#ztunes", "title": "Samples", "searchTitle": "ztunes", "sectionTitle": "ztunes", @@ -6632,7 +6950,7 @@ "kind": "section" }, { - "id": "503-samples#zslack", + "id": "526-samples#zslack", "title": "Samples", "searchTitle": "zslack", "sectionTitle": "zslack", @@ -6642,7 +6960,7 @@ "kind": "section" }, { - "id": "504-samples#zero-music", + "id": "527-samples#zero-music", "title": "Samples", "searchTitle": "zero-music", "sectionTitle": "zero-music", @@ -6652,7 +6970,7 @@ "kind": "section" }, { - "id": "68-schema", + "id": "69-schema", "title": "Zero Schema", "searchTitle": "Zero Schema", "url": "/docs/schema", @@ -6782,7 +7100,7 @@ "kind": "page" }, { - "id": "505-schema#generating-from-database", + "id": "528-schema#generating-from-database", "title": "Zero Schema", "searchTitle": "Generating from Database", "sectionTitle": "Generating from Database", @@ -6792,7 +7110,7 @@ "kind": "section" }, { - "id": "506-schema#writing-by-hand", + "id": "529-schema#writing-by-hand", "title": "Zero Schema", "searchTitle": "Writing by Hand", "sectionTitle": "Writing by Hand", @@ -6802,7 +7120,7 @@ "kind": "section" }, { - "id": "507-schema#table-schemas", + "id": "530-schema#table-schemas", "title": "Zero Schema", "searchTitle": "Table Schemas", "sectionTitle": "Table Schemas", @@ -6812,7 +7130,7 @@ "kind": "section" }, { - "id": "508-schema#name-mapping", + "id": "531-schema#name-mapping", "title": "Zero Schema", "searchTitle": "Name Mapping", "sectionTitle": "Name Mapping", @@ -6822,7 +7140,7 @@ "kind": "section" }, { - "id": "509-schema#multiple-schemas", + "id": "532-schema#multiple-schemas", "title": "Zero Schema", "searchTitle": "Multiple Schemas", "sectionTitle": "Multiple Schemas", @@ -6832,7 +7150,7 @@ "kind": "section" }, { - "id": "510-schema#optional-columns", + "id": "533-schema#optional-columns", "title": "Zero Schema", "searchTitle": "Optional Columns", "sectionTitle": "Optional Columns", @@ -6842,7 +7160,7 @@ "kind": "section" }, { - "id": "511-schema#enumerations", + "id": "534-schema#enumerations", "title": "Zero Schema", "searchTitle": "Enumerations", "sectionTitle": "Enumerations", @@ -6852,7 +7170,7 @@ "kind": "section" }, { - "id": "512-schema#custom-json-types", + "id": "535-schema#custom-json-types", "title": "Zero Schema", "searchTitle": "Custom JSON Types", "sectionTitle": "Custom JSON Types", @@ -6862,7 +7180,7 @@ "kind": "section" }, { - "id": "513-schema#compound-primary-keys", + "id": "536-schema#compound-primary-keys", "title": "Zero Schema", "searchTitle": "Compound Primary Keys", "sectionTitle": "Compound Primary Keys", @@ -6872,7 +7190,7 @@ "kind": "section" }, { - "id": "514-schema#unique-keys", + "id": "537-schema#unique-keys", "title": "Zero Schema", "searchTitle": "Unique Keys", "sectionTitle": "Unique Keys", @@ -6882,7 +7200,7 @@ "kind": "section" }, { - "id": "515-schema#relationships", + "id": "538-schema#relationships", "title": "Zero Schema", "searchTitle": "Relationships", "sectionTitle": "Relationships", @@ -6892,7 +7210,7 @@ "kind": "section" }, { - "id": "516-schema#many-to-many-relationships", + "id": "539-schema#many-to-many-relationships", "title": "Zero Schema", "searchTitle": "Many-to-Many Relationships", "sectionTitle": "Many-to-Many Relationships", @@ -6902,7 +7220,7 @@ "kind": "section" }, { - "id": "517-schema#compound-keys-relationships", + "id": "540-schema#compound-keys-relationships", "title": "Zero Schema", "searchTitle": "Compound Keys Relationships", "sectionTitle": "Compound Keys Relationships", @@ -6912,7 +7230,7 @@ "kind": "section" }, { - "id": "518-schema#circular-relationships", + "id": "541-schema#circular-relationships", "title": "Zero Schema", "searchTitle": "Circular Relationships", "sectionTitle": "Circular Relationships", @@ -6922,7 +7240,7 @@ "kind": "section" }, { - "id": "519-schema#database-schemas", + "id": "542-schema#database-schemas", "title": "Zero Schema", "searchTitle": "Database Schemas", "sectionTitle": "Database Schemas", @@ -6932,7 +7250,7 @@ "kind": "section" }, { - "id": "520-schema#register-schema-type", + "id": "543-schema#register-schema-type", "title": "Zero Schema", "searchTitle": "Register Schema Type", "sectionTitle": "Register Schema Type", @@ -6942,7 +7260,7 @@ "kind": "section" }, { - "id": "521-schema#schema-changes", + "id": "544-schema#schema-changes", "title": "Zero Schema", "searchTitle": "Schema Changes", "sectionTitle": "Schema Changes", @@ -6952,7 +7270,7 @@ "kind": "section" }, { - "id": "522-schema#development", + "id": "545-schema#development", "title": "Zero Schema", "searchTitle": "Development", "sectionTitle": "Development", @@ -6962,7 +7280,7 @@ "kind": "section" }, { - "id": "523-schema#production", + "id": "546-schema#production", "title": "Zero Schema", "searchTitle": "Production", "sectionTitle": "Production", @@ -6972,7 +7290,7 @@ "kind": "section" }, { - "id": "524-schema#expand-changes", + "id": "547-schema#expand-changes", "title": "Zero Schema", "searchTitle": "Expand Changes", "sectionTitle": "Expand Changes", @@ -6982,7 +7300,7 @@ "kind": "section" }, { - "id": "525-schema#contract-changes", + "id": "548-schema#contract-changes", "title": "Zero Schema", "searchTitle": "Contract Changes", "sectionTitle": "Contract Changes", @@ -6992,7 +7310,7 @@ "kind": "section" }, { - "id": "526-schema#compound-changes", + "id": "549-schema#compound-changes", "title": "Zero Schema", "searchTitle": "Compound Changes", "sectionTitle": "Compound Changes", @@ -7002,7 +7320,7 @@ "kind": "section" }, { - "id": "527-schema#examples", + "id": "550-schema#examples", "title": "Zero Schema", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -7012,7 +7330,7 @@ "kind": "section" }, { - "id": "528-schema#adding-a-column", + "id": "551-schema#adding-a-column", "title": "Zero Schema", "searchTitle": "Adding a Column", "sectionTitle": "Adding a Column", @@ -7022,7 +7340,7 @@ "kind": "section" }, { - "id": "529-schema#removing-a-column", + "id": "552-schema#removing-a-column", "title": "Zero Schema", "searchTitle": "Removing a Column", "sectionTitle": "Removing a Column", @@ -7032,7 +7350,7 @@ "kind": "section" }, { - "id": "530-schema#renaming-a-column", + "id": "553-schema#renaming-a-column", "title": "Zero Schema", "searchTitle": "Renaming a Column", "sectionTitle": "Renaming a Column", @@ -7042,7 +7360,7 @@ "kind": "section" }, { - "id": "531-schema#making-a-column-optional", + "id": "554-schema#making-a-column-optional", "title": "Zero Schema", "searchTitle": "Making a Column Optional", "sectionTitle": "Making a Column Optional", @@ -7052,7 +7370,7 @@ "kind": "section" }, { - "id": "532-schema#quick-reference", + "id": "555-schema#quick-reference", "title": "Zero Schema", "searchTitle": "Quick Reference", "sectionTitle": "Quick Reference", @@ -7062,7 +7380,7 @@ "kind": "section" }, { - "id": "533-schema#backfill", + "id": "556-schema#backfill", "title": "Zero Schema", "searchTitle": "Backfill", "sectionTitle": "Backfill", @@ -7072,7 +7390,7 @@ "kind": "section" }, { - "id": "534-schema#monitoring-backfill-progress", + "id": "557-schema#monitoring-backfill-progress", "title": "Zero Schema", "searchTitle": "Monitoring Backfill Progress", "sectionTitle": "Monitoring Backfill Progress", @@ -7082,7 +7400,7 @@ "kind": "section" }, { - "id": "69-self-host", + "id": "70-self-host", "title": "Self-Hosting Zero", "searchTitle": "Self-Hosting Zero", "url": "/docs/self-host", @@ -7144,7 +7462,7 @@ "kind": "page" }, { - "id": "535-self-host#docker-images", + "id": "558-self-host#docker-images", "title": "Self-Hosting Zero", "searchTitle": "Docker Images", "sectionTitle": "Docker Images", @@ -7154,7 +7472,7 @@ "kind": "section" }, { - "id": "536-self-host#minimum-viable-strategy", + "id": "559-self-host#minimum-viable-strategy", "title": "Self-Hosting Zero", "searchTitle": "Minimum Viable Strategy", "sectionTitle": "Minimum Viable Strategy", @@ -7164,7 +7482,7 @@ "kind": "section" }, { - "id": "537-self-host#maximal-strategy", + "id": "560-self-host#maximal-strategy", "title": "Self-Hosting Zero", "searchTitle": "Maximal Strategy", "sectionTitle": "Maximal Strategy", @@ -7174,7 +7492,7 @@ "kind": "section" }, { - "id": "538-self-host#replica-lifecycle", + "id": "561-self-host#replica-lifecycle", "title": "Self-Hosting Zero", "searchTitle": "Replica Lifecycle", "sectionTitle": "Replica Lifecycle", @@ -7184,7 +7502,7 @@ "kind": "section" }, { - "id": "539-self-host#performance", + "id": "562-self-host#performance", "title": "Self-Hosting Zero", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -7194,7 +7512,7 @@ "kind": "section" }, { - "id": "540-self-host#hydration", + "id": "563-self-host#hydration", "title": "Self-Hosting Zero", "searchTitle": "Hydration", "sectionTitle": "Hydration", @@ -7204,7 +7522,7 @@ "kind": "section" }, { - "id": "541-self-host#ivm-advancement", + "id": "564-self-host#ivm-advancement", "title": "Self-Hosting Zero", "searchTitle": "IVM advancement", "sectionTitle": "IVM advancement", @@ -7214,7 +7532,7 @@ "kind": "section" }, { - "id": "542-self-host#system-level", + "id": "565-self-host#system-level", "title": "Self-Hosting Zero", "searchTitle": "System-level", "sectionTitle": "System-level", @@ -7224,7 +7542,7 @@ "kind": "section" }, { - "id": "543-self-host#networking", + "id": "566-self-host#networking", "title": "Self-Hosting Zero", "searchTitle": "Networking", "sectionTitle": "Networking", @@ -7234,7 +7552,7 @@ "kind": "section" }, { - "id": "544-self-host#sticky-sessions", + "id": "567-self-host#sticky-sessions", "title": "Self-Hosting Zero", "searchTitle": "Sticky Sessions", "sectionTitle": "Sticky Sessions", @@ -7244,7 +7562,7 @@ "kind": "section" }, { - "id": "545-self-host#rolling-updates", + "id": "568-self-host#rolling-updates", "title": "Self-Hosting Zero", "searchTitle": "Rolling Updates", "sectionTitle": "Rolling Updates", @@ -7254,7 +7572,7 @@ "kind": "section" }, { - "id": "546-self-host#clientserver-version-compatibility", + "id": "569-self-host#clientserver-version-compatibility", "title": "Self-Hosting Zero", "searchTitle": "Client/Server Version Compatibility", "sectionTitle": "Client/Server Version Compatibility", @@ -7264,7 +7582,7 @@ "kind": "section" }, { - "id": "547-self-host#configuration", + "id": "570-self-host#configuration", "title": "Self-Hosting Zero", "searchTitle": "Configuration", "sectionTitle": "Configuration", @@ -7274,16 +7592,20 @@ "kind": "section" }, { - "id": "70-server-zql", + "id": "71-server-zql", "title": "ZQL on the Server", "searchTitle": "ZQL on the Server", "url": "/docs/server-zql", - "content": "The Zero package includes utilities to run ZQL on the server directly against your upstream Postgres database. This is useful for many reasons: It allows mutators to read data using ZQL to check permissions or invariants. You can use ZQL to implement standard REST endpoints, allowing you to share code with mutators. In the future (but not yet implemented), this can support server-side rendering. ZQLDatabase currently does a read of your postgres schema before every transaction. This is fine for most usages, but for high scale it may become a problem. Let us know if you need a fix for this. Creating a Database To run ZQL on the server, you will create a ZQLDatabase instance. Zero ships with several built-in factories for popular Postgres libraries and ORMs. // app/api/mutate/db-provider.ts import {zeroDrizzle} from '@rocicorp/zero/server/adapters/drizzle' import {schema} from '../../zero/schema.ts' import * as drizzleSchema from '../../drizzle/schema.ts' // pass a drizzle client instance. for example: export const drizzleClient = drizzle(pool, { schema: drizzleSchema }) export const dbProvider = zeroDrizzle(schema, drizzleClient) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {Kysely, PostgresDialect} from 'kysely' import {zeroKysely} from '@rocicorp/zero/server/adapters/kysely' import {Pool} from 'pg' import {schema} from '../../zero/schema.ts' interface Database { user: { id: string name: string | null status: 'active' | 'inactive' } } const kysely = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.ZERO_UPSTREAM_DB! }) }) }) export const dbProvider = zeroKysely(schema, kysely) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {PrismaPg} from '@prisma/adapter-pg' import {PrismaClient} from '@prisma/client' import {zeroPrisma} from '@rocicorp/zero/server/adapters/prisma' import {schema} from '../../zero/schema.ts' const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.ZERO_UPSTREAM_DB! }) }) export const dbProvider = zeroPrisma(schema, prisma) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {zeroNodePg} from '@rocicorp/zero/server/adapters/pg' import {Pool} from 'pg' import {schema} from '../../zero/schema.ts' const pool = new Pool({ connectionString: process.env.ZERO_UPSTREAM_DB! }) export const dbProvider = zeroNodePg(schema, pool) // You can also pass a client instead of a pool: // // const client = new Client({ // connectionString: process.env.ZERO_UPSTREAM_DB! // }) // await client.connect() // export const dbProvider = zeroNodePg(schema, client) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {zeroPostgresJS} from '@rocicorp/zero/server/adapters/postgresjs' import postgres from 'postgres' import {schema} from '../../zero/schema.ts' const sql = postgres(process.env.ZERO_UPSTREAM_DB!) export const dbProvider = zeroPostgresJS(schema, sql) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } } Within your mutators, you can access the underlying transaction via tx.dbTransaction.wrappedTransaction: // mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction .insert(drizzleSchema.user) .values({id, name}) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction .insertInto('user') .values({id, name, status: 'active'}) .execute() } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction.user.create( { data: { id, name, status: 'active' } } ) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction.query( 'insert into \"user\" (id, name) values ($1, $2) returning *', [id, name] ) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction< {id: string; name: string}[] >`insert into \"user\" (id, name) values (${id}, ${name}) returning *` } } ) }) Custom Database To implement support for some other Postgres bindings library, you will implement the DBConnection interface. See the implementations for the existing adapters for examples. Running ZQL Once you have an instance of ZQLDatabase, use the transaction() method to run ZQL: await dbProvider.transaction(async tx => { // await tx.mutate... // await tx.query... // await myMutator.fn({tx, ctx, args}) }) SSR Zero doesn't yet have the wiring setup in its bindings layers to really nicely support server-side rendering (patches welcome though!). For now, we don't recommend using Zero with SSR. Use your framework's recommended pattern to prevent SSR execution: import {lazy} from 'react' // Use React lazy to defer loading the ZeroProvider const ZeroProvider = lazy(() => import('@rocicorp/zero/react').then(mod => ({ default: mod.ZeroProvider })) ) function Root() { return ( ) }// Mark client-only components 'use client' import {ZeroProvider} from '@rocicorp/zero/react' export default function Root() { return ( ) }import {clientOnly} from '@solidjs/start' const ZeroProvider = clientOnly(async () => { // Optionally dynamic import to code-split return import('@rocicorp/zero/solid').then(mod => ({ default: mod.ZeroProvider })) }) export default function Root() { return ( ) }", + "content": "The Zero package includes utilities to run ZQL on the server directly against your upstream Postgres database. This is useful for many reasons: It allows mutators to read data using ZQL to check permissions or invariants. You can use ZQL to implement standard REST endpoints, allowing you to share code with mutators. In the future (but not yet implemented), this can support server-side rendering. ZQLDatabase currently does a read of your postgres schema before every transaction. This is fine for most usages, but for high scale it may become a problem. Let us know if you need a fix for this. Creating a Database To run ZQL on the server, you will create a ZQLDatabase instance. Zero ships with several built-in factories for popular Postgres libraries and ORMs. // app/api/mutate/db-provider.ts import {zeroDrizzle} from '@rocicorp/zero/server/adapters/drizzle' import {schema} from '../../zero/schema.ts' import * as drizzleSchema from '../../drizzle/schema.ts' // pass a drizzle client instance. for example: export const drizzleClient = drizzle(pool, { schema: drizzleSchema }) export const dbProvider = zeroDrizzle(schema, drizzleClient) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {Kysely, PostgresDialect} from 'kysely' import {zeroKysely} from '@rocicorp/zero/server/adapters/kysely' import {Pool} from 'pg' import {schema} from '../../zero/schema.ts' interface Database { user: { id: string name: string | null status: 'active' | 'inactive' } } const kysely = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.ZERO_UPSTREAM_DB! }) }) }) export const dbProvider = zeroKysely(schema, kysely) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {PrismaPg} from '@prisma/adapter-pg' import {PrismaClient} from '@prisma/client' import {zeroPrisma} from '@rocicorp/zero/server/adapters/prisma' import {schema} from '../../zero/schema.ts' const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.ZERO_UPSTREAM_DB! }) }) export const dbProvider = zeroPrisma(schema, prisma) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {zeroNodePg} from '@rocicorp/zero/server/adapters/pg' import {Pool} from 'pg' import {schema} from '../../zero/schema.ts' const pool = new Pool({ connectionString: process.env.ZERO_UPSTREAM_DB! }) export const dbProvider = zeroNodePg(schema, pool) // You can also pass a client instead of a pool: // // const client = new Client({ // connectionString: process.env.ZERO_UPSTREAM_DB! // }) // await client.connect() // export const dbProvider = zeroNodePg(schema, client) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {zeroPostgresJS} from '@rocicorp/zero/server/adapters/postgresjs' import postgres from 'postgres' import {schema} from '../../zero/schema.ts' const sql = postgres(process.env.ZERO_UPSTREAM_DB!) export const dbProvider = zeroPostgresJS(schema, sql) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } } Within your mutators, you can access the underlying transaction via tx.dbTransaction.wrappedTransaction: // mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction .insert(drizzleSchema.user) .values({id, name}) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction .insertInto('user') .values({id, name, status: 'active'}) .execute() } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction.user.create( { data: { id, name, status: 'active' } } ) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction.query( 'insert into \"user\" (id, name) values ($1, $2) returning *', [id, name] ) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction< {id: string; name: string}[] >`insert into \"user\" (id, name) values (${id}, ${name}) returning *` } } ) }) Isolation Level By default, each transaction starts with a plain BEGIN, so Postgres uses its default isolation level. On a stock Postgres, that is read committed. To use a different level, pass isolationLevel in the options argument of the adapter factory. It can be 'read committed', 'repeatable read', or 'serializable': export const dbProvider = zeroDrizzle( schema, drizzleClient, { isolationLevel: 'serializable' } )export const dbProvider = zeroKysely(schema, kysely, { isolationLevel: 'serializable' })// Use the Postgres spelling, not Prisma's 'Serializable'. export const dbProvider = zeroPrisma(schema, prisma, { isolationLevel: 'serializable' })// The third argument is an optional logger. export const dbProvider = zeroNodePg( schema, pool, undefined, { isolationLevel: 'serializable' } )export const dbProvider = zeroPostgresJS(schema, sql, { isolationLevel: 'serializable' }) Every transaction the adapter opens uses this level, including the ones your mutators run in. The TransactionOptions and IsolationLevel types are exported from @rocicorp/zero/server. At repeatable read and serializable, Postgres can abort a transaction that conflicts with a concurrent one. Use tx.retryOn() to run the mutator again when that happens. Custom Database To implement support for some other Postgres bindings library, you will implement the DBConnection interface. See the implementations for the existing adapters for examples. Running ZQL Once you have an instance of ZQLDatabase, use the transaction() method to run ZQL: await dbProvider.transaction(async tx => { // await tx.mutate... // await tx.query... // await myMutator.fn({tx, ctx, args}) }) SSR Zero doesn't yet have the wiring setup in its bindings layers to really nicely support server-side rendering (patches welcome though!). For now, we don't recommend using Zero with SSR. Use your framework's recommended pattern to prevent SSR execution: import {lazy} from 'react' // Use React lazy to defer loading the ZeroProvider const ZeroProvider = lazy(() => import('@rocicorp/zero/react').then(mod => ({ default: mod.ZeroProvider })) ) function Root() { return ( ) }// Mark client-only components 'use client' import {ZeroProvider} from '@rocicorp/zero/react' export default function Root() { return ( ) }import {clientOnly} from '@solidjs/start' const ZeroProvider = clientOnly(async () => { // Optionally dynamic import to code-split return import('@rocicorp/zero/solid').then(mod => ({ default: mod.ZeroProvider })) }) export default function Root() { return ( ) }", "headings": [ { "text": "Creating a Database", "id": "creating-a-database" }, + { + "text": "Isolation Level", + "id": "isolation-level" + }, { "text": "Custom Database", "id": "custom-database" @@ -7300,17 +7622,27 @@ "kind": "page" }, { - "id": "548-server-zql#creating-a-database", + "id": "571-server-zql#creating-a-database", "title": "ZQL on the Server", "searchTitle": "Creating a Database", "sectionTitle": "Creating a Database", "sectionId": "creating-a-database", "url": "/docs/server-zql", - "content": "To run ZQL on the server, you will create a ZQLDatabase instance. Zero ships with several built-in factories for popular Postgres libraries and ORMs. // app/api/mutate/db-provider.ts import {zeroDrizzle} from '@rocicorp/zero/server/adapters/drizzle' import {schema} from '../../zero/schema.ts' import * as drizzleSchema from '../../drizzle/schema.ts' // pass a drizzle client instance. for example: export const drizzleClient = drizzle(pool, { schema: drizzleSchema }) export const dbProvider = zeroDrizzle(schema, drizzleClient) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {Kysely, PostgresDialect} from 'kysely' import {zeroKysely} from '@rocicorp/zero/server/adapters/kysely' import {Pool} from 'pg' import {schema} from '../../zero/schema.ts' interface Database { user: { id: string name: string | null status: 'active' | 'inactive' } } const kysely = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.ZERO_UPSTREAM_DB! }) }) }) export const dbProvider = zeroKysely(schema, kysely) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {PrismaPg} from '@prisma/adapter-pg' import {PrismaClient} from '@prisma/client' import {zeroPrisma} from '@rocicorp/zero/server/adapters/prisma' import {schema} from '../../zero/schema.ts' const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.ZERO_UPSTREAM_DB! }) }) export const dbProvider = zeroPrisma(schema, prisma) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {zeroNodePg} from '@rocicorp/zero/server/adapters/pg' import {Pool} from 'pg' import {schema} from '../../zero/schema.ts' const pool = new Pool({ connectionString: process.env.ZERO_UPSTREAM_DB! }) export const dbProvider = zeroNodePg(schema, pool) // You can also pass a client instead of a pool: // // const client = new Client({ // connectionString: process.env.ZERO_UPSTREAM_DB! // }) // await client.connect() // export const dbProvider = zeroNodePg(schema, client) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {zeroPostgresJS} from '@rocicorp/zero/server/adapters/postgresjs' import postgres from 'postgres' import {schema} from '../../zero/schema.ts' const sql = postgres(process.env.ZERO_UPSTREAM_DB!) export const dbProvider = zeroPostgresJS(schema, sql) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } } Within your mutators, you can access the underlying transaction via tx.dbTransaction.wrappedTransaction: // mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction .insert(drizzleSchema.user) .values({id, name}) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction .insertInto('user') .values({id, name, status: 'active'}) .execute() } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction.user.create( { data: { id, name, status: 'active' } } ) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction.query( 'insert into \"user\" (id, name) values ($1, $2) returning *', [id, name] ) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction< {id: string; name: string}[] >`insert into \"user\" (id, name) values (${id}, ${name}) returning *` } } ) }) Custom Database To implement support for some other Postgres bindings library, you will implement the DBConnection interface. See the implementations for the existing adapters for examples.", + "content": "To run ZQL on the server, you will create a ZQLDatabase instance. Zero ships with several built-in factories for popular Postgres libraries and ORMs. // app/api/mutate/db-provider.ts import {zeroDrizzle} from '@rocicorp/zero/server/adapters/drizzle' import {schema} from '../../zero/schema.ts' import * as drizzleSchema from '../../drizzle/schema.ts' // pass a drizzle client instance. for example: export const drizzleClient = drizzle(pool, { schema: drizzleSchema }) export const dbProvider = zeroDrizzle(schema, drizzleClient) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {Kysely, PostgresDialect} from 'kysely' import {zeroKysely} from '@rocicorp/zero/server/adapters/kysely' import {Pool} from 'pg' import {schema} from '../../zero/schema.ts' interface Database { user: { id: string name: string | null status: 'active' | 'inactive' } } const kysely = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.ZERO_UPSTREAM_DB! }) }) }) export const dbProvider = zeroKysely(schema, kysely) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {PrismaPg} from '@prisma/adapter-pg' import {PrismaClient} from '@prisma/client' import {zeroPrisma} from '@rocicorp/zero/server/adapters/prisma' import {schema} from '../../zero/schema.ts' const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.ZERO_UPSTREAM_DB! }) }) export const dbProvider = zeroPrisma(schema, prisma) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {zeroNodePg} from '@rocicorp/zero/server/adapters/pg' import {Pool} from 'pg' import {schema} from '../../zero/schema.ts' const pool = new Pool({ connectionString: process.env.ZERO_UPSTREAM_DB! }) export const dbProvider = zeroNodePg(schema, pool) // You can also pass a client instead of a pool: // // const client = new Client({ // connectionString: process.env.ZERO_UPSTREAM_DB! // }) // await client.connect() // export const dbProvider = zeroNodePg(schema, client) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } }// app/api/mutate/db-provider.ts import {zeroPostgresJS} from '@rocicorp/zero/server/adapters/postgresjs' import postgres from 'postgres' import {schema} from '../../zero/schema.ts' const sql = postgres(process.env.ZERO_UPSTREAM_DB!) export const dbProvider = zeroPostgresJS(schema, sql) // Register the database provider for type safety declare module '@rocicorp/zero' { interface DefaultTypes { dbProvider: typeof dbProvider } } Within your mutators, you can access the underlying transaction via tx.dbTransaction.wrappedTransaction: // mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction .insert(drizzleSchema.user) .values({id, name}) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction .insertInto('user') .values({id, name, status: 'active'}) .execute() } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction.user.create( { data: { id, name, status: 'active' } } ) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction.query( 'insert into \"user\" (id, name) values ($1, $2) returning *', [id, name] ) } } ) })// mutators.ts export const mutators = defineMutators({ createUser: defineMutator( z.object({id: z.string(), name: z.string()}), async ({tx, args: {id, name}}) => { if (tx.location === 'server') { await tx.dbTransaction.wrappedTransaction< {id: string; name: string}[] >`insert into \"user\" (id, name) values (${id}, ${name}) returning *` } } ) }) Isolation Level By default, each transaction starts with a plain BEGIN, so Postgres uses its default isolation level. On a stock Postgres, that is read committed. To use a different level, pass isolationLevel in the options argument of the adapter factory. It can be 'read committed', 'repeatable read', or 'serializable': export const dbProvider = zeroDrizzle( schema, drizzleClient, { isolationLevel: 'serializable' } )export const dbProvider = zeroKysely(schema, kysely, { isolationLevel: 'serializable' })// Use the Postgres spelling, not Prisma's 'Serializable'. export const dbProvider = zeroPrisma(schema, prisma, { isolationLevel: 'serializable' })// The third argument is an optional logger. export const dbProvider = zeroNodePg( schema, pool, undefined, { isolationLevel: 'serializable' } )export const dbProvider = zeroPostgresJS(schema, sql, { isolationLevel: 'serializable' }) Every transaction the adapter opens uses this level, including the ones your mutators run in. The TransactionOptions and IsolationLevel types are exported from @rocicorp/zero/server. At repeatable read and serializable, Postgres can abort a transaction that conflicts with a concurrent one. Use tx.retryOn() to run the mutator again when that happens. Custom Database To implement support for some other Postgres bindings library, you will implement the DBConnection interface. See the implementations for the existing adapters for examples.", "kind": "section" }, { - "id": "549-server-zql#custom-database", + "id": "572-server-zql#isolation-level", + "title": "ZQL on the Server", + "searchTitle": "Isolation Level", + "sectionTitle": "Isolation Level", + "sectionId": "isolation-level", + "url": "/docs/server-zql", + "content": "By default, each transaction starts with a plain BEGIN, so Postgres uses its default isolation level. On a stock Postgres, that is read committed. To use a different level, pass isolationLevel in the options argument of the adapter factory. It can be 'read committed', 'repeatable read', or 'serializable': export const dbProvider = zeroDrizzle( schema, drizzleClient, { isolationLevel: 'serializable' } )export const dbProvider = zeroKysely(schema, kysely, { isolationLevel: 'serializable' })// Use the Postgres spelling, not Prisma's 'Serializable'. export const dbProvider = zeroPrisma(schema, prisma, { isolationLevel: 'serializable' })// The third argument is an optional logger. export const dbProvider = zeroNodePg( schema, pool, undefined, { isolationLevel: 'serializable' } )export const dbProvider = zeroPostgresJS(schema, sql, { isolationLevel: 'serializable' }) Every transaction the adapter opens uses this level, including the ones your mutators run in. The TransactionOptions and IsolationLevel types are exported from @rocicorp/zero/server. At repeatable read and serializable, Postgres can abort a transaction that conflicts with a concurrent one. Use tx.retryOn() to run the mutator again when that happens.", + "kind": "section" + }, + { + "id": "573-server-zql#custom-database", "title": "ZQL on the Server", "searchTitle": "Custom Database", "sectionTitle": "Custom Database", @@ -7320,7 +7652,7 @@ "kind": "section" }, { - "id": "550-server-zql#running-zql", + "id": "574-server-zql#running-zql", "title": "ZQL on the Server", "searchTitle": "Running ZQL", "sectionTitle": "Running ZQL", @@ -7330,7 +7662,7 @@ "kind": "section" }, { - "id": "551-server-zql#ssr", + "id": "575-server-zql#ssr", "title": "ZQL on the Server", "searchTitle": "SSR", "sectionTitle": "SSR", @@ -7340,7 +7672,7 @@ "kind": "section" }, { - "id": "71-solidjs", + "id": "72-solidjs", "title": "SolidJS", "searchTitle": "SolidJS", "url": "/docs/solidjs", @@ -7362,7 +7694,7 @@ "kind": "page" }, { - "id": "552-solidjs#setup", + "id": "576-solidjs#setup", "title": "SolidJS", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -7372,7 +7704,7 @@ "kind": "section" }, { - "id": "553-solidjs#usage", + "id": "577-solidjs#usage", "title": "SolidJS", "searchTitle": "Usage", "sectionTitle": "Usage", @@ -7382,7 +7714,7 @@ "kind": "section" }, { - "id": "554-solidjs#examples", + "id": "578-solidjs#examples", "title": "SolidJS", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -7392,7 +7724,7 @@ "kind": "section" }, { - "id": "72-status", + "id": "73-status", "title": "Project Status", "searchTitle": "Project Status", "url": "/docs/status", @@ -7418,7 +7750,7 @@ "kind": "page" }, { - "id": "555-status#breaking-changes", + "id": "579-status#breaking-changes", "title": "Project Status", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -7428,7 +7760,7 @@ "kind": "section" }, { - "id": "556-status#roadmap", + "id": "580-status#roadmap", "title": "Project Status", "searchTitle": "Roadmap", "sectionTitle": "Roadmap", @@ -7438,7 +7770,7 @@ "kind": "section" }, { - "id": "557-status#2026", + "id": "581-status#2026", "title": "Project Status", "searchTitle": "2026", "sectionTitle": "2026", @@ -7448,7 +7780,7 @@ "kind": "section" }, { - "id": "558-status#soon", + "id": "582-status#soon", "title": "Project Status", "searchTitle": "Soon", "sectionTitle": "Soon", @@ -7458,7 +7790,7 @@ "kind": "section" }, { - "id": "73-sync", + "id": "74-sync", "title": "What is Sync?", "searchTitle": "What is Sync?", "url": "/docs/sync", @@ -7480,7 +7812,7 @@ "kind": "page" }, { - "id": "559-sync#problem", + "id": "583-sync#problem", "title": "What is Sync?", "searchTitle": "Problem", "sectionTitle": "Problem", @@ -7490,7 +7822,7 @@ "kind": "section" }, { - "id": "560-sync#solution", + "id": "584-sync#solution", "title": "What is Sync?", "searchTitle": "Solution", "sectionTitle": "Solution", @@ -7500,7 +7832,7 @@ "kind": "section" }, { - "id": "561-sync#history-of-sync", + "id": "585-sync#history-of-sync", "title": "What is Sync?", "searchTitle": "History of Sync", "sectionTitle": "History of Sync", @@ -7510,7 +7842,7 @@ "kind": "section" }, { - "id": "74-tutorial", + "id": "75-tutorial", "title": "Tutorial", "searchTitle": "Tutorial", "url": "/docs/tutorial", @@ -7584,7 +7916,7 @@ "kind": "page" }, { - "id": "562-tutorial#setup", + "id": "586-tutorial#setup", "title": "Tutorial", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -7594,7 +7926,7 @@ "kind": "section" }, { - "id": "563-tutorial#create-a-project", + "id": "587-tutorial#create-a-project", "title": "Tutorial", "searchTitle": "Create a Project", "sectionTitle": "Create a Project", @@ -7604,7 +7936,7 @@ "kind": "section" }, { - "id": "564-tutorial#set-up-your-database", + "id": "588-tutorial#set-up-your-database", "title": "Tutorial", "searchTitle": "Set Up Your Database", "sectionTitle": "Set Up Your Database", @@ -7614,7 +7946,7 @@ "kind": "section" }, { - "id": "565-tutorial#install-and-run-zero-cache", + "id": "589-tutorial#install-and-run-zero-cache", "title": "Tutorial", "searchTitle": "Install and Run Zero-Cache", "sectionTitle": "Install and Run Zero-Cache", @@ -7624,7 +7956,7 @@ "kind": "section" }, { - "id": "566-tutorial#integrate-zero", + "id": "590-tutorial#integrate-zero", "title": "Tutorial", "searchTitle": "Integrate Zero", "sectionTitle": "Integrate Zero", @@ -7634,7 +7966,7 @@ "kind": "section" }, { - "id": "567-tutorial#set-up-your-zero-schema", + "id": "591-tutorial#set-up-your-zero-schema", "title": "Tutorial", "searchTitle": "Set Up Your Zero Schema", "sectionTitle": "Set Up Your Zero Schema", @@ -7644,7 +7976,7 @@ "kind": "section" }, { - "id": "568-tutorial#set-up-the-zero-client", + "id": "592-tutorial#set-up-the-zero-client", "title": "Tutorial", "searchTitle": "Set Up the Zero Client", "sectionTitle": "Set Up the Zero Client", @@ -7654,7 +7986,7 @@ "kind": "section" }, { - "id": "569-tutorial#sync-data", + "id": "593-tutorial#sync-data", "title": "Tutorial", "searchTitle": "Sync Data", "sectionTitle": "Sync Data", @@ -7664,7 +7996,7 @@ "kind": "section" }, { - "id": "570-tutorial#define-query", + "id": "594-tutorial#define-query", "title": "Tutorial", "searchTitle": "Define Query", "sectionTitle": "Define Query", @@ -7674,7 +8006,7 @@ "kind": "section" }, { - "id": "571-tutorial#add-query-endpoint", + "id": "595-tutorial#add-query-endpoint", "title": "Tutorial", "searchTitle": "Add Query Endpoint", "sectionTitle": "Add Query Endpoint", @@ -7684,7 +8016,7 @@ "kind": "section" }, { - "id": "572-tutorial#invoke-query", + "id": "596-tutorial#invoke-query", "title": "Tutorial", "searchTitle": "Invoke Query", "sectionTitle": "Invoke Query", @@ -7694,7 +8026,7 @@ "kind": "section" }, { - "id": "573-tutorial#mutate-data", + "id": "597-tutorial#mutate-data", "title": "Tutorial", "searchTitle": "Mutate Data", "sectionTitle": "Mutate Data", @@ -7704,7 +8036,7 @@ "kind": "section" }, { - "id": "574-tutorial#define-mutators", + "id": "598-tutorial#define-mutators", "title": "Tutorial", "searchTitle": "Define Mutators", "sectionTitle": "Define Mutators", @@ -7714,7 +8046,7 @@ "kind": "section" }, { - "id": "575-tutorial#add-mutate-endpoint", + "id": "599-tutorial#add-mutate-endpoint", "title": "Tutorial", "searchTitle": "Add Mutate Endpoint", "sectionTitle": "Add Mutate Endpoint", @@ -7724,7 +8056,7 @@ "kind": "section" }, { - "id": "576-tutorial#invoke-mutators", + "id": "600-tutorial#invoke-mutators", "title": "Tutorial", "searchTitle": "Invoke Mutators", "sectionTitle": "Invoke Mutators", @@ -7734,7 +8066,7 @@ "kind": "section" }, { - "id": "577-tutorial#next-steps", + "id": "601-tutorial#next-steps", "title": "Tutorial", "searchTitle": "Next Steps", "sectionTitle": "Next Steps", @@ -7744,7 +8076,7 @@ "kind": "section" }, { - "id": "75-when-to-use", + "id": "76-when-to-use", "title": "When To Use Zero", "searchTitle": "When To Use Zero", "url": "/docs/when-to-use", @@ -7810,7 +8142,7 @@ "kind": "page" }, { - "id": "578-when-to-use#zero-might-be-a-good-fit", + "id": "602-when-to-use#zero-might-be-a-good-fit", "title": "When To Use Zero", "searchTitle": "Zero Might be a Good Fit", "sectionTitle": "Zero Might be a Good Fit", @@ -7820,7 +8152,7 @@ "kind": "section" }, { - "id": "579-when-to-use#you-want-to-sync-only-a-small-subset-of-data-to-client", + "id": "603-when-to-use#you-want-to-sync-only-a-small-subset-of-data-to-client", "title": "When To Use Zero", "searchTitle": "You want to sync only a small subset of data to client", "sectionTitle": "You want to sync only a small subset of data to client", @@ -7830,7 +8162,7 @@ "kind": "section" }, { - "id": "580-when-to-use#you-need-fine-grained-read-or-write-permissions", + "id": "604-when-to-use#you-need-fine-grained-read-or-write-permissions", "title": "When To Use Zero", "searchTitle": "You need fine-grained read or write permissions", "sectionTitle": "You need fine-grained read or write permissions", @@ -7840,7 +8172,7 @@ "kind": "section" }, { - "id": "581-when-to-use#you-are-building-a-traditional-client-server-web-app", + "id": "605-when-to-use#you-are-building-a-traditional-client-server-web-app", "title": "When To Use Zero", "searchTitle": "You are building a traditional client-server web app", "sectionTitle": "You are building a traditional client-server web app", @@ -7850,7 +8182,7 @@ "kind": "section" }, { - "id": "582-when-to-use#you-use-postgresql", + "id": "606-when-to-use#you-use-postgresql", "title": "When To Use Zero", "searchTitle": "You use PostgreSQL", "sectionTitle": "You use PostgreSQL", @@ -7860,7 +8192,7 @@ "kind": "section" }, { - "id": "583-when-to-use#your-app-is-broadly-like-linear", + "id": "607-when-to-use#your-app-is-broadly-like-linear", "title": "When To Use Zero", "searchTitle": "Your app is broadly \"like Linear\"", "sectionTitle": "Your app is broadly \"like Linear\"", @@ -7870,7 +8202,7 @@ "kind": "section" }, { - "id": "584-when-to-use#interaction-performance-is-very-important-to-you", + "id": "608-when-to-use#interaction-performance-is-very-important-to-you", "title": "When To Use Zero", "searchTitle": "Interaction performance is very important to you", "sectionTitle": "Interaction performance is very important to you", @@ -7880,7 +8212,7 @@ "kind": "section" }, { - "id": "585-when-to-use#zero-might-not-be-a-good-fit", + "id": "609-when-to-use#zero-might-not-be-a-good-fit", "title": "When To Use Zero", "searchTitle": "Zero Might Not be a Good Fit", "sectionTitle": "Zero Might Not be a Good Fit", @@ -7890,7 +8222,7 @@ "kind": "section" }, { - "id": "586-when-to-use#you-need-the-privacy-or-data-ownership-benefits-of-local-first", + "id": "610-when-to-use#you-need-the-privacy-or-data-ownership-benefits-of-local-first", "title": "When To Use Zero", "searchTitle": "You need the privacy or data ownership benefits of local-first", "sectionTitle": "You need the privacy or data ownership benefits of local-first", @@ -7900,7 +8232,7 @@ "kind": "section" }, { - "id": "587-when-to-use#you-need-to-support-offline-writes-or-long-periods-offline", + "id": "611-when-to-use#you-need-to-support-offline-writes-or-long-periods-offline", "title": "When To Use Zero", "searchTitle": "You need to support offline writes or long periods offline", "sectionTitle": "You need to support offline writes or long periods offline", @@ -7910,7 +8242,7 @@ "kind": "section" }, { - "id": "588-when-to-use#you-are-building-a-native-mobile-app", + "id": "612-when-to-use#you-are-building-a-native-mobile-app", "title": "When To Use Zero", "searchTitle": "You are building a native mobile app", "sectionTitle": "You are building a native mobile app", @@ -7920,7 +8252,7 @@ "kind": "section" }, { - "id": "589-when-to-use#the-total-backend-dataset-is--100gb", + "id": "613-when-to-use#the-total-backend-dataset-is--100gb", "title": "When To Use Zero", "searchTitle": "The total backend dataset is > ~100GB", "sectionTitle": "The total backend dataset is > ~100GB", @@ -7930,7 +8262,7 @@ "kind": "section" }, { - "id": "590-when-to-use#zero-might-not-be-a-good-fit-yet", + "id": "614-when-to-use#zero-might-not-be-a-good-fit-yet", "title": "When To Use Zero", "searchTitle": "Zero Might Not be a Good Fit Yet", "sectionTitle": "Zero Might Not be a Good Fit Yet", @@ -7940,7 +8272,7 @@ "kind": "section" }, { - "id": "591-when-to-use#alternatives", + "id": "615-when-to-use#alternatives", "title": "When To Use Zero", "searchTitle": "Alternatives", "sectionTitle": "Alternatives", @@ -7950,11 +8282,11 @@ "kind": "section" }, { - "id": "76-zero-cache-config", + "id": "77-zero-cache-config", "title": "zero-cache Config", "searchTitle": "zero-cache Config", "url": "/docs/zero-cache-config", - "content": "zero-cache is configured either via CLI flag or environment variable. There is no separate zero.config file. You can also see all available flags by running zero-cache --help. Required Flags Upstream DB The \"upstream\" authoritative postgres database. In the future we will support other types of upstream besides PG. flag: --upstream-db env: ZERO_UPSTREAM_DB required: true Admin Password A password used to administer zero-cache server, for example to access the /statz endpoint and the inspector. This is required in production (when NODE_ENV=production) because we want all Zero servers to be debuggable using admin tools by default, without needing a restart. But we also don't want to expose sensitive data using them. flag: --admin-password env: ZERO_ADMIN_PASSWORD required: in production (when NODE_ENV=production) Optional Flags App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: unset Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: unset Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly three times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 2 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Mode The mode for running or connecting to the change-streamer: dedicated: runs the change-streamer and shuts down when another change-streamer takes over the replication slot. This is appropriate in a single-node configuration, or for the replication-manager in a multi-node configuration. discover: connects to the change-streamer as internally advertised in the change-db. This is appropriate for the view-syncers in a multi-node setup. This may not work in all networking configurations (e.g., some private networking or port forwarding setups). Using ZERO_CHANGE_STREAMER_URI with an explicit routable hostname is recommended instead. This option is ignored if ZERO_CHANGE_STREAMER_URI is set. flag: --change-streamer-mode env: ZERO_CHANGE_STREAMER_MODE default: dedicated Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 48 CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. When enabling Litestream v5 backups, configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which to perform an SQLite checkpoint to apply the writes in the WAL to the main database file. Each checkpoint creates a new WAL segment file that will be backed up by Litestream v3. Smaller thresholds may improve read performance, at the expense of creating more files to download when restoring the replica from the backup. This setting is ignored when backing up with Litestream v5. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 is enabled, this executable is required. The official Zero Docker image includes Litestream 0.5.16. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. When a backup URL is configured, disabling this option is also the way to run without a v5 executable. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. This is disabled by default to continue writing legacy WAL backups. Enabling it requires v5 restore and ZERO_LITESTREAM_VFS_QUERY_EXECUTABLE. With v5 backup enabled, Zero acknowledges upstream changes only after they are applied to the replica and confirmed in the backup. Each replica uses a distinct backup subpath under the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Configure an object-store lifecycle policy to remove old subpaths; Zero does not delete them automatically. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: false Litestream VFS Query Executable Path to the vfs-query executable used to monitor Litestream v5 backup progress. This is required when ZERO_LITESTREAM_BACKUP_USING_V5 is enabled. The official Zero Docker image includes this executable. flag: --litestream-vfs-query-executable env: ZERO_LITESTREAM_VFS_QUERY_EXECUTABLE Litestream VFS Poll Interval (ms) How often the Litestream VFS polls backup storage for the latest backup. This controls how quickly Zero can confirm a backup and acknowledge the corresponding upstream changes. flag: --litestream-vfs-poll-interval-ms env: ZERO_LITESTREAM_VFS_POLL_INTERVAL_MS default: 15000 Litestream VFS Poll Timeout (ms) Reserved for configuring request timeouts to the Litestream VFS poller. In this release, vfs-query uses its internal 30-second timeout regardless of this value. flag: --litestream-vfs-poll-timeout-ms env: ZERO_LITESTREAM_VFS_POLL_TIMEOUT_MS default: 10000 Litestream VFS Log File Reserved for redirecting Litestream VFS logs. In this release, vfs-query writes logs to the child process's standard error regardless of this value. flag: --litestream-vfs-log-file env: ZERO_LITESTREAM_VFS_LOG_FILE Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Minimum Checkpoint Page Count The WAL page count at which SQLite attempts a PASSIVE checkpoint, which transfers pages to the main database file without blocking writers. Defaults to checkpointThresholdMB * 250 (since SQLite page size is 4KB). This setting is ignored when backing up with Litestream v5. flag: --litestream-min-checkpoint-page-count env: ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT default: checkpointThresholdMB * 250 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY default: 48 Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Report Interval (ms) The minimum interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 30_000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10 Deprecated Flags Change Max Connections ZERO_CHANGE_MAX_CONNS is deprecated and no longer controls change database connection usage. Size the database or pooler as described under Change DB. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS Auth JWK A public key in JWK format used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwk env: ZERO_AUTH_JWK Auth JWKS URL A URL that returns a JWK set used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwks-url env: ZERO_AUTH_JWKS_URL Auth Secret A symmetric key used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-secret env: ZERO_AUTH_SECRET", + "content": "zero-cache is configured either via CLI flag or environment variable. There is no separate zero.config file. You can also see all available flags by running zero-cache --help. Required Flags Upstream DB The \"upstream\" authoritative postgres database. In the future we will support other types of upstream besides PG. flag: --upstream-db env: ZERO_UPSTREAM_DB required: true Admin Password A password used to administer zero-cache server, for example to access the /statz endpoint and the inspector. This is required in production (when NODE_ENV=production) because we want all Zero servers to be debuggable using admin tools by default, without needing a restart. But we also don't want to expose sensitive data using them. The admin password also grants access to everything that the operator password does. flag: --admin-password env: ZERO_ADMIN_PASSWORD required: in production (when NODE_ENV=production) Optional Flags Allow Legacy Queries Allows clients to send query ASTs directly to zero-cache. Ad-hoc queries (also called legacy queries) work this way. When this is false, zero-cache rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use queries. flag: --allow-legacy-queries env: ZERO_ALLOW_LEGACY_QUERIES default: false App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: 300 Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: 300 Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 4 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 168 (one week) CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. With Litestream v5 backups (the default), configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 or ZERO_LITESTREAM_BACKUP_USING_V5 is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build 0.5.18-zero.16. Restores fork the source backup with litestream restore -fork-to-url, so a custom executable must support that flag. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and ZERO_LITESTREAM_BACKUP_USING_V5 to false. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. Set this to false to write legacy WAL backups with Litestream v0.3.x instead. This requires ZERO_LITESTREAM_RESTORE_USING_V5, which is enabled by default. Zero requests an incremental backup every ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS, and acknowledges upstream changes only after a backup containing them has completed. Each replication-manager backs up to its own subfolder of the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: true Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. Leave this unset to use 48, lowered as needed so that the parts fit in a quarter of the memory available to the container. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Operator Password A password that grants access only to the diagnostic endpoints that cannot expose application data. People who operate zero-cache can use it to diagnose the server without being able to read the data it serves. These endpoints are: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Initial Readiness Threshold (ms) The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. When lag reporting is disabled, readiness does not wait for replication lag. flag: --replication-lag-initial-readiness-threshold-ms env: ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS default: 1000 Replication Lag Max Readiness Delay (ms) The maximum time that a server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. flag: --replication-lag-max-readiness-delay-ms env: ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS default: 600000 (10 minutes) Replication Lag Report Interval (ms) The approximate interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. Lag reports are also used at startup: a new server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready. This interval sets how long the server waits for the next report. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 10000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS View Syncer Hydration Budget (ms) A soft time limit for hydrating inactive (cached) queries when a view-syncer hydrates a client group's queries, for example after a client reconnects. Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your query endpoint does not count toward the budget. A value of 0 disables the budget. flag: --view-syncer-hydration-budget-ms env: ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS default: 0 View Syncer Query Hydration Timeout (ms) The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see Handling Errors). For the next five minutes, the query is rejected without being run again. After that, it can be retried. A value of 0 disables the limit. flag: --view-syncer-query-hydration-timeout-ms env: ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS default: 0 Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10 Deprecated Flags Change Max Connections ZERO_CHANGE_MAX_CONNS is deprecated and no longer controls change database connection usage. Size the database or pooler as described under Change DB. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS Change Streamer Mode ZERO_CHANGE_STREAMER_MODE is deprecated. Set ZERO_CHANGE_STREAMER_URI on view-syncers instead. The mode for running or connecting to the change-streamer: dedicated: runs the change-streamer and shuts down when another change-streamer takes over the replication slot. This is appropriate in a single-node configuration, or for the replication-manager in a multi-node configuration. discover: connects to the change-streamer as internally advertised in the change-db. This is appropriate for the view-syncers in a multi-node setup. This may not work in all networking configurations (e.g., some private networking or port forwarding setups). This option is ignored if ZERO_CHANGE_STREAMER_URI is set. flag: --change-streamer-mode env: ZERO_CHANGE_STREAMER_MODE default: dedicated Litestream Minimum Checkpoint Page Count ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT is deprecated. Use ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB instead. The WAL page count at which SQLite attempts a PASSIVE checkpoint. If unset, it is derived from the checkpoint threshold as checkpointThresholdMB * 256 (4 KiB pages). flag: --litestream-min-checkpoint-page-count env: ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT default: checkpointThresholdMB * 256 Auth JWK A public key in JWK format used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwk env: ZERO_AUTH_JWK Auth JWKS URL A URL that returns a JWK set used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwks-url env: ZERO_AUTH_JWKS_URL Auth Secret A symmetric key used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-secret env: ZERO_AUTH_SECRET", "headings": [ { "text": "Required Flags", @@ -7972,6 +8304,10 @@ "text": "Optional Flags", "id": "optional-flags" }, + { + "text": "Allow Legacy Queries", + "id": "allow-legacy-queries" + }, { "text": "App ID", "id": "app-id" @@ -8008,10 +8344,6 @@ "text": "Change Streamer Flow Control Slow Subscriber Grace Period Seconds", "id": "change-streamer-flow-control-slow-subscriber-grace-period-seconds" }, - { - "text": "Change Streamer Mode", - "id": "change-streamer-mode" - }, { "text": "Change Streamer Port", "id": "change-streamer-port" @@ -8100,22 +8432,6 @@ "text": "Litestream Backup Using V5", "id": "litestream-backup-using-v5" }, - { - "text": "Litestream VFS Query Executable", - "id": "litestream-vfs-query-executable" - }, - { - "text": "Litestream VFS Poll Interval (ms)", - "id": "litestream-vfs-poll-interval-ms" - }, - { - "text": "Litestream VFS Poll Timeout (ms)", - "id": "litestream-vfs-poll-timeout-ms" - }, - { - "text": "Litestream VFS Log File", - "id": "litestream-vfs-log-file" - }, { "text": "Litestream Incremental Backup Interval Minutes", "id": "litestream-incremental-backup-interval-minutes" @@ -8128,10 +8444,6 @@ "text": "Litestream Maximum Checkpoint Page Count", "id": "litestream-maximum-checkpoint-page-count" }, - { - "text": "Litestream Minimum Checkpoint Page Count", - "id": "litestream-minimum-checkpoint-page-count" - }, { "text": "Litestream Multipart Concurrency", "id": "litestream-multipart-concurrency" @@ -8204,6 +8516,10 @@ "text": "Number of Sync Workers", "id": "number-of-sync-workers" }, + { + "text": "Operator Password", + "id": "operator-password" + }, { "text": "Per User Mutation Limit Max", "id": "per-user-mutation-limit-max" @@ -8252,6 +8568,14 @@ "text": "Replica Vacuum Interval Hours", "id": "replica-vacuum-interval-hours" }, + { + "text": "Replication Lag Initial Readiness Threshold (ms)", + "id": "replication-lag-initial-readiness-threshold-ms" + }, + { + "text": "Replication Lag Max Readiness Delay (ms)", + "id": "replication-lag-max-readiness-delay-ms" + }, { "text": "Replication Lag Report Interval (ms)", "id": "replication-lag-report-interval-ms" @@ -8296,6 +8620,14 @@ "text": "Upstream PG Stream Inbound Timeout", "id": "upstream-pg-stream-inbound-timeout" }, + { + "text": "View Syncer Hydration Budget (ms)", + "id": "view-syncer-hydration-budget-ms" + }, + { + "text": "View Syncer Query Hydration Timeout (ms)", + "id": "view-syncer-query-hydration-timeout-ms" + }, { "text": "Websocket Compression", "id": "websocket-compression" @@ -8320,6 +8652,14 @@ "text": "Change Max Connections", "id": "change-max-connections" }, + { + "text": "Change Streamer Mode", + "id": "change-streamer-mode" + }, + { + "text": "Litestream Minimum Checkpoint Page Count", + "id": "litestream-minimum-checkpoint-page-count" + }, { "text": "Auth JWK", "id": "auth-jwk" @@ -8336,17 +8676,17 @@ "kind": "page" }, { - "id": "592-zero-cache-config#required-flags", + "id": "616-zero-cache-config#required-flags", "title": "zero-cache Config", "searchTitle": "Required Flags", "sectionTitle": "Required Flags", "sectionId": "required-flags", "url": "/docs/zero-cache-config", - "content": "Upstream DB The \"upstream\" authoritative postgres database. In the future we will support other types of upstream besides PG. flag: --upstream-db env: ZERO_UPSTREAM_DB required: true Admin Password A password used to administer zero-cache server, for example to access the /statz endpoint and the inspector. This is required in production (when NODE_ENV=production) because we want all Zero servers to be debuggable using admin tools by default, without needing a restart. But we also don't want to expose sensitive data using them. flag: --admin-password env: ZERO_ADMIN_PASSWORD required: in production (when NODE_ENV=production)", + "content": "Upstream DB The \"upstream\" authoritative postgres database. In the future we will support other types of upstream besides PG. flag: --upstream-db env: ZERO_UPSTREAM_DB required: true Admin Password A password used to administer zero-cache server, for example to access the /statz endpoint and the inspector. This is required in production (when NODE_ENV=production) because we want all Zero servers to be debuggable using admin tools by default, without needing a restart. But we also don't want to expose sensitive data using them. The admin password also grants access to everything that the operator password does. flag: --admin-password env: ZERO_ADMIN_PASSWORD required: in production (when NODE_ENV=production)", "kind": "section" }, { - "id": "593-zero-cache-config#upstream-db", + "id": "617-zero-cache-config#upstream-db", "title": "zero-cache Config", "searchTitle": "Upstream DB", "sectionTitle": "Upstream DB", @@ -8356,27 +8696,37 @@ "kind": "section" }, { - "id": "594-zero-cache-config#admin-password", + "id": "618-zero-cache-config#admin-password", "title": "zero-cache Config", "searchTitle": "Admin Password", "sectionTitle": "Admin Password", "sectionId": "admin-password", "url": "/docs/zero-cache-config", - "content": "A password used to administer zero-cache server, for example to access the /statz endpoint and the inspector. This is required in production (when NODE_ENV=production) because we want all Zero servers to be debuggable using admin tools by default, without needing a restart. But we also don't want to expose sensitive data using them. flag: --admin-password env: ZERO_ADMIN_PASSWORD required: in production (when NODE_ENV=production)", + "content": "A password used to administer zero-cache server, for example to access the /statz endpoint and the inspector. This is required in production (when NODE_ENV=production) because we want all Zero servers to be debuggable using admin tools by default, without needing a restart. But we also don't want to expose sensitive data using them. The admin password also grants access to everything that the operator password does. flag: --admin-password env: ZERO_ADMIN_PASSWORD required: in production (when NODE_ENV=production)", "kind": "section" }, { - "id": "595-zero-cache-config#optional-flags", + "id": "619-zero-cache-config#optional-flags", "title": "zero-cache Config", "searchTitle": "Optional Flags", "sectionTitle": "Optional Flags", "sectionId": "optional-flags", "url": "/docs/zero-cache-config", - "content": "App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: unset Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: unset Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly three times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 2 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Mode The mode for running or connecting to the change-streamer: dedicated: runs the change-streamer and shuts down when another change-streamer takes over the replication slot. This is appropriate in a single-node configuration, or for the replication-manager in a multi-node configuration. discover: connects to the change-streamer as internally advertised in the change-db. This is appropriate for the view-syncers in a multi-node setup. This may not work in all networking configurations (e.g., some private networking or port forwarding setups). Using ZERO_CHANGE_STREAMER_URI with an explicit routable hostname is recommended instead. This option is ignored if ZERO_CHANGE_STREAMER_URI is set. flag: --change-streamer-mode env: ZERO_CHANGE_STREAMER_MODE default: dedicated Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 48 CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. When enabling Litestream v5 backups, configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which to perform an SQLite checkpoint to apply the writes in the WAL to the main database file. Each checkpoint creates a new WAL segment file that will be backed up by Litestream v3. Smaller thresholds may improve read performance, at the expense of creating more files to download when restoring the replica from the backup. This setting is ignored when backing up with Litestream v5. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 is enabled, this executable is required. The official Zero Docker image includes Litestream 0.5.16. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. When a backup URL is configured, disabling this option is also the way to run without a v5 executable. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. This is disabled by default to continue writing legacy WAL backups. Enabling it requires v5 restore and ZERO_LITESTREAM_VFS_QUERY_EXECUTABLE. With v5 backup enabled, Zero acknowledges upstream changes only after they are applied to the replica and confirmed in the backup. Each replica uses a distinct backup subpath under the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Configure an object-store lifecycle policy to remove old subpaths; Zero does not delete them automatically. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: false Litestream VFS Query Executable Path to the vfs-query executable used to monitor Litestream v5 backup progress. This is required when ZERO_LITESTREAM_BACKUP_USING_V5 is enabled. The official Zero Docker image includes this executable. flag: --litestream-vfs-query-executable env: ZERO_LITESTREAM_VFS_QUERY_EXECUTABLE Litestream VFS Poll Interval (ms) How often the Litestream VFS polls backup storage for the latest backup. This controls how quickly Zero can confirm a backup and acknowledge the corresponding upstream changes. flag: --litestream-vfs-poll-interval-ms env: ZERO_LITESTREAM_VFS_POLL_INTERVAL_MS default: 15000 Litestream VFS Poll Timeout (ms) Reserved for configuring request timeouts to the Litestream VFS poller. In this release, vfs-query uses its internal 30-second timeout regardless of this value. flag: --litestream-vfs-poll-timeout-ms env: ZERO_LITESTREAM_VFS_POLL_TIMEOUT_MS default: 10000 Litestream VFS Log File Reserved for redirecting Litestream VFS logs. In this release, vfs-query writes logs to the child process's standard error regardless of this value. flag: --litestream-vfs-log-file env: ZERO_LITESTREAM_VFS_LOG_FILE Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Minimum Checkpoint Page Count The WAL page count at which SQLite attempts a PASSIVE checkpoint, which transfers pages to the main database file without blocking writers. Defaults to checkpointThresholdMB * 250 (since SQLite page size is 4KB). This setting is ignored when backing up with Litestream v5. flag: --litestream-min-checkpoint-page-count env: ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT default: checkpointThresholdMB * 250 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY default: 48 Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Report Interval (ms) The minimum interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 30_000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10", + "content": "Allow Legacy Queries Allows clients to send query ASTs directly to zero-cache. Ad-hoc queries (also called legacy queries) work this way. When this is false, zero-cache rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use queries. flag: --allow-legacy-queries env: ZERO_ALLOW_LEGACY_QUERIES default: false App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: 300 Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: 300 Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 4 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 168 (one week) CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. With Litestream v5 backups (the default), configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 or ZERO_LITESTREAM_BACKUP_USING_V5 is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build 0.5.18-zero.16. Restores fork the source backup with litestream restore -fork-to-url, so a custom executable must support that flag. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and ZERO_LITESTREAM_BACKUP_USING_V5 to false. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. Set this to false to write legacy WAL backups with Litestream v0.3.x instead. This requires ZERO_LITESTREAM_RESTORE_USING_V5, which is enabled by default. Zero requests an incremental backup every ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS, and acknowledges upstream changes only after a backup containing them has completed. Each replication-manager backs up to its own subfolder of the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: true Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. Leave this unset to use 48, lowered as needed so that the parts fit in a quarter of the memory available to the container. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Operator Password A password that grants access only to the diagnostic endpoints that cannot expose application data. People who operate zero-cache can use it to diagnose the server without being able to read the data it serves. These endpoints are: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Initial Readiness Threshold (ms) The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. When lag reporting is disabled, readiness does not wait for replication lag. flag: --replication-lag-initial-readiness-threshold-ms env: ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS default: 1000 Replication Lag Max Readiness Delay (ms) The maximum time that a server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. flag: --replication-lag-max-readiness-delay-ms env: ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS default: 600000 (10 minutes) Replication Lag Report Interval (ms) The approximate interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. Lag reports are also used at startup: a new server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready. This interval sets how long the server waits for the next report. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 10000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS View Syncer Hydration Budget (ms) A soft time limit for hydrating inactive (cached) queries when a view-syncer hydrates a client group's queries, for example after a client reconnects. Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your query endpoint does not count toward the budget. A value of 0 disables the budget. flag: --view-syncer-hydration-budget-ms env: ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS default: 0 View Syncer Query Hydration Timeout (ms) The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see Handling Errors). For the next five minutes, the query is rejected without being run again. After that, it can be retried. A value of 0 disables the limit. flag: --view-syncer-query-hydration-timeout-ms env: ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS default: 0 Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10", "kind": "section" }, { - "id": "596-zero-cache-config#app-id", + "id": "620-zero-cache-config#allow-legacy-queries", + "title": "zero-cache Config", + "searchTitle": "Allow Legacy Queries", + "sectionTitle": "Allow Legacy Queries", + "sectionId": "allow-legacy-queries", + "url": "/docs/zero-cache-config", + "content": "Allows clients to send query ASTs directly to zero-cache. Ad-hoc queries (also called legacy queries) work this way. When this is false, zero-cache rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use queries. flag: --allow-legacy-queries env: ZERO_ALLOW_LEGACY_QUERIES default: false", + "kind": "section" + }, + { + "id": "621-zero-cache-config#app-id", "title": "zero-cache Config", "searchTitle": "App ID", "sectionTitle": "App ID", @@ -8386,7 +8736,7 @@ "kind": "section" }, { - "id": "597-zero-cache-config#app-publications", + "id": "622-zero-cache-config#app-publications", "title": "zero-cache Config", "searchTitle": "App Publications", "sectionTitle": "App Publications", @@ -8396,27 +8746,27 @@ "kind": "section" }, { - "id": "598-zero-cache-config#auth-revalidate-interval-seconds", + "id": "623-zero-cache-config#auth-revalidate-interval-seconds", "title": "zero-cache Config", "searchTitle": "Auth Revalidate Interval Seconds", "sectionTitle": "Auth Revalidate Interval Seconds", "sectionId": "auth-revalidate-interval-seconds", "url": "/docs/zero-cache-config", - "content": "How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: unset", + "content": "How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: 300", "kind": "section" }, { - "id": "599-zero-cache-config#auth-retransform-interval-seconds", + "id": "624-zero-cache-config#auth-retransform-interval-seconds", "title": "zero-cache Config", "searchTitle": "Auth Retransform Interval Seconds", "sectionTitle": "Auth Retransform Interval Seconds", "sectionId": "auth-retransform-interval-seconds", "url": "/docs/zero-cache-config", - "content": "How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: unset", + "content": "How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: 300", "kind": "section" }, { - "id": "600-zero-cache-config#auto-reset", + "id": "625-zero-cache-config#auto-reset", "title": "zero-cache Config", "searchTitle": "Auto Reset", "sectionTitle": "Auto Reset", @@ -8426,7 +8776,7 @@ "kind": "section" }, { - "id": "601-zero-cache-config#change-db", + "id": "626-zero-cache-config#change-db", "title": "zero-cache Config", "searchTitle": "Change DB", "sectionTitle": "Change DB", @@ -8436,7 +8786,7 @@ "kind": "section" }, { - "id": "602-zero-cache-config#change-streamer-back-pressure-limit-heap-proportion", + "id": "627-zero-cache-config#change-streamer-back-pressure-limit-heap-proportion", "title": "zero-cache Config", "searchTitle": "Change Streamer Back Pressure Limit Heap Proportion", "sectionTitle": "Change Streamer Back Pressure Limit Heap Proportion", @@ -8446,17 +8796,17 @@ "kind": "section" }, { - "id": "603-zero-cache-config#change-streamer-flow-control-consensus-timeout-proportion", + "id": "628-zero-cache-config#change-streamer-flow-control-consensus-timeout-proportion", "title": "zero-cache Config", "searchTitle": "Change Streamer Flow Control Consensus Timeout Proportion", "sectionTitle": "Change Streamer Flow Control Consensus Timeout Proportion", "sectionId": "change-streamer-flow-control-consensus-timeout-proportion", "url": "/docs/zero-cache-config", - "content": "During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly three times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 2", + "content": "During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 4", "kind": "section" }, { - "id": "604-zero-cache-config#change-streamer-flow-control-slow-subscriber-grace-period-seconds", + "id": "629-zero-cache-config#change-streamer-flow-control-slow-subscriber-grace-period-seconds", "title": "zero-cache Config", "searchTitle": "Change Streamer Flow Control Slow Subscriber Grace Period Seconds", "sectionTitle": "Change Streamer Flow Control Slow Subscriber Grace Period Seconds", @@ -8466,17 +8816,7 @@ "kind": "section" }, { - "id": "605-zero-cache-config#change-streamer-mode", - "title": "zero-cache Config", - "searchTitle": "Change Streamer Mode", - "sectionTitle": "Change Streamer Mode", - "sectionId": "change-streamer-mode", - "url": "/docs/zero-cache-config", - "content": "The mode for running or connecting to the change-streamer: dedicated: runs the change-streamer and shuts down when another change-streamer takes over the replication slot. This is appropriate in a single-node configuration, or for the replication-manager in a multi-node configuration. discover: connects to the change-streamer as internally advertised in the change-db. This is appropriate for the view-syncers in a multi-node setup. This may not work in all networking configurations (e.g., some private networking or port forwarding setups). Using ZERO_CHANGE_STREAMER_URI with an explicit routable hostname is recommended instead. This option is ignored if ZERO_CHANGE_STREAMER_URI is set. flag: --change-streamer-mode env: ZERO_CHANGE_STREAMER_MODE default: dedicated", - "kind": "section" - }, - { - "id": "606-zero-cache-config#change-streamer-port", + "id": "630-zero-cache-config#change-streamer-port", "title": "zero-cache Config", "searchTitle": "Change Streamer Port", "sectionTitle": "Change Streamer Port", @@ -8486,7 +8826,7 @@ "kind": "section" }, { - "id": "607-zero-cache-config#change-streamer-startup-delay-ms", + "id": "631-zero-cache-config#change-streamer-startup-delay-ms", "title": "zero-cache Config", "searchTitle": "Change Streamer Startup Delay (ms)", "sectionTitle": "Change Streamer Startup Delay (ms)", @@ -8496,7 +8836,7 @@ "kind": "section" }, { - "id": "608-zero-cache-config#change-streamer-uri", + "id": "632-zero-cache-config#change-streamer-uri", "title": "zero-cache Config", "searchTitle": "Change Streamer URI", "sectionTitle": "Change Streamer URI", @@ -8506,7 +8846,7 @@ "kind": "section" }, { - "id": "609-zero-cache-config#cvr-db", + "id": "633-zero-cache-config#cvr-db", "title": "zero-cache Config", "searchTitle": "CVR DB", "sectionTitle": "CVR DB", @@ -8516,17 +8856,17 @@ "kind": "section" }, { - "id": "610-zero-cache-config#cvr-garbage-collection-inactivity-threshold-hours", + "id": "634-zero-cache-config#cvr-garbage-collection-inactivity-threshold-hours", "title": "zero-cache Config", "searchTitle": "CVR Garbage Collection Inactivity Threshold Hours", "sectionTitle": "CVR Garbage Collection Inactivity Threshold Hours", "sectionId": "cvr-garbage-collection-inactivity-threshold-hours", "url": "/docs/zero-cache-config", - "content": "The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 48", + "content": "The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 168 (one week)", "kind": "section" }, { - "id": "611-zero-cache-config#cvr-garbage-collection-initial-batch-size", + "id": "635-zero-cache-config#cvr-garbage-collection-initial-batch-size", "title": "zero-cache Config", "searchTitle": "CVR Garbage Collection Initial Batch Size", "sectionTitle": "CVR Garbage Collection Initial Batch Size", @@ -8536,7 +8876,7 @@ "kind": "section" }, { - "id": "612-zero-cache-config#cvr-garbage-collection-initial-interval-seconds", + "id": "636-zero-cache-config#cvr-garbage-collection-initial-interval-seconds", "title": "zero-cache Config", "searchTitle": "CVR Garbage Collection Initial Interval Seconds", "sectionTitle": "CVR Garbage Collection Initial Interval Seconds", @@ -8546,7 +8886,7 @@ "kind": "section" }, { - "id": "613-zero-cache-config#cvr-max-connections", + "id": "637-zero-cache-config#cvr-max-connections", "title": "zero-cache Config", "searchTitle": "CVR Max Connections", "sectionTitle": "CVR Max Connections", @@ -8556,7 +8896,7 @@ "kind": "section" }, { - "id": "614-zero-cache-config#enable-query-planner", + "id": "638-zero-cache-config#enable-query-planner", "title": "zero-cache Config", "searchTitle": "Enable Query Planner", "sectionTitle": "Enable Query Planner", @@ -8566,7 +8906,7 @@ "kind": "section" }, { - "id": "615-zero-cache-config#enable-crud-mutations", + "id": "639-zero-cache-config#enable-crud-mutations", "title": "zero-cache Config", "searchTitle": "Enable CRUD Mutations", "sectionTitle": "Enable CRUD Mutations", @@ -8576,7 +8916,7 @@ "kind": "section" }, { - "id": "616-zero-cache-config#enable-telemetry", + "id": "640-zero-cache-config#enable-telemetry", "title": "zero-cache Config", "searchTitle": "Enable Telemetry", "sectionTitle": "Enable Telemetry", @@ -8586,7 +8926,7 @@ "kind": "section" }, { - "id": "617-zero-cache-config#initial-sync-table-copy-workers", + "id": "641-zero-cache-config#initial-sync-table-copy-workers", "title": "zero-cache Config", "searchTitle": "Initial Sync Table Copy Workers", "sectionTitle": "Initial Sync Table Copy Workers", @@ -8596,7 +8936,7 @@ "kind": "section" }, { - "id": "618-zero-cache-config#lazy-startup", + "id": "642-zero-cache-config#lazy-startup", "title": "zero-cache Config", "searchTitle": "Lazy Startup", "sectionTitle": "Lazy Startup", @@ -8606,17 +8946,17 @@ "kind": "section" }, { - "id": "619-zero-cache-config#litestream-backup-url", + "id": "643-zero-cache-config#litestream-backup-url", "title": "zero-cache Config", "searchTitle": "Litestream Backup URL", "sectionTitle": "Litestream Backup URL", "sectionId": "litestream-backup-url", "url": "/docs/zero-cache-config", - "content": "The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. When enabling Litestream v5 backups, configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL", + "content": "The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. With Litestream v5 backups (the default), configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL", "kind": "section" }, { - "id": "620-zero-cache-config#litestream-endpoint", + "id": "644-zero-cache-config#litestream-endpoint", "title": "zero-cache Config", "searchTitle": "Litestream Endpoint", "sectionTitle": "Litestream Endpoint", @@ -8626,17 +8966,17 @@ "kind": "section" }, { - "id": "621-zero-cache-config#litestream-checkpoint-threshold-mb", + "id": "645-zero-cache-config#litestream-checkpoint-threshold-mb", "title": "zero-cache Config", "searchTitle": "Litestream Checkpoint Threshold MB", "sectionTitle": "Litestream Checkpoint Threshold MB", "sectionId": "litestream-checkpoint-threshold-mb", "url": "/docs/zero-cache-config", - "content": "The size of the WAL file at which to perform an SQLite checkpoint to apply the writes in the WAL to the main database file. Each checkpoint creates a new WAL segment file that will be backed up by Litestream v3. Smaller thresholds may improve read performance, at the expense of creating more files to download when restoring the replica from the backup. This setting is ignored when backing up with Litestream v5. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40", + "content": "The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40", "kind": "section" }, { - "id": "622-zero-cache-config#litestream-config-path", + "id": "646-zero-cache-config#litestream-config-path", "title": "zero-cache Config", "searchTitle": "Litestream Config Path", "sectionTitle": "Litestream Config Path", @@ -8646,7 +8986,7 @@ "kind": "section" }, { - "id": "623-zero-cache-config#litestream-config-path-v5", + "id": "647-zero-cache-config#litestream-config-path-v5", "title": "zero-cache Config", "searchTitle": "Litestream Config Path V5", "sectionTitle": "Litestream Config Path V5", @@ -8656,7 +8996,7 @@ "kind": "section" }, { - "id": "624-zero-cache-config#litestream-executable", + "id": "648-zero-cache-config#litestream-executable", "title": "zero-cache Config", "searchTitle": "Litestream Executable", "sectionTitle": "Litestream Executable", @@ -8666,77 +9006,37 @@ "kind": "section" }, { - "id": "625-zero-cache-config#litestream-v5-executable", + "id": "649-zero-cache-config#litestream-v5-executable", "title": "zero-cache Config", "searchTitle": "Litestream V5 Executable", "sectionTitle": "Litestream V5 Executable", "sectionId": "litestream-v5-executable", "url": "/docs/zero-cache-config", - "content": "Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 is enabled, this executable is required. The official Zero Docker image includes Litestream 0.5.16. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5", + "content": "Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 or ZERO_LITESTREAM_BACKUP_USING_V5 is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build 0.5.18-zero.16. Restores fork the source backup with litestream restore -fork-to-url, so a custom executable must support that flag. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5", "kind": "section" }, { - "id": "626-zero-cache-config#litestream-restore-using-v5", + "id": "650-zero-cache-config#litestream-restore-using-v5", "title": "zero-cache Config", "searchTitle": "Litestream Restore Using V5", "sectionTitle": "Litestream Restore Using V5", "sectionId": "litestream-restore-using-v5", "url": "/docs/zero-cache-config", - "content": "Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. When a backup URL is configured, disabling this option is also the way to run without a v5 executable. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true", + "content": "Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and ZERO_LITESTREAM_BACKUP_USING_V5 to false. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true", "kind": "section" }, { - "id": "627-zero-cache-config#litestream-backup-using-v5", + "id": "651-zero-cache-config#litestream-backup-using-v5", "title": "zero-cache Config", "searchTitle": "Litestream Backup Using V5", "sectionTitle": "Litestream Backup Using V5", "sectionId": "litestream-backup-using-v5", "url": "/docs/zero-cache-config", - "content": "Write LTX backups with Litestream v0.5.x. This is disabled by default to continue writing legacy WAL backups. Enabling it requires v5 restore and ZERO_LITESTREAM_VFS_QUERY_EXECUTABLE. With v5 backup enabled, Zero acknowledges upstream changes only after they are applied to the replica and confirmed in the backup. Each replica uses a distinct backup subpath under the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Configure an object-store lifecycle policy to remove old subpaths; Zero does not delete them automatically. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: false", - "kind": "section" - }, - { - "id": "628-zero-cache-config#litestream-vfs-query-executable", - "title": "zero-cache Config", - "searchTitle": "Litestream VFS Query Executable", - "sectionTitle": "Litestream VFS Query Executable", - "sectionId": "litestream-vfs-query-executable", - "url": "/docs/zero-cache-config", - "content": "Path to the vfs-query executable used to monitor Litestream v5 backup progress. This is required when ZERO_LITESTREAM_BACKUP_USING_V5 is enabled. The official Zero Docker image includes this executable. flag: --litestream-vfs-query-executable env: ZERO_LITESTREAM_VFS_QUERY_EXECUTABLE", - "kind": "section" - }, - { - "id": "629-zero-cache-config#litestream-vfs-poll-interval-ms", - "title": "zero-cache Config", - "searchTitle": "Litestream VFS Poll Interval (ms)", - "sectionTitle": "Litestream VFS Poll Interval (ms)", - "sectionId": "litestream-vfs-poll-interval-ms", - "url": "/docs/zero-cache-config", - "content": "How often the Litestream VFS polls backup storage for the latest backup. This controls how quickly Zero can confirm a backup and acknowledge the corresponding upstream changes. flag: --litestream-vfs-poll-interval-ms env: ZERO_LITESTREAM_VFS_POLL_INTERVAL_MS default: 15000", - "kind": "section" - }, - { - "id": "630-zero-cache-config#litestream-vfs-poll-timeout-ms", - "title": "zero-cache Config", - "searchTitle": "Litestream VFS Poll Timeout (ms)", - "sectionTitle": "Litestream VFS Poll Timeout (ms)", - "sectionId": "litestream-vfs-poll-timeout-ms", - "url": "/docs/zero-cache-config", - "content": "Reserved for configuring request timeouts to the Litestream VFS poller. In this release, vfs-query uses its internal 30-second timeout regardless of this value. flag: --litestream-vfs-poll-timeout-ms env: ZERO_LITESTREAM_VFS_POLL_TIMEOUT_MS default: 10000", - "kind": "section" - }, - { - "id": "631-zero-cache-config#litestream-vfs-log-file", - "title": "zero-cache Config", - "searchTitle": "Litestream VFS Log File", - "sectionTitle": "Litestream VFS Log File", - "sectionId": "litestream-vfs-log-file", - "url": "/docs/zero-cache-config", - "content": "Reserved for redirecting Litestream VFS logs. In this release, vfs-query writes logs to the child process's standard error regardless of this value. flag: --litestream-vfs-log-file env: ZERO_LITESTREAM_VFS_LOG_FILE", + "content": "Write LTX backups with Litestream v0.5.x. Set this to false to write legacy WAL backups with Litestream v0.3.x instead. This requires ZERO_LITESTREAM_RESTORE_USING_V5, which is enabled by default. Zero requests an incremental backup every ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS, and acknowledges upstream changes only after a backup containing them has completed. Each replication-manager backs up to its own subfolder of the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: true", "kind": "section" }, { - "id": "632-zero-cache-config#litestream-incremental-backup-interval-minutes", + "id": "652-zero-cache-config#litestream-incremental-backup-interval-minutes", "title": "zero-cache Config", "searchTitle": "Litestream Incremental Backup Interval Minutes", "sectionTitle": "Litestream Incremental Backup Interval Minutes", @@ -8746,7 +9046,7 @@ "kind": "section" }, { - "id": "633-zero-cache-config#litestream-incremental-backup-interval-seconds", + "id": "653-zero-cache-config#litestream-incremental-backup-interval-seconds", "title": "zero-cache Config", "searchTitle": "Litestream Incremental Backup Interval Seconds", "sectionTitle": "Litestream Incremental Backup Interval Seconds", @@ -8756,7 +9056,7 @@ "kind": "section" }, { - "id": "634-zero-cache-config#litestream-maximum-checkpoint-page-count", + "id": "654-zero-cache-config#litestream-maximum-checkpoint-page-count", "title": "zero-cache Config", "searchTitle": "Litestream Maximum Checkpoint Page Count", "sectionTitle": "Litestream Maximum Checkpoint Page Count", @@ -8766,27 +9066,17 @@ "kind": "section" }, { - "id": "635-zero-cache-config#litestream-minimum-checkpoint-page-count", - "title": "zero-cache Config", - "searchTitle": "Litestream Minimum Checkpoint Page Count", - "sectionTitle": "Litestream Minimum Checkpoint Page Count", - "sectionId": "litestream-minimum-checkpoint-page-count", - "url": "/docs/zero-cache-config", - "content": "The WAL page count at which SQLite attempts a PASSIVE checkpoint, which transfers pages to the main database file without blocking writers. Defaults to checkpointThresholdMB * 250 (since SQLite page size is 4KB). This setting is ignored when backing up with Litestream v5. flag: --litestream-min-checkpoint-page-count env: ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT default: checkpointThresholdMB * 250", - "kind": "section" - }, - { - "id": "636-zero-cache-config#litestream-multipart-concurrency", + "id": "655-zero-cache-config#litestream-multipart-concurrency", "title": "zero-cache Config", "searchTitle": "Litestream Multipart Concurrency", "sectionTitle": "Litestream Multipart Concurrency", "sectionId": "litestream-multipart-concurrency", "url": "/docs/zero-cache-config", - "content": "The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY default: 48", + "content": "The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. Leave this unset to use 48, lowered as needed so that the parts fit in a quarter of the memory available to the container. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY", "kind": "section" }, { - "id": "637-zero-cache-config#litestream-multipart-size", + "id": "656-zero-cache-config#litestream-multipart-size", "title": "zero-cache Config", "searchTitle": "Litestream Multipart Size", "sectionTitle": "Litestream Multipart Size", @@ -8796,7 +9086,7 @@ "kind": "section" }, { - "id": "638-zero-cache-config#litestream-log-level", + "id": "657-zero-cache-config#litestream-log-level", "title": "zero-cache Config", "searchTitle": "Litestream Log Level", "sectionTitle": "Litestream Log Level", @@ -8806,7 +9096,7 @@ "kind": "section" }, { - "id": "639-zero-cache-config#litestream-port", + "id": "658-zero-cache-config#litestream-port", "title": "zero-cache Config", "searchTitle": "Litestream Port", "sectionTitle": "Litestream Port", @@ -8816,7 +9106,7 @@ "kind": "section" }, { - "id": "640-zero-cache-config#litestream-region", + "id": "659-zero-cache-config#litestream-region", "title": "zero-cache Config", "searchTitle": "Litestream Region", "sectionTitle": "Litestream Region", @@ -8826,7 +9116,7 @@ "kind": "section" }, { - "id": "641-zero-cache-config#litestream-restore-parallelism", + "id": "660-zero-cache-config#litestream-restore-parallelism", "title": "zero-cache Config", "searchTitle": "Litestream Restore Parallelism", "sectionTitle": "Litestream Restore Parallelism", @@ -8836,7 +9126,7 @@ "kind": "section" }, { - "id": "642-zero-cache-config#litestream-snapshot-backup-interval-hours", + "id": "661-zero-cache-config#litestream-snapshot-backup-interval-hours", "title": "zero-cache Config", "searchTitle": "Litestream Snapshot Backup Interval Hours", "sectionTitle": "Litestream Snapshot Backup Interval Hours", @@ -8846,7 +9136,7 @@ "kind": "section" }, { - "id": "643-zero-cache-config#log-format", + "id": "662-zero-cache-config#log-format", "title": "zero-cache Config", "searchTitle": "Log Format", "sectionTitle": "Log Format", @@ -8856,7 +9146,7 @@ "kind": "section" }, { - "id": "644-zero-cache-config#log-ivm-sampling", + "id": "663-zero-cache-config#log-ivm-sampling", "title": "zero-cache Config", "searchTitle": "Log IVM Sampling", "sectionTitle": "Log IVM Sampling", @@ -8866,7 +9156,7 @@ "kind": "section" }, { - "id": "645-zero-cache-config#log-level", + "id": "664-zero-cache-config#log-level", "title": "zero-cache Config", "searchTitle": "Log Level", "sectionTitle": "Log Level", @@ -8876,7 +9166,7 @@ "kind": "section" }, { - "id": "646-zero-cache-config#log-slow-hydrate-threshold", + "id": "665-zero-cache-config#log-slow-hydrate-threshold", "title": "zero-cache Config", "searchTitle": "Log Slow Hydrate Threshold", "sectionTitle": "Log Slow Hydrate Threshold", @@ -8886,7 +9176,7 @@ "kind": "section" }, { - "id": "647-zero-cache-config#log-slow-row-threshold", + "id": "666-zero-cache-config#log-slow-row-threshold", "title": "zero-cache Config", "searchTitle": "Log Slow Row Threshold", "sectionTitle": "Log Slow Row Threshold", @@ -8896,7 +9186,7 @@ "kind": "section" }, { - "id": "648-zero-cache-config#mutate-api-key", + "id": "667-zero-cache-config#mutate-api-key", "title": "zero-cache Config", "searchTitle": "Mutate API Key", "sectionTitle": "Mutate API Key", @@ -8906,7 +9196,7 @@ "kind": "section" }, { - "id": "649-zero-cache-config#mutate-allowed-client-headers", + "id": "668-zero-cache-config#mutate-allowed-client-headers", "title": "zero-cache Config", "searchTitle": "Mutate Allowed Client Headers", "sectionTitle": "Mutate Allowed Client Headers", @@ -8916,7 +9206,7 @@ "kind": "section" }, { - "id": "650-zero-cache-config#mutate-allowed-request-headers", + "id": "669-zero-cache-config#mutate-allowed-request-headers", "title": "zero-cache Config", "searchTitle": "Mutate Allowed Request Headers", "sectionTitle": "Mutate Allowed Request Headers", @@ -8926,7 +9216,7 @@ "kind": "section" }, { - "id": "651-zero-cache-config#mutate-forward-cookies", + "id": "670-zero-cache-config#mutate-forward-cookies", "title": "zero-cache Config", "searchTitle": "Mutate Forward Cookies", "sectionTitle": "Mutate Forward Cookies", @@ -8936,7 +9226,7 @@ "kind": "section" }, { - "id": "652-zero-cache-config#mutate-url", + "id": "671-zero-cache-config#mutate-url", "title": "zero-cache Config", "searchTitle": "Mutate URL", "sectionTitle": "Mutate URL", @@ -8946,7 +9236,7 @@ "kind": "section" }, { - "id": "653-zero-cache-config#number-of-sync-workers", + "id": "672-zero-cache-config#number-of-sync-workers", "title": "zero-cache Config", "searchTitle": "Number of Sync Workers", "sectionTitle": "Number of Sync Workers", @@ -8956,7 +9246,17 @@ "kind": "section" }, { - "id": "654-zero-cache-config#per-user-mutation-limit-max", + "id": "673-zero-cache-config#operator-password", + "title": "zero-cache Config", + "searchTitle": "Operator Password", + "sectionTitle": "Operator Password", + "sectionId": "operator-password", + "url": "/docs/zero-cache-config", + "content": "A password that grants access only to the diagnostic endpoints that cannot expose application data. People who operate zero-cache can use it to diagnose the server without being able to read the data it serves. These endpoints are: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD", + "kind": "section" + }, + { + "id": "674-zero-cache-config#per-user-mutation-limit-max", "title": "zero-cache Config", "searchTitle": "Per User Mutation Limit Max", "sectionTitle": "Per User Mutation Limit Max", @@ -8966,7 +9266,7 @@ "kind": "section" }, { - "id": "655-zero-cache-config#per-user-mutation-limit-window-ms", + "id": "675-zero-cache-config#per-user-mutation-limit-window-ms", "title": "zero-cache Config", "searchTitle": "Per User Mutation Limit Window (ms)", "sectionTitle": "Per User Mutation Limit Window (ms)", @@ -8976,7 +9276,7 @@ "kind": "section" }, { - "id": "656-zero-cache-config#pg-replication-slot-failover", + "id": "676-zero-cache-config#pg-replication-slot-failover", "title": "zero-cache Config", "searchTitle": "PG Replication Slot Failover", "sectionTitle": "PG Replication Slot Failover", @@ -8986,7 +9286,7 @@ "kind": "section" }, { - "id": "657-zero-cache-config#port", + "id": "677-zero-cache-config#port", "title": "zero-cache Config", "searchTitle": "Port", "sectionTitle": "Port", @@ -8996,7 +9296,7 @@ "kind": "section" }, { - "id": "658-zero-cache-config#query-api-key", + "id": "678-zero-cache-config#query-api-key", "title": "zero-cache Config", "searchTitle": "Query API Key", "sectionTitle": "Query API Key", @@ -9006,7 +9306,7 @@ "kind": "section" }, { - "id": "659-zero-cache-config#query-allowed-client-headers", + "id": "679-zero-cache-config#query-allowed-client-headers", "title": "zero-cache Config", "searchTitle": "Query Allowed Client Headers", "sectionTitle": "Query Allowed Client Headers", @@ -9016,7 +9316,7 @@ "kind": "section" }, { - "id": "660-zero-cache-config#query-allowed-request-headers", + "id": "680-zero-cache-config#query-allowed-request-headers", "title": "zero-cache Config", "searchTitle": "Query Allowed Request Headers", "sectionTitle": "Query Allowed Request Headers", @@ -9026,7 +9326,7 @@ "kind": "section" }, { - "id": "661-zero-cache-config#query-forward-cookies", + "id": "681-zero-cache-config#query-forward-cookies", "title": "zero-cache Config", "searchTitle": "Query Forward Cookies", "sectionTitle": "Query Forward Cookies", @@ -9036,7 +9336,7 @@ "kind": "section" }, { - "id": "662-zero-cache-config#query-hydration-stats", + "id": "682-zero-cache-config#query-hydration-stats", "title": "zero-cache Config", "searchTitle": "Query Hydration Stats", "sectionTitle": "Query Hydration Stats", @@ -9046,7 +9346,7 @@ "kind": "section" }, { - "id": "663-zero-cache-config#query-url", + "id": "683-zero-cache-config#query-url", "title": "zero-cache Config", "searchTitle": "Query URL", "sectionTitle": "Query URL", @@ -9056,7 +9356,7 @@ "kind": "section" }, { - "id": "664-zero-cache-config#replica-file", + "id": "684-zero-cache-config#replica-file", "title": "zero-cache Config", "searchTitle": "Replica File", "sectionTitle": "Replica File", @@ -9066,7 +9366,7 @@ "kind": "section" }, { - "id": "665-zero-cache-config#replica-vacuum-interval-hours", + "id": "685-zero-cache-config#replica-vacuum-interval-hours", "title": "zero-cache Config", "searchTitle": "Replica Vacuum Interval Hours", "sectionTitle": "Replica Vacuum Interval Hours", @@ -9076,17 +9376,37 @@ "kind": "section" }, { - "id": "666-zero-cache-config#replication-lag-report-interval-ms", + "id": "686-zero-cache-config#replication-lag-initial-readiness-threshold-ms", + "title": "zero-cache Config", + "searchTitle": "Replication Lag Initial Readiness Threshold (ms)", + "sectionTitle": "Replication Lag Initial Readiness Threshold (ms)", + "sectionId": "replication-lag-initial-readiness-threshold-ms", + "url": "/docs/zero-cache-config", + "content": "The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. When lag reporting is disabled, readiness does not wait for replication lag. flag: --replication-lag-initial-readiness-threshold-ms env: ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS default: 1000", + "kind": "section" + }, + { + "id": "687-zero-cache-config#replication-lag-max-readiness-delay-ms", + "title": "zero-cache Config", + "searchTitle": "Replication Lag Max Readiness Delay (ms)", + "sectionTitle": "Replication Lag Max Readiness Delay (ms)", + "sectionId": "replication-lag-max-readiness-delay-ms", + "url": "/docs/zero-cache-config", + "content": "The maximum time that a server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. flag: --replication-lag-max-readiness-delay-ms env: ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS default: 600000 (10 minutes)", + "kind": "section" + }, + { + "id": "688-zero-cache-config#replication-lag-report-interval-ms", "title": "zero-cache Config", "searchTitle": "Replication Lag Report Interval (ms)", "sectionTitle": "Replication Lag Report Interval (ms)", "sectionId": "replication-lag-report-interval-ms", "url": "/docs/zero-cache-config", - "content": "The minimum interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 30_000", + "content": "The approximate interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. Lag reports are also used at startup: a new server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready. This interval sets how long the server waits for the next report. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 10000", "kind": "section" }, { - "id": "667-zero-cache-config#server-version", + "id": "689-zero-cache-config#server-version", "title": "zero-cache Config", "searchTitle": "Server Version", "sectionTitle": "Server Version", @@ -9096,7 +9416,7 @@ "kind": "section" }, { - "id": "668-zero-cache-config#shadow-sync-enabled", + "id": "690-zero-cache-config#shadow-sync-enabled", "title": "zero-cache Config", "searchTitle": "Shadow Sync Enabled", "sectionTitle": "Shadow Sync Enabled", @@ -9106,7 +9426,7 @@ "kind": "section" }, { - "id": "669-zero-cache-config#shadow-sync-interval-hours", + "id": "691-zero-cache-config#shadow-sync-interval-hours", "title": "zero-cache Config", "searchTitle": "Shadow Sync Interval Hours", "sectionTitle": "Shadow Sync Interval Hours", @@ -9116,7 +9436,7 @@ "kind": "section" }, { - "id": "670-zero-cache-config#shadow-sync-sample-rate", + "id": "692-zero-cache-config#shadow-sync-sample-rate", "title": "zero-cache Config", "searchTitle": "Shadow Sync Sample Rate", "sectionTitle": "Shadow Sync Sample Rate", @@ -9126,7 +9446,7 @@ "kind": "section" }, { - "id": "671-zero-cache-config#shadow-sync-max-rows-per-table", + "id": "693-zero-cache-config#shadow-sync-max-rows-per-table", "title": "zero-cache Config", "searchTitle": "Shadow Sync Max Rows Per Table", "sectionTitle": "Shadow Sync Max Rows Per Table", @@ -9136,7 +9456,7 @@ "kind": "section" }, { - "id": "672-zero-cache-config#storage-db-temp-dir", + "id": "694-zero-cache-config#storage-db-temp-dir", "title": "zero-cache Config", "searchTitle": "Storage DB Temp Dir", "sectionTitle": "Storage DB Temp Dir", @@ -9146,7 +9466,7 @@ "kind": "section" }, { - "id": "673-zero-cache-config#task-id", + "id": "695-zero-cache-config#task-id", "title": "zero-cache Config", "searchTitle": "Task ID", "sectionTitle": "Task ID", @@ -9156,7 +9476,7 @@ "kind": "section" }, { - "id": "674-zero-cache-config#upstream-max-connections", + "id": "696-zero-cache-config#upstream-max-connections", "title": "zero-cache Config", "searchTitle": "Upstream Max Connections", "sectionTitle": "Upstream Max Connections", @@ -9166,7 +9486,7 @@ "kind": "section" }, { - "id": "675-zero-cache-config#upstream-pg-replication-slot-failover", + "id": "697-zero-cache-config#upstream-pg-replication-slot-failover", "title": "zero-cache Config", "searchTitle": "Upstream PG Replication Slot Failover", "sectionTitle": "Upstream PG Replication Slot Failover", @@ -9176,7 +9496,7 @@ "kind": "section" }, { - "id": "676-zero-cache-config#upstream-pg-stream-inbound-timeout", + "id": "698-zero-cache-config#upstream-pg-stream-inbound-timeout", "title": "zero-cache Config", "searchTitle": "Upstream PG Stream Inbound Timeout", "sectionTitle": "Upstream PG Stream Inbound Timeout", @@ -9186,7 +9506,27 @@ "kind": "section" }, { - "id": "677-zero-cache-config#websocket-compression", + "id": "699-zero-cache-config#view-syncer-hydration-budget-ms", + "title": "zero-cache Config", + "searchTitle": "View Syncer Hydration Budget (ms)", + "sectionTitle": "View Syncer Hydration Budget (ms)", + "sectionId": "view-syncer-hydration-budget-ms", + "url": "/docs/zero-cache-config", + "content": "A soft time limit for hydrating inactive (cached) queries when a view-syncer hydrates a client group's queries, for example after a client reconnects. Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your query endpoint does not count toward the budget. A value of 0 disables the budget. flag: --view-syncer-hydration-budget-ms env: ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS default: 0", + "kind": "section" + }, + { + "id": "700-zero-cache-config#view-syncer-query-hydration-timeout-ms", + "title": "zero-cache Config", + "searchTitle": "View Syncer Query Hydration Timeout (ms)", + "sectionTitle": "View Syncer Query Hydration Timeout (ms)", + "sectionId": "view-syncer-query-hydration-timeout-ms", + "url": "/docs/zero-cache-config", + "content": "The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see Handling Errors). For the next five minutes, the query is rejected without being run again. After that, it can be retried. A value of 0 disables the limit. flag: --view-syncer-query-hydration-timeout-ms env: ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS default: 0", + "kind": "section" + }, + { + "id": "701-zero-cache-config#websocket-compression", "title": "zero-cache Config", "searchTitle": "Websocket Compression", "sectionTitle": "Websocket Compression", @@ -9196,7 +9536,7 @@ "kind": "section" }, { - "id": "678-zero-cache-config#websocket-compression-options", + "id": "702-zero-cache-config#websocket-compression-options", "title": "zero-cache Config", "searchTitle": "Websocket Compression Options", "sectionTitle": "Websocket Compression Options", @@ -9206,7 +9546,7 @@ "kind": "section" }, { - "id": "679-zero-cache-config#websocket-max-payload-bytes", + "id": "703-zero-cache-config#websocket-max-payload-bytes", "title": "zero-cache Config", "searchTitle": "Websocket Max Payload Bytes", "sectionTitle": "Websocket Max Payload Bytes", @@ -9216,7 +9556,7 @@ "kind": "section" }, { - "id": "680-zero-cache-config#yield-threshold-ms", + "id": "704-zero-cache-config#yield-threshold-ms", "title": "zero-cache Config", "searchTitle": "Yield Threshold (ms)", "sectionTitle": "Yield Threshold (ms)", @@ -9226,17 +9566,17 @@ "kind": "section" }, { - "id": "681-zero-cache-config#deprecated-flags", + "id": "705-zero-cache-config#deprecated-flags", "title": "zero-cache Config", "searchTitle": "Deprecated Flags", "sectionTitle": "Deprecated Flags", "sectionId": "deprecated-flags", "url": "/docs/zero-cache-config", - "content": "Change Max Connections ZERO_CHANGE_MAX_CONNS is deprecated and no longer controls change database connection usage. Size the database or pooler as described under Change DB. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS Auth JWK A public key in JWK format used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwk env: ZERO_AUTH_JWK Auth JWKS URL A URL that returns a JWK set used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwks-url env: ZERO_AUTH_JWKS_URL Auth Secret A symmetric key used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-secret env: ZERO_AUTH_SECRET", + "content": "Change Max Connections ZERO_CHANGE_MAX_CONNS is deprecated and no longer controls change database connection usage. Size the database or pooler as described under Change DB. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS Change Streamer Mode ZERO_CHANGE_STREAMER_MODE is deprecated. Set ZERO_CHANGE_STREAMER_URI on view-syncers instead. The mode for running or connecting to the change-streamer: dedicated: runs the change-streamer and shuts down when another change-streamer takes over the replication slot. This is appropriate in a single-node configuration, or for the replication-manager in a multi-node configuration. discover: connects to the change-streamer as internally advertised in the change-db. This is appropriate for the view-syncers in a multi-node setup. This may not work in all networking configurations (e.g., some private networking or port forwarding setups). This option is ignored if ZERO_CHANGE_STREAMER_URI is set. flag: --change-streamer-mode env: ZERO_CHANGE_STREAMER_MODE default: dedicated Litestream Minimum Checkpoint Page Count ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT is deprecated. Use ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB instead. The WAL page count at which SQLite attempts a PASSIVE checkpoint. If unset, it is derived from the checkpoint threshold as checkpointThresholdMB * 256 (4 KiB pages). flag: --litestream-min-checkpoint-page-count env: ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT default: checkpointThresholdMB * 256 Auth JWK A public key in JWK format used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwk env: ZERO_AUTH_JWK Auth JWKS URL A URL that returns a JWK set used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwks-url env: ZERO_AUTH_JWKS_URL Auth Secret A symmetric key used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-secret env: ZERO_AUTH_SECRET", "kind": "section" }, { - "id": "682-zero-cache-config#change-max-connections", + "id": "706-zero-cache-config#change-max-connections", "title": "zero-cache Config", "searchTitle": "Change Max Connections", "sectionTitle": "Change Max Connections", @@ -9246,7 +9586,27 @@ "kind": "section" }, { - "id": "683-zero-cache-config#auth-jwk", + "id": "707-zero-cache-config#change-streamer-mode", + "title": "zero-cache Config", + "searchTitle": "Change Streamer Mode", + "sectionTitle": "Change Streamer Mode", + "sectionId": "change-streamer-mode", + "url": "/docs/zero-cache-config", + "content": "ZERO_CHANGE_STREAMER_MODE is deprecated. Set ZERO_CHANGE_STREAMER_URI on view-syncers instead. The mode for running or connecting to the change-streamer: dedicated: runs the change-streamer and shuts down when another change-streamer takes over the replication slot. This is appropriate in a single-node configuration, or for the replication-manager in a multi-node configuration. discover: connects to the change-streamer as internally advertised in the change-db. This is appropriate for the view-syncers in a multi-node setup. This may not work in all networking configurations (e.g., some private networking or port forwarding setups). This option is ignored if ZERO_CHANGE_STREAMER_URI is set. flag: --change-streamer-mode env: ZERO_CHANGE_STREAMER_MODE default: dedicated", + "kind": "section" + }, + { + "id": "708-zero-cache-config#litestream-minimum-checkpoint-page-count", + "title": "zero-cache Config", + "searchTitle": "Litestream Minimum Checkpoint Page Count", + "sectionTitle": "Litestream Minimum Checkpoint Page Count", + "sectionId": "litestream-minimum-checkpoint-page-count", + "url": "/docs/zero-cache-config", + "content": "ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT is deprecated. Use ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB instead. The WAL page count at which SQLite attempts a PASSIVE checkpoint. If unset, it is derived from the checkpoint threshold as checkpointThresholdMB * 256 (4 KiB pages). flag: --litestream-min-checkpoint-page-count env: ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT default: checkpointThresholdMB * 256", + "kind": "section" + }, + { + "id": "709-zero-cache-config#auth-jwk", "title": "zero-cache Config", "searchTitle": "Auth JWK", "sectionTitle": "Auth JWK", @@ -9256,7 +9616,7 @@ "kind": "section" }, { - "id": "684-zero-cache-config#auth-jwks-url", + "id": "710-zero-cache-config#auth-jwks-url", "title": "zero-cache Config", "searchTitle": "Auth JWKS URL", "sectionTitle": "Auth JWKS URL", @@ -9266,7 +9626,7 @@ "kind": "section" }, { - "id": "685-zero-cache-config#auth-secret", + "id": "711-zero-cache-config#auth-secret", "title": "zero-cache Config", "searchTitle": "Auth Secret", "sectionTitle": "Auth Secret", @@ -9276,7 +9636,7 @@ "kind": "section" }, { - "id": "77-zql", + "id": "78-zql", "title": "ZQL", "searchTitle": "ZQL", "url": "/docs/zql", @@ -9386,7 +9746,7 @@ "kind": "page" }, { - "id": "686-zql#create-a-builder", + "id": "712-zql#create-a-builder", "title": "ZQL", "searchTitle": "Create a Builder", "sectionTitle": "Create a Builder", @@ -9396,7 +9756,7 @@ "kind": "section" }, { - "id": "687-zql#select", + "id": "713-zql#select", "title": "ZQL", "searchTitle": "Select", "sectionTitle": "Select", @@ -9406,7 +9766,7 @@ "kind": "section" }, { - "id": "688-zql#ordering", + "id": "714-zql#ordering", "title": "ZQL", "searchTitle": "Ordering", "sectionTitle": "Ordering", @@ -9416,7 +9776,7 @@ "kind": "section" }, { - "id": "689-zql#limit", + "id": "715-zql#limit", "title": "ZQL", "searchTitle": "Limit", "sectionTitle": "Limit", @@ -9426,7 +9786,7 @@ "kind": "section" }, { - "id": "690-zql#paging", + "id": "716-zql#paging", "title": "ZQL", "searchTitle": "Paging", "sectionTitle": "Paging", @@ -9436,7 +9796,7 @@ "kind": "section" }, { - "id": "691-zql#getting-a-single-result", + "id": "717-zql#getting-a-single-result", "title": "ZQL", "searchTitle": "Getting a Single Result", "sectionTitle": "Getting a Single Result", @@ -9446,7 +9806,7 @@ "kind": "section" }, { - "id": "692-zql#relationships", + "id": "718-zql#relationships", "title": "ZQL", "searchTitle": "Relationships", "sectionTitle": "Relationships", @@ -9456,7 +9816,7 @@ "kind": "section" }, { - "id": "693-zql#refining-relationships", + "id": "719-zql#refining-relationships", "title": "ZQL", "searchTitle": "Refining Relationships", "sectionTitle": "Refining Relationships", @@ -9466,7 +9826,7 @@ "kind": "section" }, { - "id": "694-zql#nested-relationships", + "id": "720-zql#nested-relationships", "title": "ZQL", "searchTitle": "Nested Relationships", "sectionTitle": "Nested Relationships", @@ -9476,7 +9836,7 @@ "kind": "section" }, { - "id": "695-zql#where", + "id": "721-zql#where", "title": "ZQL", "searchTitle": "Where", "sectionTitle": "Where", @@ -9486,7 +9846,7 @@ "kind": "section" }, { - "id": "696-zql#comparison-operators", + "id": "722-zql#comparison-operators", "title": "ZQL", "searchTitle": "Comparison Operators", "sectionTitle": "Comparison Operators", @@ -9496,7 +9856,7 @@ "kind": "section" }, { - "id": "697-zql#equals-is-the-default-comparison-operator", + "id": "723-zql#equals-is-the-default-comparison-operator", "title": "ZQL", "searchTitle": "Equals is the Default Comparison Operator", "sectionTitle": "Equals is the Default Comparison Operator", @@ -9506,7 +9866,7 @@ "kind": "section" }, { - "id": "698-zql#comparing-to-null", + "id": "724-zql#comparing-to-null", "title": "ZQL", "searchTitle": "Comparing to null", "sectionTitle": "Comparing to null", @@ -9516,7 +9876,7 @@ "kind": "section" }, { - "id": "699-zql#comparing-to-undefined", + "id": "725-zql#comparing-to-undefined", "title": "ZQL", "searchTitle": "Comparing to undefined", "sectionTitle": "Comparing to undefined", @@ -9526,7 +9886,7 @@ "kind": "section" }, { - "id": "700-zql#compound-filters", + "id": "726-zql#compound-filters", "title": "ZQL", "searchTitle": "Compound Filters", "sectionTitle": "Compound Filters", @@ -9536,7 +9896,7 @@ "kind": "section" }, { - "id": "701-zql#comparing-literal-values", + "id": "727-zql#comparing-literal-values", "title": "ZQL", "searchTitle": "Comparing Literal Values", "sectionTitle": "Comparing Literal Values", @@ -9546,7 +9906,7 @@ "kind": "section" }, { - "id": "702-zql#relationship-filters", + "id": "728-zql#relationship-filters", "title": "ZQL", "searchTitle": "Relationship Filters", "sectionTitle": "Relationship Filters", @@ -9556,7 +9916,7 @@ "kind": "section" }, { - "id": "703-zql#type-helpers", + "id": "729-zql#type-helpers", "title": "ZQL", "searchTitle": "Type Helpers", "sectionTitle": "Type Helpers", @@ -9566,7 +9926,7 @@ "kind": "section" }, { - "id": "704-zql#planning", + "id": "730-zql#planning", "title": "ZQL", "searchTitle": "Planning", "sectionTitle": "Planning", @@ -9576,7 +9936,7 @@ "kind": "section" }, { - "id": "705-zql#inspecting-query-plans", + "id": "731-zql#inspecting-query-plans", "title": "ZQL", "searchTitle": "Inspecting Query Plans", "sectionTitle": "Inspecting Query Plans", @@ -9586,7 +9946,7 @@ "kind": "section" }, { - "id": "706-zql#manually-flipping-joins", + "id": "732-zql#manually-flipping-joins", "title": "ZQL", "searchTitle": "Manually Flipping Joins", "sectionTitle": "Manually Flipping Joins", @@ -9596,7 +9956,7 @@ "kind": "section" }, { - "id": "707-zql#scalar-subqueries", + "id": "733-zql#scalar-subqueries", "title": "ZQL", "searchTitle": "Scalar Subqueries", "sectionTitle": "Scalar Subqueries", @@ -9606,7 +9966,7 @@ "kind": "section" }, { - "id": "708-zql#why-it-matters", + "id": "734-zql#why-it-matters", "title": "ZQL", "searchTitle": "Why It Matters", "sectionTitle": "Why It Matters", @@ -9616,7 +9976,7 @@ "kind": "section" }, { - "id": "709-zql#trade-offs", + "id": "735-zql#trade-offs", "title": "ZQL", "searchTitle": "Trade-offs", "sectionTitle": "Trade-offs", @@ -9626,7 +9986,7 @@ "kind": "section" }, { - "id": "710-zql#future-work", + "id": "736-zql#future-work", "title": "ZQL", "searchTitle": "Future Work", "sectionTitle": "Future Work", From e7c50176f2288fedc1832e814de1a855fb0ec5fd Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Wed, 14 Oct 2026 19:02:23 +0000 Subject: [PATCH 09/20] docs(release-notes): add benchmark results to the 1.11 Performance section Leads with client cold start, adds the React Native query engine results, and mentions cold start on React Native. --- contents/docs/release-notes/1.11.mdx | 72 +++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/contents/docs/release-notes/1.11.mdx b/contents/docs/release-notes/1.11.mdx index c32fcff9..a08be92e 100644 --- a/contents/docs/release-notes/1.11.mdx +++ b/contents/docs/release-notes/1.11.mdx @@ -41,6 +41,31 @@ Zero 1.11 shows cached query results sooner, recovers from client storage failur ## Performance +### Client Cold Start + +Apps usually register their queries before Zero has loaded local data, for example when `useQuery` runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once ([#6472](https://github.com/rocicorp/mono/pull/6472)). With 13 queries and about 30,000 local rows, startup is about **7x faster**, and about **8x faster** on React Native. + + + ### Server-Side Incremental Updates When a transaction replicates, `zero-cache` brings every client group's queries up to date with it. Zero 1.11 does this about **2.5x faster**: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU. @@ -72,34 +97,59 @@ When a transaction replicates, `zero-cache` brings every client group's queries ]} /> -### Client Cold Start +### React Native -Apps usually register their queries before Zero has loaded local data, for example when `useQuery` runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once ([#6472](https://github.com/rocicorp/mono/pull/6472)). With 13 queries and about 30,000 local rows, startup is about **7x faster**. +On React Native, Zero's query engine is about **1.6x faster** in Zero 1.11. Across 56 query engine benchmarks on Hermes, the geometric mean speedup is 1.63x and 48 benchmarks are more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. ### Building and Hashing Queries -Components build their queries on every render, and Zero hashes each query to identify it. Building the same query now returns the same object with its hash already computed ([#6469](https://github.com/rocicorp/mono/pull/6469)), so building and hashing a query is about **7x to 17x faster**, depending on its size. +Components build their queries on every render, and Zero hashes each query to identify it. Building the same query again usually returns the same object, with its hash already computed ([#6469](https://github.com/rocicorp/mono/pull/6469)), so building and hashing a query is about **7x to 17x faster**, depending on its size. -{/* TODO: React Native benchmark results. */} - ### Slower in Zero 1.11: Client Writes With Many Joined Queries One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about **1.2x longer** in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped ([#6645](https://github.com/rocicorp/mono/pull/6645), [#6708](https://github.com/rocicorp/mono/pull/6708)). When every change reaches some parent, as here, keeping that index up to date is extra work. From bb84573e8e7af0a2707ea84f942977e781477733 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Wed, 14 Oct 2026 19:02:27 +0000 Subject: [PATCH 10/20] chore(search): regenerate the search index --- assets/search-index.json | 606 ++++++++++++++++++++------------------- 1 file changed, 310 insertions(+), 296 deletions(-) diff --git a/assets/search-index.json b/assets/search-index.json index a8133e29..8600e506 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -5687,7 +5687,7 @@ "title": "Zero 1.11", "searchTitle": "Zero 1.11", "url": "/docs/release-notes/1.11", - "content": "Installation npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Overview Zero 1.11 shows cached query results sooner, recovers from client storage failures, and makes Litestream v5 the default for backups. It also speeds up queries on the client and server and fixes a long list of correctness and reliability issues. Features Queries whose results a previous session stored locally now start with the new 'cached' result type, so apps can show stored data right away and still tell it apart from data confirmed by the server. (#6578) (thanks @pawarren!) Building the same query again usually returns the same Query object, so useQuery no longer rebuilds and rehashes its query on every render, and queries built inline can be used as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero now starts in a new 'initializing' connection state while it loads local data. Slow local loading no longer counts against the connect timeout. (#6561) When local storage fails to open, fills up, or reports I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore reports which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Upgrading from 1.10 keeps each client's local data and its pending mutations. Future protocol changes no longer start clients over with an empty local database. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when they fail with an error that tx.retryOn() accepts, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. Replicas restored from another replica's backup continue it incrementally, periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits until replication has caught up before reporting ready. (1c5ddcdb4) Inactive client groups are now kept for 7 days instead of 48 hours, so clients that return within a week avoid a full resync. (#6484) New tools for diagnosing slow queries: zero-cache logs which queries are slow and why, with index suggestions, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) Opt-in limits protect zero-cache from expensive queries: a hydration budget for reconnecting clients and a per-query hydration timeout. (#6477, #6499) New OpenTelemetry metrics for WAL usage, index-build progress in replication status events, and restore progress in a new view-syncer status event. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image shrank from about 1.09 GB to 807 MB. (#6695) Performance Server-Side Incremental Updates When a transaction replicates, zero-cache brings every client group's queries up to date with it. Zero 1.11 does this about 2.5x faster: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU. Client Cold Start Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster. Building and Hashing Queries Components build their queries on every render, and Zero hashes each query to identify it. Building the same query now returns the same object with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size. Slower in Zero 1.11: Client Writes With Many Joined Queries One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped (#6645, #6708). When every change reaches some parent, as here, keeping that index up to date is extra work. Fixes Query Results Queries using or with a flipped exists no longer return wrong rows. (#6636) Joins no longer leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery are now synced, and changes to them propagate. (913c0665f) Catching up a client after a large update no longer fails with Missing row, and a schema change during rehydration no longer fails with table has no column named. (#6662, #6616) Queries whose SQLite plan nests one multi-index OR inside another no longer crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache no longer calls ZERO_QUERY_URL again for unchanged custom queries when it rebuilds its query pipelines. Permission changes still re-transform every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments no longer share one view. A SolidJS view rebuilt after its query changes uses the current TTL. (#6461, #6497) In mutators, a row just inserted without its optional columns now reads back with those columns as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) no longer return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients no longer log ChunkNotFoundError during persist, and a write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database now deletes the right file. (#6656) (thanks @pawarren!) Clients no longer go offline after the app resumes from the background or the computer wakes from sleep, and React Native connect timeouts retry instead of leaving the client stuck in an error state. (#6510, #6485) (thanks @pawarren!) Slow local startup no longer makes a healthy server look unreachable. (#6475) A mutator that throws while being replayed no longer disconnects the client in a loop. The mutation is still sent to the server. (#6555) Client memory no longer grows with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing are logged as warnings or info instead of errors, and fatal errors now include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability Fixed memory and resource leaks in zero-cache around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) Fixed view-syncer crashes and Internal errors when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg no longer crash when Postgres closes a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector no longer runs a view-syncer out of memory on large tables. (#6596) Error messages no longer include row values, query literals or mutation arguments. (#6413) Replication, Backfills, and Backups Replication no longer rolls back or stops after an interrupted stream, and a replication-manager whose slot was invalidated now resyncs instead of crash-looping. (#6562, #6590) Backfills no longer apply rows before replication has caught up to them, resume when a view-syncer moves to another replication-manager, and no longer leak a Postgres connection that holds back vacuum. (#6685, #6684, #6513) Replication slots are more robust: creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag is now measured correctly on Aurora. (e6a2590db) After initial sync, the replication-manager waits for a backup that covers its replica before serving, so restoring view-syncers no longer fall back to Postgres. (#6465) Fewer unnecessary query pipeline resets, and dropping an upstream index now refreshes table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager keeps serving until the new one takes over. (21f349135) Change-database connections no longer leak after a failed catch-up, and a change-database reset no longer hangs. (#6529, #6632) Litestream's upload concurrency now scales with available memory, avoiding out-of-memory crashes in small containers. (240c05730) Configuration values that start with a dash, such as some shard IDs, are now parsed correctly. (81e6ac570) In the zero Docker image, initial sync and backfills no longer leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe) Breaking Changes Legacy queries: zero-cache no longer accepts queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: code that switches over every result type or connection state must handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options, which no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours instead of 48, replication lag is reported every 10 seconds instead of 30, and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 instead of 2. Startup can take longer while zero-cache waits for a backup and for replication to catch up; give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: mutator return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates for tx.retryOn() to work. Using tx after its transaction ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL no longer apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they had not yet sent are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Mutations that were not yet sent are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after an upgrade, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE; use ZERO_CHANGE_STREAMER_URI. (698286b55) Events: a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", + "content": "Installation npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Overview Zero 1.11 shows cached query results sooner, recovers from client storage failures, and makes Litestream v5 the default for backups. It also speeds up queries on the client and server and fixes a long list of correctness and reliability issues. Features Queries whose results a previous session stored locally now start with the new 'cached' result type, so apps can show stored data right away and still tell it apart from data confirmed by the server. (#6578) (thanks @pawarren!) Building the same query again usually returns the same Query object, so useQuery no longer rebuilds and rehashes its query on every render, and queries built inline can be used as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero now starts in a new 'initializing' connection state while it loads local data. Slow local loading no longer counts against the connect timeout. (#6561) When local storage fails to open, fills up, or reports I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore reports which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Upgrading from 1.10 keeps each client's local data and its pending mutations. Future protocol changes no longer start clients over with an empty local database. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when they fail with an error that tx.retryOn() accepts, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. Replicas restored from another replica's backup continue it incrementally, periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits until replication has caught up before reporting ready. (1c5ddcdb4) Inactive client groups are now kept for 7 days instead of 48 hours, so clients that return within a week avoid a full resync. (#6484) New tools for diagnosing slow queries: zero-cache logs which queries are slow and why, with index suggestions, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) Opt-in limits protect zero-cache from expensive queries: a hydration budget for reconnecting clients and a per-query hydration timeout. (#6477, #6499) New OpenTelemetry metrics for WAL usage, index-build progress in replication status events, and restore progress in a new view-syncer status event. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image shrank from about 1.09 GB to 807 MB. (#6695) Performance Client Cold Start Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache brings every client group's queries up to date with it. Zero 1.11 does this about 2.5x faster: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU. React Native On React Native, Zero's query engine is about 1.6x faster in Zero 1.11. Across 56 query engine benchmarks on Hermes, the geometric mean speedup is 1.63x and 48 benchmarks are more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components build their queries on every render, and Zero hashes each query to identify it. Building the same query again usually returns the same object, with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size. Slower in Zero 1.11: Client Writes With Many Joined Queries One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped (#6645, #6708). When every change reaches some parent, as here, keeping that index up to date is extra work. Fixes Query Results Queries using or with a flipped exists no longer return wrong rows. (#6636) Joins no longer leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery are now synced, and changes to them propagate. (913c0665f) Catching up a client after a large update no longer fails with Missing row, and a schema change during rehydration no longer fails with table has no column named. (#6662, #6616) Queries whose SQLite plan nests one multi-index OR inside another no longer crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache no longer calls ZERO_QUERY_URL again for unchanged custom queries when it rebuilds its query pipelines. Permission changes still re-transform every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments no longer share one view. A SolidJS view rebuilt after its query changes uses the current TTL. (#6461, #6497) In mutators, a row just inserted without its optional columns now reads back with those columns as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) no longer return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients no longer log ChunkNotFoundError during persist, and a write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database now deletes the right file. (#6656) (thanks @pawarren!) Clients no longer go offline after the app resumes from the background or the computer wakes from sleep, and React Native connect timeouts retry instead of leaving the client stuck in an error state. (#6510, #6485) (thanks @pawarren!) Slow local startup no longer makes a healthy server look unreachable. (#6475) A mutator that throws while being replayed no longer disconnects the client in a loop. The mutation is still sent to the server. (#6555) Client memory no longer grows with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing are logged as warnings or info instead of errors, and fatal errors now include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability Fixed memory and resource leaks in zero-cache around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) Fixed view-syncer crashes and Internal errors when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg no longer crash when Postgres closes a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector no longer runs a view-syncer out of memory on large tables. (#6596) Error messages no longer include row values, query literals or mutation arguments. (#6413) Replication, Backfills, and Backups Replication no longer rolls back or stops after an interrupted stream, and a replication-manager whose slot was invalidated now resyncs instead of crash-looping. (#6562, #6590) Backfills no longer apply rows before replication has caught up to them, resume when a view-syncer moves to another replication-manager, and no longer leak a Postgres connection that holds back vacuum. (#6685, #6684, #6513) Replication slots are more robust: creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag is now measured correctly on Aurora. (e6a2590db) After initial sync, the replication-manager waits for a backup that covers its replica before serving, so restoring view-syncers no longer fall back to Postgres. (#6465) Fewer unnecessary query pipeline resets, and dropping an upstream index now refreshes table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager keeps serving until the new one takes over. (21f349135) Change-database connections no longer leak after a failed catch-up, and a change-database reset no longer hangs. (#6529, #6632) Litestream's upload concurrency now scales with available memory, avoiding out-of-memory crashes in small containers. (240c05730) Configuration values that start with a dash, such as some shard IDs, are now parsed correctly. (81e6ac570) In the zero Docker image, initial sync and backfills no longer leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe) Breaking Changes Legacy queries: zero-cache no longer accepts queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: code that switches over every result type or connection state must handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options, which no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours instead of 48, replication lag is reported every 10 seconds instead of 30, and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 instead of 2. Startup can take longer while zero-cache waits for a backup and for replication to catch up; give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: mutator return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates for tx.retryOn() to work. Using tx after its transaction ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL no longer apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they had not yet sent are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Mutations that were not yet sent are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after an upgrade, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE; use ZERO_CHANGE_STREAMER_URI. (698286b55) Events: a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", "headings": [ { "text": "Installation", @@ -5705,13 +5705,17 @@ "text": "Performance", "id": "performance" }, + { + "text": "Client Cold Start", + "id": "client-cold-start" + }, { "text": "Server-Side Incremental Updates", "id": "server-side-incremental-updates" }, { - "text": "Client Cold Start", - "id": "client-cold-start" + "text": "React Native", + "id": "react-native" }, { "text": "Building and Hashing Queries", @@ -5789,11 +5793,21 @@ "sectionTitle": "Performance", "sectionId": "performance", "url": "/docs/release-notes/1.11", - "content": "Server-Side Incremental Updates When a transaction replicates, zero-cache brings every client group's queries up to date with it. Zero 1.11 does this about 2.5x faster: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU. Client Cold Start Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster. Building and Hashing Queries Components build their queries on every render, and Zero hashes each query to identify it. Building the same query now returns the same object with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size. Slower in Zero 1.11: Client Writes With Many Joined Queries One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped (#6645, #6708). When every change reaches some parent, as here, keeping that index up to date is extra work.", + "content": "Client Cold Start Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache brings every client group's queries up to date with it. Zero 1.11 does this about 2.5x faster: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU. React Native On React Native, Zero's query engine is about 1.6x faster in Zero 1.11. Across 56 query engine benchmarks on Hermes, the geometric mean speedup is 1.63x and 48 benchmarks are more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components build their queries on every render, and Zero hashes each query to identify it. Building the same query again usually returns the same object, with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size. Slower in Zero 1.11: Client Writes With Many Joined Queries One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped (#6645, #6708). When every change reaches some parent, as here, keeping that index up to date is extra work.", + "kind": "section" + }, + { + "id": "450-release-notes/1.11#client-cold-start", + "title": "Zero 1.11", + "searchTitle": "Client Cold Start", + "sectionTitle": "Client Cold Start", + "sectionId": "client-cold-start", + "url": "/docs/release-notes/1.11", + "content": "Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native.", "kind": "section" }, { - "id": "450-release-notes/1.11#server-side-incremental-updates", + "id": "451-release-notes/1.11#server-side-incremental-updates", "title": "Zero 1.11", "searchTitle": "Server-Side Incremental Updates", "sectionTitle": "Server-Side Incremental Updates", @@ -5803,27 +5817,27 @@ "kind": "section" }, { - "id": "451-release-notes/1.11#client-cold-start", + "id": "452-release-notes/1.11#react-native", "title": "Zero 1.11", - "searchTitle": "Client Cold Start", - "sectionTitle": "Client Cold Start", - "sectionId": "client-cold-start", + "searchTitle": "React Native", + "sectionTitle": "React Native", + "sectionId": "react-native", "url": "/docs/release-notes/1.11", - "content": "Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster.", + "content": "On React Native, Zero's query engine is about 1.6x faster in Zero 1.11. Across 56 query engine benchmarks on Hermes, the geometric mean speedup is 1.63x and 48 benchmarks are more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster.", "kind": "section" }, { - "id": "452-release-notes/1.11#building-and-hashing-queries", + "id": "453-release-notes/1.11#building-and-hashing-queries", "title": "Zero 1.11", "searchTitle": "Building and Hashing Queries", "sectionTitle": "Building and Hashing Queries", "sectionId": "building-and-hashing-queries", "url": "/docs/release-notes/1.11", - "content": "Components build their queries on every render, and Zero hashes each query to identify it. Building the same query now returns the same object with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size.", + "content": "Components build their queries on every render, and Zero hashes each query to identify it. Building the same query again usually returns the same object, with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size.", "kind": "section" }, { - "id": "453-release-notes/1.11#slower-in-zero-111-client-writes-with-many-joined-queries", + "id": "454-release-notes/1.11#slower-in-zero-111-client-writes-with-many-joined-queries", "title": "Zero 1.11", "searchTitle": "Slower in Zero 1.11: Client Writes With Many Joined Queries", "sectionTitle": "Slower in Zero 1.11: Client Writes With Many Joined Queries", @@ -5833,7 +5847,7 @@ "kind": "section" }, { - "id": "454-release-notes/1.11#fixes", + "id": "455-release-notes/1.11#fixes", "title": "Zero 1.11", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5843,7 +5857,7 @@ "kind": "section" }, { - "id": "455-release-notes/1.11#query-results", + "id": "456-release-notes/1.11#query-results", "title": "Zero 1.11", "searchTitle": "Query Results", "sectionTitle": "Query Results", @@ -5853,7 +5867,7 @@ "kind": "section" }, { - "id": "456-release-notes/1.11#clients-and-react-native", + "id": "457-release-notes/1.11#clients-and-react-native", "title": "Zero 1.11", "searchTitle": "Clients and React Native", "sectionTitle": "Clients and React Native", @@ -5863,7 +5877,7 @@ "kind": "section" }, { - "id": "457-release-notes/1.11#zero-cache-reliability", + "id": "458-release-notes/1.11#zero-cache-reliability", "title": "Zero 1.11", "searchTitle": "zero-cache Reliability", "sectionTitle": "zero-cache Reliability", @@ -5873,7 +5887,7 @@ "kind": "section" }, { - "id": "458-release-notes/1.11#replication-backfills-and-backups", + "id": "459-release-notes/1.11#replication-backfills-and-backups", "title": "Zero 1.11", "searchTitle": "Replication, Backfills, and Backups", "sectionTitle": "Replication, Backfills, and Backups", @@ -5883,7 +5897,7 @@ "kind": "section" }, { - "id": "459-release-notes/1.11#type-checking", + "id": "460-release-notes/1.11#type-checking", "title": "Zero 1.11", "searchTitle": "Type Checking", "sectionTitle": "Type Checking", @@ -5893,7 +5907,7 @@ "kind": "section" }, { - "id": "460-release-notes/1.11#breaking-changes", + "id": "461-release-notes/1.11#breaking-changes", "title": "Zero 1.11", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5933,7 +5947,7 @@ "kind": "page" }, { - "id": "461-release-notes/1.2#installation", + "id": "462-release-notes/1.2#installation", "title": "Zero 1.2", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5943,7 +5957,7 @@ "kind": "section" }, { - "id": "462-release-notes/1.2#features", + "id": "463-release-notes/1.2#features", "title": "Zero 1.2", "searchTitle": "Features", "sectionTitle": "Features", @@ -5953,7 +5967,7 @@ "kind": "section" }, { - "id": "463-release-notes/1.2#performance", + "id": "464-release-notes/1.2#performance", "title": "Zero 1.2", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -5963,7 +5977,7 @@ "kind": "section" }, { - "id": "464-release-notes/1.2#fixes", + "id": "465-release-notes/1.2#fixes", "title": "Zero 1.2", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5973,7 +5987,7 @@ "kind": "section" }, { - "id": "465-release-notes/1.2#breaking-changes", + "id": "466-release-notes/1.2#breaking-changes", "title": "Zero 1.2", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6013,7 +6027,7 @@ "kind": "page" }, { - "id": "466-release-notes/1.3#installation", + "id": "467-release-notes/1.3#installation", "title": "Zero 1.3", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6023,7 +6037,7 @@ "kind": "section" }, { - "id": "467-release-notes/1.3#features", + "id": "468-release-notes/1.3#features", "title": "Zero 1.3", "searchTitle": "Features", "sectionTitle": "Features", @@ -6033,7 +6047,7 @@ "kind": "section" }, { - "id": "468-release-notes/1.3#performance", + "id": "469-release-notes/1.3#performance", "title": "Zero 1.3", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6043,7 +6057,7 @@ "kind": "section" }, { - "id": "469-release-notes/1.3#fixes", + "id": "470-release-notes/1.3#fixes", "title": "Zero 1.3", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6053,7 +6067,7 @@ "kind": "section" }, { - "id": "470-release-notes/1.3#breaking-changes", + "id": "471-release-notes/1.3#breaking-changes", "title": "Zero 1.3", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6101,7 +6115,7 @@ "kind": "page" }, { - "id": "471-release-notes/1.4#installation", + "id": "472-release-notes/1.4#installation", "title": "Zero 1.4", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6111,7 +6125,7 @@ "kind": "section" }, { - "id": "472-release-notes/1.4#upgrading", + "id": "473-release-notes/1.4#upgrading", "title": "Zero 1.4", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -6121,7 +6135,7 @@ "kind": "section" }, { - "id": "473-release-notes/1.4#userid-anon", + "id": "474-release-notes/1.4#userid-anon", "title": "Zero 1.4", "searchTitle": "userID: \"anon\"", "sectionTitle": "userID: \"anon\"", @@ -6131,7 +6145,7 @@ "kind": "section" }, { - "id": "474-release-notes/1.4#features", + "id": "475-release-notes/1.4#features", "title": "Zero 1.4", "searchTitle": "Features", "sectionTitle": "Features", @@ -6141,7 +6155,7 @@ "kind": "section" }, { - "id": "475-release-notes/1.4#performance", + "id": "476-release-notes/1.4#performance", "title": "Zero 1.4", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6151,7 +6165,7 @@ "kind": "section" }, { - "id": "476-release-notes/1.4#fixes", + "id": "477-release-notes/1.4#fixes", "title": "Zero 1.4", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6161,7 +6175,7 @@ "kind": "section" }, { - "id": "477-release-notes/1.4#breaking-changes", + "id": "478-release-notes/1.4#breaking-changes", "title": "Zero 1.4", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6213,7 +6227,7 @@ "kind": "page" }, { - "id": "478-release-notes/1.5#installation", + "id": "479-release-notes/1.5#installation", "title": "Zero 1.5", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6223,7 +6237,7 @@ "kind": "section" }, { - "id": "479-release-notes/1.5#upgrading", + "id": "480-release-notes/1.5#upgrading", "title": "Zero 1.5", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -6233,7 +6247,7 @@ "kind": "section" }, { - "id": "480-release-notes/1.5#authenticated-client-groups", + "id": "481-release-notes/1.5#authenticated-client-groups", "title": "Zero 1.5", "searchTitle": "Authenticated Client Groups", "sectionTitle": "Authenticated Client Groups", @@ -6243,7 +6257,7 @@ "kind": "section" }, { - "id": "481-release-notes/1.5#deploy-order", + "id": "482-release-notes/1.5#deploy-order", "title": "Zero 1.5", "searchTitle": "Deploy Order", "sectionTitle": "Deploy Order", @@ -6253,7 +6267,7 @@ "kind": "section" }, { - "id": "482-release-notes/1.5#features", + "id": "483-release-notes/1.5#features", "title": "Zero 1.5", "searchTitle": "Features", "sectionTitle": "Features", @@ -6263,7 +6277,7 @@ "kind": "section" }, { - "id": "483-release-notes/1.5#performance", + "id": "484-release-notes/1.5#performance", "title": "Zero 1.5", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6273,7 +6287,7 @@ "kind": "section" }, { - "id": "484-release-notes/1.5#fixes", + "id": "485-release-notes/1.5#fixes", "title": "Zero 1.5", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6283,7 +6297,7 @@ "kind": "section" }, { - "id": "485-release-notes/1.5#breaking-changes", + "id": "486-release-notes/1.5#breaking-changes", "title": "Zero 1.5", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6331,7 +6345,7 @@ "kind": "page" }, { - "id": "486-release-notes/1.6#installation", + "id": "487-release-notes/1.6#installation", "title": "Zero 1.6", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6341,7 +6355,7 @@ "kind": "section" }, { - "id": "487-release-notes/1.6#upgrading", + "id": "488-release-notes/1.6#upgrading", "title": "Zero 1.6", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -6351,7 +6365,7 @@ "kind": "section" }, { - "id": "488-release-notes/1.6#planetscale-failover", + "id": "489-release-notes/1.6#planetscale-failover", "title": "Zero 1.6", "searchTitle": "PlanetScale Failover", "sectionTitle": "PlanetScale Failover", @@ -6361,7 +6375,7 @@ "kind": "section" }, { - "id": "489-release-notes/1.6#features", + "id": "490-release-notes/1.6#features", "title": "Zero 1.6", "searchTitle": "Features", "sectionTitle": "Features", @@ -6371,7 +6385,7 @@ "kind": "section" }, { - "id": "490-release-notes/1.6#performance", + "id": "491-release-notes/1.6#performance", "title": "Zero 1.6", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6381,7 +6395,7 @@ "kind": "section" }, { - "id": "491-release-notes/1.6#fixes", + "id": "492-release-notes/1.6#fixes", "title": "Zero 1.6", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6391,7 +6405,7 @@ "kind": "section" }, { - "id": "492-release-notes/1.6#breaking-changes", + "id": "493-release-notes/1.6#breaking-changes", "title": "Zero 1.6", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6443,7 +6457,7 @@ "kind": "page" }, { - "id": "493-release-notes/1.7#installation", + "id": "494-release-notes/1.7#installation", "title": "Zero 1.7", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6453,7 +6467,7 @@ "kind": "section" }, { - "id": "494-release-notes/1.7#overview", + "id": "495-release-notes/1.7#overview", "title": "Zero 1.7", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6463,7 +6477,7 @@ "kind": "section" }, { - "id": "495-release-notes/1.7#features", + "id": "496-release-notes/1.7#features", "title": "Zero 1.7", "searchTitle": "Features", "sectionTitle": "Features", @@ -6473,7 +6487,7 @@ "kind": "section" }, { - "id": "496-release-notes/1.7#performance", + "id": "497-release-notes/1.7#performance", "title": "Zero 1.7", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6483,7 +6497,7 @@ "kind": "section" }, { - "id": "497-release-notes/1.7#replication", + "id": "498-release-notes/1.7#replication", "title": "Zero 1.7", "searchTitle": "Replication", "sectionTitle": "Replication", @@ -6493,7 +6507,7 @@ "kind": "section" }, { - "id": "498-release-notes/1.7#flipped-exists-queries", + "id": "499-release-notes/1.7#flipped-exists-queries", "title": "Zero 1.7", "searchTitle": "Flipped Exists Queries", "sectionTitle": "Flipped Exists Queries", @@ -6503,7 +6517,7 @@ "kind": "section" }, { - "id": "499-release-notes/1.7#fixes", + "id": "500-release-notes/1.7#fixes", "title": "Zero 1.7", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6513,7 +6527,7 @@ "kind": "section" }, { - "id": "500-release-notes/1.7#breaking-changes", + "id": "501-release-notes/1.7#breaking-changes", "title": "Zero 1.7", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6569,7 +6583,7 @@ "kind": "page" }, { - "id": "501-release-notes/1.8#installation", + "id": "502-release-notes/1.8#installation", "title": "Zero 1.8", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6579,7 +6593,7 @@ "kind": "section" }, { - "id": "502-release-notes/1.8#overview", + "id": "503-release-notes/1.8#overview", "title": "Zero 1.8", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6589,7 +6603,7 @@ "kind": "section" }, { - "id": "503-release-notes/1.8#features", + "id": "504-release-notes/1.8#features", "title": "Zero 1.8", "searchTitle": "Features", "sectionTitle": "Features", @@ -6599,7 +6613,7 @@ "kind": "section" }, { - "id": "504-release-notes/1.8#performance", + "id": "505-release-notes/1.8#performance", "title": "Zero 1.8", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6609,7 +6623,7 @@ "kind": "section" }, { - "id": "505-release-notes/1.8#replicating-large-transactions", + "id": "506-release-notes/1.8#replicating-large-transactions", "title": "Zero 1.8", "searchTitle": "Replicating Large Transactions", "sectionTitle": "Replicating Large Transactions", @@ -6619,7 +6633,7 @@ "kind": "section" }, { - "id": "506-release-notes/1.8#maintaining-limit-queries", + "id": "507-release-notes/1.8#maintaining-limit-queries", "title": "Zero 1.8", "searchTitle": "Maintaining limit() Queries", "sectionTitle": "Maintaining limit() Queries", @@ -6629,7 +6643,7 @@ "kind": "section" }, { - "id": "507-release-notes/1.8#client-side-hydration", + "id": "508-release-notes/1.8#client-side-hydration", "title": "Zero 1.8", "searchTitle": "Client-Side Hydration", "sectionTitle": "Client-Side Hydration", @@ -6639,7 +6653,7 @@ "kind": "section" }, { - "id": "508-release-notes/1.8#fixes", + "id": "509-release-notes/1.8#fixes", "title": "Zero 1.8", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6649,7 +6663,7 @@ "kind": "section" }, { - "id": "509-release-notes/1.8#breaking-changes", + "id": "510-release-notes/1.8#breaking-changes", "title": "Zero 1.8", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6693,7 +6707,7 @@ "kind": "page" }, { - "id": "510-release-notes/1.9#installation", + "id": "511-release-notes/1.9#installation", "title": "Zero 1.9", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6703,7 +6717,7 @@ "kind": "section" }, { - "id": "511-release-notes/1.9#overview", + "id": "512-release-notes/1.9#overview", "title": "Zero 1.9", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6713,7 +6727,7 @@ "kind": "section" }, { - "id": "512-release-notes/1.9#features", + "id": "513-release-notes/1.9#features", "title": "Zero 1.9", "searchTitle": "Features", "sectionTitle": "Features", @@ -6723,7 +6737,7 @@ "kind": "section" }, { - "id": "513-release-notes/1.9#performance", + "id": "514-release-notes/1.9#performance", "title": "Zero 1.9", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6733,7 +6747,7 @@ "kind": "section" }, { - "id": "514-release-notes/1.9#cold-mutation-latency", + "id": "515-release-notes/1.9#cold-mutation-latency", "title": "Zero 1.9", "searchTitle": "Cold Mutation Latency", "sectionTitle": "Cold Mutation Latency", @@ -6743,7 +6757,7 @@ "kind": "section" }, { - "id": "515-release-notes/1.9#fixes", + "id": "516-release-notes/1.9#fixes", "title": "Zero 1.9", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6780,7 +6794,7 @@ "kind": "page" }, { - "id": "516-reporting-bugs#zbugs", + "id": "517-reporting-bugs#zbugs", "title": "Reporting Bugs", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -6790,7 +6804,7 @@ "kind": "section" }, { - "id": "517-reporting-bugs#discord", + "id": "518-reporting-bugs#discord", "title": "Reporting Bugs", "searchTitle": "Discord", "sectionTitle": "Discord", @@ -6826,7 +6840,7 @@ "kind": "page" }, { - "id": "518-rest#pattern", + "id": "519-rest#pattern", "title": "REST", "searchTitle": "Pattern", "sectionTitle": "Pattern", @@ -6836,7 +6850,7 @@ "kind": "section" }, { - "id": "519-rest#tanstack-start-example", + "id": "520-rest#tanstack-start-example", "title": "REST", "searchTitle": "TanStack Start Example", "sectionTitle": "TanStack Start Example", @@ -6846,7 +6860,7 @@ "kind": "section" }, { - "id": "520-rest#openapi-generation", + "id": "521-rest#openapi-generation", "title": "REST", "searchTitle": "OpenAPI Generation", "sectionTitle": "OpenAPI Generation", @@ -6856,7 +6870,7 @@ "kind": "section" }, { - "id": "521-rest#full-working-example", + "id": "522-rest#full-working-example", "title": "REST", "searchTitle": "Full Working Example", "sectionTitle": "Full Working Example", @@ -6884,7 +6898,7 @@ "kind": "page" }, { - "id": "522-roadmap#q4-2025", + "id": "523-roadmap#q4-2025", "title": "Roadmap", "searchTitle": "Q4 2025", "sectionTitle": "Q4 2025", @@ -6894,7 +6908,7 @@ "kind": "section" }, { - "id": "523-roadmap#beyond", + "id": "524-roadmap#beyond", "title": "Roadmap", "searchTitle": "Beyond", "sectionTitle": "Beyond", @@ -6930,7 +6944,7 @@ "kind": "page" }, { - "id": "524-samples#gigabugs", + "id": "525-samples#gigabugs", "title": "Samples", "searchTitle": "Gigabugs", "sectionTitle": "Gigabugs", @@ -6940,7 +6954,7 @@ "kind": "section" }, { - "id": "525-samples#ztunes", + "id": "526-samples#ztunes", "title": "Samples", "searchTitle": "ztunes", "sectionTitle": "ztunes", @@ -6950,7 +6964,7 @@ "kind": "section" }, { - "id": "526-samples#zslack", + "id": "527-samples#zslack", "title": "Samples", "searchTitle": "zslack", "sectionTitle": "zslack", @@ -6960,7 +6974,7 @@ "kind": "section" }, { - "id": "527-samples#zero-music", + "id": "528-samples#zero-music", "title": "Samples", "searchTitle": "zero-music", "sectionTitle": "zero-music", @@ -7100,7 +7114,7 @@ "kind": "page" }, { - "id": "528-schema#generating-from-database", + "id": "529-schema#generating-from-database", "title": "Zero Schema", "searchTitle": "Generating from Database", "sectionTitle": "Generating from Database", @@ -7110,7 +7124,7 @@ "kind": "section" }, { - "id": "529-schema#writing-by-hand", + "id": "530-schema#writing-by-hand", "title": "Zero Schema", "searchTitle": "Writing by Hand", "sectionTitle": "Writing by Hand", @@ -7120,7 +7134,7 @@ "kind": "section" }, { - "id": "530-schema#table-schemas", + "id": "531-schema#table-schemas", "title": "Zero Schema", "searchTitle": "Table Schemas", "sectionTitle": "Table Schemas", @@ -7130,7 +7144,7 @@ "kind": "section" }, { - "id": "531-schema#name-mapping", + "id": "532-schema#name-mapping", "title": "Zero Schema", "searchTitle": "Name Mapping", "sectionTitle": "Name Mapping", @@ -7140,7 +7154,7 @@ "kind": "section" }, { - "id": "532-schema#multiple-schemas", + "id": "533-schema#multiple-schemas", "title": "Zero Schema", "searchTitle": "Multiple Schemas", "sectionTitle": "Multiple Schemas", @@ -7150,7 +7164,7 @@ "kind": "section" }, { - "id": "533-schema#optional-columns", + "id": "534-schema#optional-columns", "title": "Zero Schema", "searchTitle": "Optional Columns", "sectionTitle": "Optional Columns", @@ -7160,7 +7174,7 @@ "kind": "section" }, { - "id": "534-schema#enumerations", + "id": "535-schema#enumerations", "title": "Zero Schema", "searchTitle": "Enumerations", "sectionTitle": "Enumerations", @@ -7170,7 +7184,7 @@ "kind": "section" }, { - "id": "535-schema#custom-json-types", + "id": "536-schema#custom-json-types", "title": "Zero Schema", "searchTitle": "Custom JSON Types", "sectionTitle": "Custom JSON Types", @@ -7180,7 +7194,7 @@ "kind": "section" }, { - "id": "536-schema#compound-primary-keys", + "id": "537-schema#compound-primary-keys", "title": "Zero Schema", "searchTitle": "Compound Primary Keys", "sectionTitle": "Compound Primary Keys", @@ -7190,7 +7204,7 @@ "kind": "section" }, { - "id": "537-schema#unique-keys", + "id": "538-schema#unique-keys", "title": "Zero Schema", "searchTitle": "Unique Keys", "sectionTitle": "Unique Keys", @@ -7200,7 +7214,7 @@ "kind": "section" }, { - "id": "538-schema#relationships", + "id": "539-schema#relationships", "title": "Zero Schema", "searchTitle": "Relationships", "sectionTitle": "Relationships", @@ -7210,7 +7224,7 @@ "kind": "section" }, { - "id": "539-schema#many-to-many-relationships", + "id": "540-schema#many-to-many-relationships", "title": "Zero Schema", "searchTitle": "Many-to-Many Relationships", "sectionTitle": "Many-to-Many Relationships", @@ -7220,7 +7234,7 @@ "kind": "section" }, { - "id": "540-schema#compound-keys-relationships", + "id": "541-schema#compound-keys-relationships", "title": "Zero Schema", "searchTitle": "Compound Keys Relationships", "sectionTitle": "Compound Keys Relationships", @@ -7230,7 +7244,7 @@ "kind": "section" }, { - "id": "541-schema#circular-relationships", + "id": "542-schema#circular-relationships", "title": "Zero Schema", "searchTitle": "Circular Relationships", "sectionTitle": "Circular Relationships", @@ -7240,7 +7254,7 @@ "kind": "section" }, { - "id": "542-schema#database-schemas", + "id": "543-schema#database-schemas", "title": "Zero Schema", "searchTitle": "Database Schemas", "sectionTitle": "Database Schemas", @@ -7250,7 +7264,7 @@ "kind": "section" }, { - "id": "543-schema#register-schema-type", + "id": "544-schema#register-schema-type", "title": "Zero Schema", "searchTitle": "Register Schema Type", "sectionTitle": "Register Schema Type", @@ -7260,7 +7274,7 @@ "kind": "section" }, { - "id": "544-schema#schema-changes", + "id": "545-schema#schema-changes", "title": "Zero Schema", "searchTitle": "Schema Changes", "sectionTitle": "Schema Changes", @@ -7270,7 +7284,7 @@ "kind": "section" }, { - "id": "545-schema#development", + "id": "546-schema#development", "title": "Zero Schema", "searchTitle": "Development", "sectionTitle": "Development", @@ -7280,7 +7294,7 @@ "kind": "section" }, { - "id": "546-schema#production", + "id": "547-schema#production", "title": "Zero Schema", "searchTitle": "Production", "sectionTitle": "Production", @@ -7290,7 +7304,7 @@ "kind": "section" }, { - "id": "547-schema#expand-changes", + "id": "548-schema#expand-changes", "title": "Zero Schema", "searchTitle": "Expand Changes", "sectionTitle": "Expand Changes", @@ -7300,7 +7314,7 @@ "kind": "section" }, { - "id": "548-schema#contract-changes", + "id": "549-schema#contract-changes", "title": "Zero Schema", "searchTitle": "Contract Changes", "sectionTitle": "Contract Changes", @@ -7310,7 +7324,7 @@ "kind": "section" }, { - "id": "549-schema#compound-changes", + "id": "550-schema#compound-changes", "title": "Zero Schema", "searchTitle": "Compound Changes", "sectionTitle": "Compound Changes", @@ -7320,7 +7334,7 @@ "kind": "section" }, { - "id": "550-schema#examples", + "id": "551-schema#examples", "title": "Zero Schema", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -7330,7 +7344,7 @@ "kind": "section" }, { - "id": "551-schema#adding-a-column", + "id": "552-schema#adding-a-column", "title": "Zero Schema", "searchTitle": "Adding a Column", "sectionTitle": "Adding a Column", @@ -7340,7 +7354,7 @@ "kind": "section" }, { - "id": "552-schema#removing-a-column", + "id": "553-schema#removing-a-column", "title": "Zero Schema", "searchTitle": "Removing a Column", "sectionTitle": "Removing a Column", @@ -7350,7 +7364,7 @@ "kind": "section" }, { - "id": "553-schema#renaming-a-column", + "id": "554-schema#renaming-a-column", "title": "Zero Schema", "searchTitle": "Renaming a Column", "sectionTitle": "Renaming a Column", @@ -7360,7 +7374,7 @@ "kind": "section" }, { - "id": "554-schema#making-a-column-optional", + "id": "555-schema#making-a-column-optional", "title": "Zero Schema", "searchTitle": "Making a Column Optional", "sectionTitle": "Making a Column Optional", @@ -7370,7 +7384,7 @@ "kind": "section" }, { - "id": "555-schema#quick-reference", + "id": "556-schema#quick-reference", "title": "Zero Schema", "searchTitle": "Quick Reference", "sectionTitle": "Quick Reference", @@ -7380,7 +7394,7 @@ "kind": "section" }, { - "id": "556-schema#backfill", + "id": "557-schema#backfill", "title": "Zero Schema", "searchTitle": "Backfill", "sectionTitle": "Backfill", @@ -7390,7 +7404,7 @@ "kind": "section" }, { - "id": "557-schema#monitoring-backfill-progress", + "id": "558-schema#monitoring-backfill-progress", "title": "Zero Schema", "searchTitle": "Monitoring Backfill Progress", "sectionTitle": "Monitoring Backfill Progress", @@ -7462,7 +7476,7 @@ "kind": "page" }, { - "id": "558-self-host#docker-images", + "id": "559-self-host#docker-images", "title": "Self-Hosting Zero", "searchTitle": "Docker Images", "sectionTitle": "Docker Images", @@ -7472,7 +7486,7 @@ "kind": "section" }, { - "id": "559-self-host#minimum-viable-strategy", + "id": "560-self-host#minimum-viable-strategy", "title": "Self-Hosting Zero", "searchTitle": "Minimum Viable Strategy", "sectionTitle": "Minimum Viable Strategy", @@ -7482,7 +7496,7 @@ "kind": "section" }, { - "id": "560-self-host#maximal-strategy", + "id": "561-self-host#maximal-strategy", "title": "Self-Hosting Zero", "searchTitle": "Maximal Strategy", "sectionTitle": "Maximal Strategy", @@ -7492,7 +7506,7 @@ "kind": "section" }, { - "id": "561-self-host#replica-lifecycle", + "id": "562-self-host#replica-lifecycle", "title": "Self-Hosting Zero", "searchTitle": "Replica Lifecycle", "sectionTitle": "Replica Lifecycle", @@ -7502,7 +7516,7 @@ "kind": "section" }, { - "id": "562-self-host#performance", + "id": "563-self-host#performance", "title": "Self-Hosting Zero", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -7512,7 +7526,7 @@ "kind": "section" }, { - "id": "563-self-host#hydration", + "id": "564-self-host#hydration", "title": "Self-Hosting Zero", "searchTitle": "Hydration", "sectionTitle": "Hydration", @@ -7522,7 +7536,7 @@ "kind": "section" }, { - "id": "564-self-host#ivm-advancement", + "id": "565-self-host#ivm-advancement", "title": "Self-Hosting Zero", "searchTitle": "IVM advancement", "sectionTitle": "IVM advancement", @@ -7532,7 +7546,7 @@ "kind": "section" }, { - "id": "565-self-host#system-level", + "id": "566-self-host#system-level", "title": "Self-Hosting Zero", "searchTitle": "System-level", "sectionTitle": "System-level", @@ -7542,7 +7556,7 @@ "kind": "section" }, { - "id": "566-self-host#networking", + "id": "567-self-host#networking", "title": "Self-Hosting Zero", "searchTitle": "Networking", "sectionTitle": "Networking", @@ -7552,7 +7566,7 @@ "kind": "section" }, { - "id": "567-self-host#sticky-sessions", + "id": "568-self-host#sticky-sessions", "title": "Self-Hosting Zero", "searchTitle": "Sticky Sessions", "sectionTitle": "Sticky Sessions", @@ -7562,7 +7576,7 @@ "kind": "section" }, { - "id": "568-self-host#rolling-updates", + "id": "569-self-host#rolling-updates", "title": "Self-Hosting Zero", "searchTitle": "Rolling Updates", "sectionTitle": "Rolling Updates", @@ -7572,7 +7586,7 @@ "kind": "section" }, { - "id": "569-self-host#clientserver-version-compatibility", + "id": "570-self-host#clientserver-version-compatibility", "title": "Self-Hosting Zero", "searchTitle": "Client/Server Version Compatibility", "sectionTitle": "Client/Server Version Compatibility", @@ -7582,7 +7596,7 @@ "kind": "section" }, { - "id": "570-self-host#configuration", + "id": "571-self-host#configuration", "title": "Self-Hosting Zero", "searchTitle": "Configuration", "sectionTitle": "Configuration", @@ -7622,7 +7636,7 @@ "kind": "page" }, { - "id": "571-server-zql#creating-a-database", + "id": "572-server-zql#creating-a-database", "title": "ZQL on the Server", "searchTitle": "Creating a Database", "sectionTitle": "Creating a Database", @@ -7632,7 +7646,7 @@ "kind": "section" }, { - "id": "572-server-zql#isolation-level", + "id": "573-server-zql#isolation-level", "title": "ZQL on the Server", "searchTitle": "Isolation Level", "sectionTitle": "Isolation Level", @@ -7642,7 +7656,7 @@ "kind": "section" }, { - "id": "573-server-zql#custom-database", + "id": "574-server-zql#custom-database", "title": "ZQL on the Server", "searchTitle": "Custom Database", "sectionTitle": "Custom Database", @@ -7652,7 +7666,7 @@ "kind": "section" }, { - "id": "574-server-zql#running-zql", + "id": "575-server-zql#running-zql", "title": "ZQL on the Server", "searchTitle": "Running ZQL", "sectionTitle": "Running ZQL", @@ -7662,7 +7676,7 @@ "kind": "section" }, { - "id": "575-server-zql#ssr", + "id": "576-server-zql#ssr", "title": "ZQL on the Server", "searchTitle": "SSR", "sectionTitle": "SSR", @@ -7694,7 +7708,7 @@ "kind": "page" }, { - "id": "576-solidjs#setup", + "id": "577-solidjs#setup", "title": "SolidJS", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -7704,7 +7718,7 @@ "kind": "section" }, { - "id": "577-solidjs#usage", + "id": "578-solidjs#usage", "title": "SolidJS", "searchTitle": "Usage", "sectionTitle": "Usage", @@ -7714,7 +7728,7 @@ "kind": "section" }, { - "id": "578-solidjs#examples", + "id": "579-solidjs#examples", "title": "SolidJS", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -7750,7 +7764,7 @@ "kind": "page" }, { - "id": "579-status#breaking-changes", + "id": "580-status#breaking-changes", "title": "Project Status", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -7760,7 +7774,7 @@ "kind": "section" }, { - "id": "580-status#roadmap", + "id": "581-status#roadmap", "title": "Project Status", "searchTitle": "Roadmap", "sectionTitle": "Roadmap", @@ -7770,7 +7784,7 @@ "kind": "section" }, { - "id": "581-status#2026", + "id": "582-status#2026", "title": "Project Status", "searchTitle": "2026", "sectionTitle": "2026", @@ -7780,7 +7794,7 @@ "kind": "section" }, { - "id": "582-status#soon", + "id": "583-status#soon", "title": "Project Status", "searchTitle": "Soon", "sectionTitle": "Soon", @@ -7812,7 +7826,7 @@ "kind": "page" }, { - "id": "583-sync#problem", + "id": "584-sync#problem", "title": "What is Sync?", "searchTitle": "Problem", "sectionTitle": "Problem", @@ -7822,7 +7836,7 @@ "kind": "section" }, { - "id": "584-sync#solution", + "id": "585-sync#solution", "title": "What is Sync?", "searchTitle": "Solution", "sectionTitle": "Solution", @@ -7832,7 +7846,7 @@ "kind": "section" }, { - "id": "585-sync#history-of-sync", + "id": "586-sync#history-of-sync", "title": "What is Sync?", "searchTitle": "History of Sync", "sectionTitle": "History of Sync", @@ -7916,7 +7930,7 @@ "kind": "page" }, { - "id": "586-tutorial#setup", + "id": "587-tutorial#setup", "title": "Tutorial", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -7926,7 +7940,7 @@ "kind": "section" }, { - "id": "587-tutorial#create-a-project", + "id": "588-tutorial#create-a-project", "title": "Tutorial", "searchTitle": "Create a Project", "sectionTitle": "Create a Project", @@ -7936,7 +7950,7 @@ "kind": "section" }, { - "id": "588-tutorial#set-up-your-database", + "id": "589-tutorial#set-up-your-database", "title": "Tutorial", "searchTitle": "Set Up Your Database", "sectionTitle": "Set Up Your Database", @@ -7946,7 +7960,7 @@ "kind": "section" }, { - "id": "589-tutorial#install-and-run-zero-cache", + "id": "590-tutorial#install-and-run-zero-cache", "title": "Tutorial", "searchTitle": "Install and Run Zero-Cache", "sectionTitle": "Install and Run Zero-Cache", @@ -7956,7 +7970,7 @@ "kind": "section" }, { - "id": "590-tutorial#integrate-zero", + "id": "591-tutorial#integrate-zero", "title": "Tutorial", "searchTitle": "Integrate Zero", "sectionTitle": "Integrate Zero", @@ -7966,7 +7980,7 @@ "kind": "section" }, { - "id": "591-tutorial#set-up-your-zero-schema", + "id": "592-tutorial#set-up-your-zero-schema", "title": "Tutorial", "searchTitle": "Set Up Your Zero Schema", "sectionTitle": "Set Up Your Zero Schema", @@ -7976,7 +7990,7 @@ "kind": "section" }, { - "id": "592-tutorial#set-up-the-zero-client", + "id": "593-tutorial#set-up-the-zero-client", "title": "Tutorial", "searchTitle": "Set Up the Zero Client", "sectionTitle": "Set Up the Zero Client", @@ -7986,7 +8000,7 @@ "kind": "section" }, { - "id": "593-tutorial#sync-data", + "id": "594-tutorial#sync-data", "title": "Tutorial", "searchTitle": "Sync Data", "sectionTitle": "Sync Data", @@ -7996,7 +8010,7 @@ "kind": "section" }, { - "id": "594-tutorial#define-query", + "id": "595-tutorial#define-query", "title": "Tutorial", "searchTitle": "Define Query", "sectionTitle": "Define Query", @@ -8006,7 +8020,7 @@ "kind": "section" }, { - "id": "595-tutorial#add-query-endpoint", + "id": "596-tutorial#add-query-endpoint", "title": "Tutorial", "searchTitle": "Add Query Endpoint", "sectionTitle": "Add Query Endpoint", @@ -8016,7 +8030,7 @@ "kind": "section" }, { - "id": "596-tutorial#invoke-query", + "id": "597-tutorial#invoke-query", "title": "Tutorial", "searchTitle": "Invoke Query", "sectionTitle": "Invoke Query", @@ -8026,7 +8040,7 @@ "kind": "section" }, { - "id": "597-tutorial#mutate-data", + "id": "598-tutorial#mutate-data", "title": "Tutorial", "searchTitle": "Mutate Data", "sectionTitle": "Mutate Data", @@ -8036,7 +8050,7 @@ "kind": "section" }, { - "id": "598-tutorial#define-mutators", + "id": "599-tutorial#define-mutators", "title": "Tutorial", "searchTitle": "Define Mutators", "sectionTitle": "Define Mutators", @@ -8046,7 +8060,7 @@ "kind": "section" }, { - "id": "599-tutorial#add-mutate-endpoint", + "id": "600-tutorial#add-mutate-endpoint", "title": "Tutorial", "searchTitle": "Add Mutate Endpoint", "sectionTitle": "Add Mutate Endpoint", @@ -8056,7 +8070,7 @@ "kind": "section" }, { - "id": "600-tutorial#invoke-mutators", + "id": "601-tutorial#invoke-mutators", "title": "Tutorial", "searchTitle": "Invoke Mutators", "sectionTitle": "Invoke Mutators", @@ -8066,7 +8080,7 @@ "kind": "section" }, { - "id": "601-tutorial#next-steps", + "id": "602-tutorial#next-steps", "title": "Tutorial", "searchTitle": "Next Steps", "sectionTitle": "Next Steps", @@ -8142,7 +8156,7 @@ "kind": "page" }, { - "id": "602-when-to-use#zero-might-be-a-good-fit", + "id": "603-when-to-use#zero-might-be-a-good-fit", "title": "When To Use Zero", "searchTitle": "Zero Might be a Good Fit", "sectionTitle": "Zero Might be a Good Fit", @@ -8152,7 +8166,7 @@ "kind": "section" }, { - "id": "603-when-to-use#you-want-to-sync-only-a-small-subset-of-data-to-client", + "id": "604-when-to-use#you-want-to-sync-only-a-small-subset-of-data-to-client", "title": "When To Use Zero", "searchTitle": "You want to sync only a small subset of data to client", "sectionTitle": "You want to sync only a small subset of data to client", @@ -8162,7 +8176,7 @@ "kind": "section" }, { - "id": "604-when-to-use#you-need-fine-grained-read-or-write-permissions", + "id": "605-when-to-use#you-need-fine-grained-read-or-write-permissions", "title": "When To Use Zero", "searchTitle": "You need fine-grained read or write permissions", "sectionTitle": "You need fine-grained read or write permissions", @@ -8172,7 +8186,7 @@ "kind": "section" }, { - "id": "605-when-to-use#you-are-building-a-traditional-client-server-web-app", + "id": "606-when-to-use#you-are-building-a-traditional-client-server-web-app", "title": "When To Use Zero", "searchTitle": "You are building a traditional client-server web app", "sectionTitle": "You are building a traditional client-server web app", @@ -8182,7 +8196,7 @@ "kind": "section" }, { - "id": "606-when-to-use#you-use-postgresql", + "id": "607-when-to-use#you-use-postgresql", "title": "When To Use Zero", "searchTitle": "You use PostgreSQL", "sectionTitle": "You use PostgreSQL", @@ -8192,7 +8206,7 @@ "kind": "section" }, { - "id": "607-when-to-use#your-app-is-broadly-like-linear", + "id": "608-when-to-use#your-app-is-broadly-like-linear", "title": "When To Use Zero", "searchTitle": "Your app is broadly \"like Linear\"", "sectionTitle": "Your app is broadly \"like Linear\"", @@ -8202,7 +8216,7 @@ "kind": "section" }, { - "id": "608-when-to-use#interaction-performance-is-very-important-to-you", + "id": "609-when-to-use#interaction-performance-is-very-important-to-you", "title": "When To Use Zero", "searchTitle": "Interaction performance is very important to you", "sectionTitle": "Interaction performance is very important to you", @@ -8212,7 +8226,7 @@ "kind": "section" }, { - "id": "609-when-to-use#zero-might-not-be-a-good-fit", + "id": "610-when-to-use#zero-might-not-be-a-good-fit", "title": "When To Use Zero", "searchTitle": "Zero Might Not be a Good Fit", "sectionTitle": "Zero Might Not be a Good Fit", @@ -8222,7 +8236,7 @@ "kind": "section" }, { - "id": "610-when-to-use#you-need-the-privacy-or-data-ownership-benefits-of-local-first", + "id": "611-when-to-use#you-need-the-privacy-or-data-ownership-benefits-of-local-first", "title": "When To Use Zero", "searchTitle": "You need the privacy or data ownership benefits of local-first", "sectionTitle": "You need the privacy or data ownership benefits of local-first", @@ -8232,7 +8246,7 @@ "kind": "section" }, { - "id": "611-when-to-use#you-need-to-support-offline-writes-or-long-periods-offline", + "id": "612-when-to-use#you-need-to-support-offline-writes-or-long-periods-offline", "title": "When To Use Zero", "searchTitle": "You need to support offline writes or long periods offline", "sectionTitle": "You need to support offline writes or long periods offline", @@ -8242,7 +8256,7 @@ "kind": "section" }, { - "id": "612-when-to-use#you-are-building-a-native-mobile-app", + "id": "613-when-to-use#you-are-building-a-native-mobile-app", "title": "When To Use Zero", "searchTitle": "You are building a native mobile app", "sectionTitle": "You are building a native mobile app", @@ -8252,7 +8266,7 @@ "kind": "section" }, { - "id": "613-when-to-use#the-total-backend-dataset-is--100gb", + "id": "614-when-to-use#the-total-backend-dataset-is--100gb", "title": "When To Use Zero", "searchTitle": "The total backend dataset is > ~100GB", "sectionTitle": "The total backend dataset is > ~100GB", @@ -8262,7 +8276,7 @@ "kind": "section" }, { - "id": "614-when-to-use#zero-might-not-be-a-good-fit-yet", + "id": "615-when-to-use#zero-might-not-be-a-good-fit-yet", "title": "When To Use Zero", "searchTitle": "Zero Might Not be a Good Fit Yet", "sectionTitle": "Zero Might Not be a Good Fit Yet", @@ -8272,7 +8286,7 @@ "kind": "section" }, { - "id": "615-when-to-use#alternatives", + "id": "616-when-to-use#alternatives", "title": "When To Use Zero", "searchTitle": "Alternatives", "sectionTitle": "Alternatives", @@ -8676,7 +8690,7 @@ "kind": "page" }, { - "id": "616-zero-cache-config#required-flags", + "id": "617-zero-cache-config#required-flags", "title": "zero-cache Config", "searchTitle": "Required Flags", "sectionTitle": "Required Flags", @@ -8686,7 +8700,7 @@ "kind": "section" }, { - "id": "617-zero-cache-config#upstream-db", + "id": "618-zero-cache-config#upstream-db", "title": "zero-cache Config", "searchTitle": "Upstream DB", "sectionTitle": "Upstream DB", @@ -8696,7 +8710,7 @@ "kind": "section" }, { - "id": "618-zero-cache-config#admin-password", + "id": "619-zero-cache-config#admin-password", "title": "zero-cache Config", "searchTitle": "Admin Password", "sectionTitle": "Admin Password", @@ -8706,7 +8720,7 @@ "kind": "section" }, { - "id": "619-zero-cache-config#optional-flags", + "id": "620-zero-cache-config#optional-flags", "title": "zero-cache Config", "searchTitle": "Optional Flags", "sectionTitle": "Optional Flags", @@ -8716,7 +8730,7 @@ "kind": "section" }, { - "id": "620-zero-cache-config#allow-legacy-queries", + "id": "621-zero-cache-config#allow-legacy-queries", "title": "zero-cache Config", "searchTitle": "Allow Legacy Queries", "sectionTitle": "Allow Legacy Queries", @@ -8726,7 +8740,7 @@ "kind": "section" }, { - "id": "621-zero-cache-config#app-id", + "id": "622-zero-cache-config#app-id", "title": "zero-cache Config", "searchTitle": "App ID", "sectionTitle": "App ID", @@ -8736,7 +8750,7 @@ "kind": "section" }, { - "id": "622-zero-cache-config#app-publications", + "id": "623-zero-cache-config#app-publications", "title": "zero-cache Config", "searchTitle": "App Publications", "sectionTitle": "App Publications", @@ -8746,7 +8760,7 @@ "kind": "section" }, { - "id": "623-zero-cache-config#auth-revalidate-interval-seconds", + "id": "624-zero-cache-config#auth-revalidate-interval-seconds", "title": "zero-cache Config", "searchTitle": "Auth Revalidate Interval Seconds", "sectionTitle": "Auth Revalidate Interval Seconds", @@ -8756,7 +8770,7 @@ "kind": "section" }, { - "id": "624-zero-cache-config#auth-retransform-interval-seconds", + "id": "625-zero-cache-config#auth-retransform-interval-seconds", "title": "zero-cache Config", "searchTitle": "Auth Retransform Interval Seconds", "sectionTitle": "Auth Retransform Interval Seconds", @@ -8766,7 +8780,7 @@ "kind": "section" }, { - "id": "625-zero-cache-config#auto-reset", + "id": "626-zero-cache-config#auto-reset", "title": "zero-cache Config", "searchTitle": "Auto Reset", "sectionTitle": "Auto Reset", @@ -8776,7 +8790,7 @@ "kind": "section" }, { - "id": "626-zero-cache-config#change-db", + "id": "627-zero-cache-config#change-db", "title": "zero-cache Config", "searchTitle": "Change DB", "sectionTitle": "Change DB", @@ -8786,7 +8800,7 @@ "kind": "section" }, { - "id": "627-zero-cache-config#change-streamer-back-pressure-limit-heap-proportion", + "id": "628-zero-cache-config#change-streamer-back-pressure-limit-heap-proportion", "title": "zero-cache Config", "searchTitle": "Change Streamer Back Pressure Limit Heap Proportion", "sectionTitle": "Change Streamer Back Pressure Limit Heap Proportion", @@ -8796,7 +8810,7 @@ "kind": "section" }, { - "id": "628-zero-cache-config#change-streamer-flow-control-consensus-timeout-proportion", + "id": "629-zero-cache-config#change-streamer-flow-control-consensus-timeout-proportion", "title": "zero-cache Config", "searchTitle": "Change Streamer Flow Control Consensus Timeout Proportion", "sectionTitle": "Change Streamer Flow Control Consensus Timeout Proportion", @@ -8806,7 +8820,7 @@ "kind": "section" }, { - "id": "629-zero-cache-config#change-streamer-flow-control-slow-subscriber-grace-period-seconds", + "id": "630-zero-cache-config#change-streamer-flow-control-slow-subscriber-grace-period-seconds", "title": "zero-cache Config", "searchTitle": "Change Streamer Flow Control Slow Subscriber Grace Period Seconds", "sectionTitle": "Change Streamer Flow Control Slow Subscriber Grace Period Seconds", @@ -8816,7 +8830,7 @@ "kind": "section" }, { - "id": "630-zero-cache-config#change-streamer-port", + "id": "631-zero-cache-config#change-streamer-port", "title": "zero-cache Config", "searchTitle": "Change Streamer Port", "sectionTitle": "Change Streamer Port", @@ -8826,7 +8840,7 @@ "kind": "section" }, { - "id": "631-zero-cache-config#change-streamer-startup-delay-ms", + "id": "632-zero-cache-config#change-streamer-startup-delay-ms", "title": "zero-cache Config", "searchTitle": "Change Streamer Startup Delay (ms)", "sectionTitle": "Change Streamer Startup Delay (ms)", @@ -8836,7 +8850,7 @@ "kind": "section" }, { - "id": "632-zero-cache-config#change-streamer-uri", + "id": "633-zero-cache-config#change-streamer-uri", "title": "zero-cache Config", "searchTitle": "Change Streamer URI", "sectionTitle": "Change Streamer URI", @@ -8846,7 +8860,7 @@ "kind": "section" }, { - "id": "633-zero-cache-config#cvr-db", + "id": "634-zero-cache-config#cvr-db", "title": "zero-cache Config", "searchTitle": "CVR DB", "sectionTitle": "CVR DB", @@ -8856,7 +8870,7 @@ "kind": "section" }, { - "id": "634-zero-cache-config#cvr-garbage-collection-inactivity-threshold-hours", + "id": "635-zero-cache-config#cvr-garbage-collection-inactivity-threshold-hours", "title": "zero-cache Config", "searchTitle": "CVR Garbage Collection Inactivity Threshold Hours", "sectionTitle": "CVR Garbage Collection Inactivity Threshold Hours", @@ -8866,7 +8880,7 @@ "kind": "section" }, { - "id": "635-zero-cache-config#cvr-garbage-collection-initial-batch-size", + "id": "636-zero-cache-config#cvr-garbage-collection-initial-batch-size", "title": "zero-cache Config", "searchTitle": "CVR Garbage Collection Initial Batch Size", "sectionTitle": "CVR Garbage Collection Initial Batch Size", @@ -8876,7 +8890,7 @@ "kind": "section" }, { - "id": "636-zero-cache-config#cvr-garbage-collection-initial-interval-seconds", + "id": "637-zero-cache-config#cvr-garbage-collection-initial-interval-seconds", "title": "zero-cache Config", "searchTitle": "CVR Garbage Collection Initial Interval Seconds", "sectionTitle": "CVR Garbage Collection Initial Interval Seconds", @@ -8886,7 +8900,7 @@ "kind": "section" }, { - "id": "637-zero-cache-config#cvr-max-connections", + "id": "638-zero-cache-config#cvr-max-connections", "title": "zero-cache Config", "searchTitle": "CVR Max Connections", "sectionTitle": "CVR Max Connections", @@ -8896,7 +8910,7 @@ "kind": "section" }, { - "id": "638-zero-cache-config#enable-query-planner", + "id": "639-zero-cache-config#enable-query-planner", "title": "zero-cache Config", "searchTitle": "Enable Query Planner", "sectionTitle": "Enable Query Planner", @@ -8906,7 +8920,7 @@ "kind": "section" }, { - "id": "639-zero-cache-config#enable-crud-mutations", + "id": "640-zero-cache-config#enable-crud-mutations", "title": "zero-cache Config", "searchTitle": "Enable CRUD Mutations", "sectionTitle": "Enable CRUD Mutations", @@ -8916,7 +8930,7 @@ "kind": "section" }, { - "id": "640-zero-cache-config#enable-telemetry", + "id": "641-zero-cache-config#enable-telemetry", "title": "zero-cache Config", "searchTitle": "Enable Telemetry", "sectionTitle": "Enable Telemetry", @@ -8926,7 +8940,7 @@ "kind": "section" }, { - "id": "641-zero-cache-config#initial-sync-table-copy-workers", + "id": "642-zero-cache-config#initial-sync-table-copy-workers", "title": "zero-cache Config", "searchTitle": "Initial Sync Table Copy Workers", "sectionTitle": "Initial Sync Table Copy Workers", @@ -8936,7 +8950,7 @@ "kind": "section" }, { - "id": "642-zero-cache-config#lazy-startup", + "id": "643-zero-cache-config#lazy-startup", "title": "zero-cache Config", "searchTitle": "Lazy Startup", "sectionTitle": "Lazy Startup", @@ -8946,7 +8960,7 @@ "kind": "section" }, { - "id": "643-zero-cache-config#litestream-backup-url", + "id": "644-zero-cache-config#litestream-backup-url", "title": "zero-cache Config", "searchTitle": "Litestream Backup URL", "sectionTitle": "Litestream Backup URL", @@ -8956,7 +8970,7 @@ "kind": "section" }, { - "id": "644-zero-cache-config#litestream-endpoint", + "id": "645-zero-cache-config#litestream-endpoint", "title": "zero-cache Config", "searchTitle": "Litestream Endpoint", "sectionTitle": "Litestream Endpoint", @@ -8966,7 +8980,7 @@ "kind": "section" }, { - "id": "645-zero-cache-config#litestream-checkpoint-threshold-mb", + "id": "646-zero-cache-config#litestream-checkpoint-threshold-mb", "title": "zero-cache Config", "searchTitle": "Litestream Checkpoint Threshold MB", "sectionTitle": "Litestream Checkpoint Threshold MB", @@ -8976,7 +8990,7 @@ "kind": "section" }, { - "id": "646-zero-cache-config#litestream-config-path", + "id": "647-zero-cache-config#litestream-config-path", "title": "zero-cache Config", "searchTitle": "Litestream Config Path", "sectionTitle": "Litestream Config Path", @@ -8986,7 +9000,7 @@ "kind": "section" }, { - "id": "647-zero-cache-config#litestream-config-path-v5", + "id": "648-zero-cache-config#litestream-config-path-v5", "title": "zero-cache Config", "searchTitle": "Litestream Config Path V5", "sectionTitle": "Litestream Config Path V5", @@ -8996,7 +9010,7 @@ "kind": "section" }, { - "id": "648-zero-cache-config#litestream-executable", + "id": "649-zero-cache-config#litestream-executable", "title": "zero-cache Config", "searchTitle": "Litestream Executable", "sectionTitle": "Litestream Executable", @@ -9006,7 +9020,7 @@ "kind": "section" }, { - "id": "649-zero-cache-config#litestream-v5-executable", + "id": "650-zero-cache-config#litestream-v5-executable", "title": "zero-cache Config", "searchTitle": "Litestream V5 Executable", "sectionTitle": "Litestream V5 Executable", @@ -9016,7 +9030,7 @@ "kind": "section" }, { - "id": "650-zero-cache-config#litestream-restore-using-v5", + "id": "651-zero-cache-config#litestream-restore-using-v5", "title": "zero-cache Config", "searchTitle": "Litestream Restore Using V5", "sectionTitle": "Litestream Restore Using V5", @@ -9026,7 +9040,7 @@ "kind": "section" }, { - "id": "651-zero-cache-config#litestream-backup-using-v5", + "id": "652-zero-cache-config#litestream-backup-using-v5", "title": "zero-cache Config", "searchTitle": "Litestream Backup Using V5", "sectionTitle": "Litestream Backup Using V5", @@ -9036,7 +9050,7 @@ "kind": "section" }, { - "id": "652-zero-cache-config#litestream-incremental-backup-interval-minutes", + "id": "653-zero-cache-config#litestream-incremental-backup-interval-minutes", "title": "zero-cache Config", "searchTitle": "Litestream Incremental Backup Interval Minutes", "sectionTitle": "Litestream Incremental Backup Interval Minutes", @@ -9046,7 +9060,7 @@ "kind": "section" }, { - "id": "653-zero-cache-config#litestream-incremental-backup-interval-seconds", + "id": "654-zero-cache-config#litestream-incremental-backup-interval-seconds", "title": "zero-cache Config", "searchTitle": "Litestream Incremental Backup Interval Seconds", "sectionTitle": "Litestream Incremental Backup Interval Seconds", @@ -9056,7 +9070,7 @@ "kind": "section" }, { - "id": "654-zero-cache-config#litestream-maximum-checkpoint-page-count", + "id": "655-zero-cache-config#litestream-maximum-checkpoint-page-count", "title": "zero-cache Config", "searchTitle": "Litestream Maximum Checkpoint Page Count", "sectionTitle": "Litestream Maximum Checkpoint Page Count", @@ -9066,7 +9080,7 @@ "kind": "section" }, { - "id": "655-zero-cache-config#litestream-multipart-concurrency", + "id": "656-zero-cache-config#litestream-multipart-concurrency", "title": "zero-cache Config", "searchTitle": "Litestream Multipart Concurrency", "sectionTitle": "Litestream Multipart Concurrency", @@ -9076,7 +9090,7 @@ "kind": "section" }, { - "id": "656-zero-cache-config#litestream-multipart-size", + "id": "657-zero-cache-config#litestream-multipart-size", "title": "zero-cache Config", "searchTitle": "Litestream Multipart Size", "sectionTitle": "Litestream Multipart Size", @@ -9086,7 +9100,7 @@ "kind": "section" }, { - "id": "657-zero-cache-config#litestream-log-level", + "id": "658-zero-cache-config#litestream-log-level", "title": "zero-cache Config", "searchTitle": "Litestream Log Level", "sectionTitle": "Litestream Log Level", @@ -9096,7 +9110,7 @@ "kind": "section" }, { - "id": "658-zero-cache-config#litestream-port", + "id": "659-zero-cache-config#litestream-port", "title": "zero-cache Config", "searchTitle": "Litestream Port", "sectionTitle": "Litestream Port", @@ -9106,7 +9120,7 @@ "kind": "section" }, { - "id": "659-zero-cache-config#litestream-region", + "id": "660-zero-cache-config#litestream-region", "title": "zero-cache Config", "searchTitle": "Litestream Region", "sectionTitle": "Litestream Region", @@ -9116,7 +9130,7 @@ "kind": "section" }, { - "id": "660-zero-cache-config#litestream-restore-parallelism", + "id": "661-zero-cache-config#litestream-restore-parallelism", "title": "zero-cache Config", "searchTitle": "Litestream Restore Parallelism", "sectionTitle": "Litestream Restore Parallelism", @@ -9126,7 +9140,7 @@ "kind": "section" }, { - "id": "661-zero-cache-config#litestream-snapshot-backup-interval-hours", + "id": "662-zero-cache-config#litestream-snapshot-backup-interval-hours", "title": "zero-cache Config", "searchTitle": "Litestream Snapshot Backup Interval Hours", "sectionTitle": "Litestream Snapshot Backup Interval Hours", @@ -9136,7 +9150,7 @@ "kind": "section" }, { - "id": "662-zero-cache-config#log-format", + "id": "663-zero-cache-config#log-format", "title": "zero-cache Config", "searchTitle": "Log Format", "sectionTitle": "Log Format", @@ -9146,7 +9160,7 @@ "kind": "section" }, { - "id": "663-zero-cache-config#log-ivm-sampling", + "id": "664-zero-cache-config#log-ivm-sampling", "title": "zero-cache Config", "searchTitle": "Log IVM Sampling", "sectionTitle": "Log IVM Sampling", @@ -9156,7 +9170,7 @@ "kind": "section" }, { - "id": "664-zero-cache-config#log-level", + "id": "665-zero-cache-config#log-level", "title": "zero-cache Config", "searchTitle": "Log Level", "sectionTitle": "Log Level", @@ -9166,7 +9180,7 @@ "kind": "section" }, { - "id": "665-zero-cache-config#log-slow-hydrate-threshold", + "id": "666-zero-cache-config#log-slow-hydrate-threshold", "title": "zero-cache Config", "searchTitle": "Log Slow Hydrate Threshold", "sectionTitle": "Log Slow Hydrate Threshold", @@ -9176,7 +9190,7 @@ "kind": "section" }, { - "id": "666-zero-cache-config#log-slow-row-threshold", + "id": "667-zero-cache-config#log-slow-row-threshold", "title": "zero-cache Config", "searchTitle": "Log Slow Row Threshold", "sectionTitle": "Log Slow Row Threshold", @@ -9186,7 +9200,7 @@ "kind": "section" }, { - "id": "667-zero-cache-config#mutate-api-key", + "id": "668-zero-cache-config#mutate-api-key", "title": "zero-cache Config", "searchTitle": "Mutate API Key", "sectionTitle": "Mutate API Key", @@ -9196,7 +9210,7 @@ "kind": "section" }, { - "id": "668-zero-cache-config#mutate-allowed-client-headers", + "id": "669-zero-cache-config#mutate-allowed-client-headers", "title": "zero-cache Config", "searchTitle": "Mutate Allowed Client Headers", "sectionTitle": "Mutate Allowed Client Headers", @@ -9206,7 +9220,7 @@ "kind": "section" }, { - "id": "669-zero-cache-config#mutate-allowed-request-headers", + "id": "670-zero-cache-config#mutate-allowed-request-headers", "title": "zero-cache Config", "searchTitle": "Mutate Allowed Request Headers", "sectionTitle": "Mutate Allowed Request Headers", @@ -9216,7 +9230,7 @@ "kind": "section" }, { - "id": "670-zero-cache-config#mutate-forward-cookies", + "id": "671-zero-cache-config#mutate-forward-cookies", "title": "zero-cache Config", "searchTitle": "Mutate Forward Cookies", "sectionTitle": "Mutate Forward Cookies", @@ -9226,7 +9240,7 @@ "kind": "section" }, { - "id": "671-zero-cache-config#mutate-url", + "id": "672-zero-cache-config#mutate-url", "title": "zero-cache Config", "searchTitle": "Mutate URL", "sectionTitle": "Mutate URL", @@ -9236,7 +9250,7 @@ "kind": "section" }, { - "id": "672-zero-cache-config#number-of-sync-workers", + "id": "673-zero-cache-config#number-of-sync-workers", "title": "zero-cache Config", "searchTitle": "Number of Sync Workers", "sectionTitle": "Number of Sync Workers", @@ -9246,7 +9260,7 @@ "kind": "section" }, { - "id": "673-zero-cache-config#operator-password", + "id": "674-zero-cache-config#operator-password", "title": "zero-cache Config", "searchTitle": "Operator Password", "sectionTitle": "Operator Password", @@ -9256,7 +9270,7 @@ "kind": "section" }, { - "id": "674-zero-cache-config#per-user-mutation-limit-max", + "id": "675-zero-cache-config#per-user-mutation-limit-max", "title": "zero-cache Config", "searchTitle": "Per User Mutation Limit Max", "sectionTitle": "Per User Mutation Limit Max", @@ -9266,7 +9280,7 @@ "kind": "section" }, { - "id": "675-zero-cache-config#per-user-mutation-limit-window-ms", + "id": "676-zero-cache-config#per-user-mutation-limit-window-ms", "title": "zero-cache Config", "searchTitle": "Per User Mutation Limit Window (ms)", "sectionTitle": "Per User Mutation Limit Window (ms)", @@ -9276,7 +9290,7 @@ "kind": "section" }, { - "id": "676-zero-cache-config#pg-replication-slot-failover", + "id": "677-zero-cache-config#pg-replication-slot-failover", "title": "zero-cache Config", "searchTitle": "PG Replication Slot Failover", "sectionTitle": "PG Replication Slot Failover", @@ -9286,7 +9300,7 @@ "kind": "section" }, { - "id": "677-zero-cache-config#port", + "id": "678-zero-cache-config#port", "title": "zero-cache Config", "searchTitle": "Port", "sectionTitle": "Port", @@ -9296,7 +9310,7 @@ "kind": "section" }, { - "id": "678-zero-cache-config#query-api-key", + "id": "679-zero-cache-config#query-api-key", "title": "zero-cache Config", "searchTitle": "Query API Key", "sectionTitle": "Query API Key", @@ -9306,7 +9320,7 @@ "kind": "section" }, { - "id": "679-zero-cache-config#query-allowed-client-headers", + "id": "680-zero-cache-config#query-allowed-client-headers", "title": "zero-cache Config", "searchTitle": "Query Allowed Client Headers", "sectionTitle": "Query Allowed Client Headers", @@ -9316,7 +9330,7 @@ "kind": "section" }, { - "id": "680-zero-cache-config#query-allowed-request-headers", + "id": "681-zero-cache-config#query-allowed-request-headers", "title": "zero-cache Config", "searchTitle": "Query Allowed Request Headers", "sectionTitle": "Query Allowed Request Headers", @@ -9326,7 +9340,7 @@ "kind": "section" }, { - "id": "681-zero-cache-config#query-forward-cookies", + "id": "682-zero-cache-config#query-forward-cookies", "title": "zero-cache Config", "searchTitle": "Query Forward Cookies", "sectionTitle": "Query Forward Cookies", @@ -9336,7 +9350,7 @@ "kind": "section" }, { - "id": "682-zero-cache-config#query-hydration-stats", + "id": "683-zero-cache-config#query-hydration-stats", "title": "zero-cache Config", "searchTitle": "Query Hydration Stats", "sectionTitle": "Query Hydration Stats", @@ -9346,7 +9360,7 @@ "kind": "section" }, { - "id": "683-zero-cache-config#query-url", + "id": "684-zero-cache-config#query-url", "title": "zero-cache Config", "searchTitle": "Query URL", "sectionTitle": "Query URL", @@ -9356,7 +9370,7 @@ "kind": "section" }, { - "id": "684-zero-cache-config#replica-file", + "id": "685-zero-cache-config#replica-file", "title": "zero-cache Config", "searchTitle": "Replica File", "sectionTitle": "Replica File", @@ -9366,7 +9380,7 @@ "kind": "section" }, { - "id": "685-zero-cache-config#replica-vacuum-interval-hours", + "id": "686-zero-cache-config#replica-vacuum-interval-hours", "title": "zero-cache Config", "searchTitle": "Replica Vacuum Interval Hours", "sectionTitle": "Replica Vacuum Interval Hours", @@ -9376,7 +9390,7 @@ "kind": "section" }, { - "id": "686-zero-cache-config#replication-lag-initial-readiness-threshold-ms", + "id": "687-zero-cache-config#replication-lag-initial-readiness-threshold-ms", "title": "zero-cache Config", "searchTitle": "Replication Lag Initial Readiness Threshold (ms)", "sectionTitle": "Replication Lag Initial Readiness Threshold (ms)", @@ -9386,7 +9400,7 @@ "kind": "section" }, { - "id": "687-zero-cache-config#replication-lag-max-readiness-delay-ms", + "id": "688-zero-cache-config#replication-lag-max-readiness-delay-ms", "title": "zero-cache Config", "searchTitle": "Replication Lag Max Readiness Delay (ms)", "sectionTitle": "Replication Lag Max Readiness Delay (ms)", @@ -9396,7 +9410,7 @@ "kind": "section" }, { - "id": "688-zero-cache-config#replication-lag-report-interval-ms", + "id": "689-zero-cache-config#replication-lag-report-interval-ms", "title": "zero-cache Config", "searchTitle": "Replication Lag Report Interval (ms)", "sectionTitle": "Replication Lag Report Interval (ms)", @@ -9406,7 +9420,7 @@ "kind": "section" }, { - "id": "689-zero-cache-config#server-version", + "id": "690-zero-cache-config#server-version", "title": "zero-cache Config", "searchTitle": "Server Version", "sectionTitle": "Server Version", @@ -9416,7 +9430,7 @@ "kind": "section" }, { - "id": "690-zero-cache-config#shadow-sync-enabled", + "id": "691-zero-cache-config#shadow-sync-enabled", "title": "zero-cache Config", "searchTitle": "Shadow Sync Enabled", "sectionTitle": "Shadow Sync Enabled", @@ -9426,7 +9440,7 @@ "kind": "section" }, { - "id": "691-zero-cache-config#shadow-sync-interval-hours", + "id": "692-zero-cache-config#shadow-sync-interval-hours", "title": "zero-cache Config", "searchTitle": "Shadow Sync Interval Hours", "sectionTitle": "Shadow Sync Interval Hours", @@ -9436,7 +9450,7 @@ "kind": "section" }, { - "id": "692-zero-cache-config#shadow-sync-sample-rate", + "id": "693-zero-cache-config#shadow-sync-sample-rate", "title": "zero-cache Config", "searchTitle": "Shadow Sync Sample Rate", "sectionTitle": "Shadow Sync Sample Rate", @@ -9446,7 +9460,7 @@ "kind": "section" }, { - "id": "693-zero-cache-config#shadow-sync-max-rows-per-table", + "id": "694-zero-cache-config#shadow-sync-max-rows-per-table", "title": "zero-cache Config", "searchTitle": "Shadow Sync Max Rows Per Table", "sectionTitle": "Shadow Sync Max Rows Per Table", @@ -9456,7 +9470,7 @@ "kind": "section" }, { - "id": "694-zero-cache-config#storage-db-temp-dir", + "id": "695-zero-cache-config#storage-db-temp-dir", "title": "zero-cache Config", "searchTitle": "Storage DB Temp Dir", "sectionTitle": "Storage DB Temp Dir", @@ -9466,7 +9480,7 @@ "kind": "section" }, { - "id": "695-zero-cache-config#task-id", + "id": "696-zero-cache-config#task-id", "title": "zero-cache Config", "searchTitle": "Task ID", "sectionTitle": "Task ID", @@ -9476,7 +9490,7 @@ "kind": "section" }, { - "id": "696-zero-cache-config#upstream-max-connections", + "id": "697-zero-cache-config#upstream-max-connections", "title": "zero-cache Config", "searchTitle": "Upstream Max Connections", "sectionTitle": "Upstream Max Connections", @@ -9486,7 +9500,7 @@ "kind": "section" }, { - "id": "697-zero-cache-config#upstream-pg-replication-slot-failover", + "id": "698-zero-cache-config#upstream-pg-replication-slot-failover", "title": "zero-cache Config", "searchTitle": "Upstream PG Replication Slot Failover", "sectionTitle": "Upstream PG Replication Slot Failover", @@ -9496,7 +9510,7 @@ "kind": "section" }, { - "id": "698-zero-cache-config#upstream-pg-stream-inbound-timeout", + "id": "699-zero-cache-config#upstream-pg-stream-inbound-timeout", "title": "zero-cache Config", "searchTitle": "Upstream PG Stream Inbound Timeout", "sectionTitle": "Upstream PG Stream Inbound Timeout", @@ -9506,7 +9520,7 @@ "kind": "section" }, { - "id": "699-zero-cache-config#view-syncer-hydration-budget-ms", + "id": "700-zero-cache-config#view-syncer-hydration-budget-ms", "title": "zero-cache Config", "searchTitle": "View Syncer Hydration Budget (ms)", "sectionTitle": "View Syncer Hydration Budget (ms)", @@ -9516,7 +9530,7 @@ "kind": "section" }, { - "id": "700-zero-cache-config#view-syncer-query-hydration-timeout-ms", + "id": "701-zero-cache-config#view-syncer-query-hydration-timeout-ms", "title": "zero-cache Config", "searchTitle": "View Syncer Query Hydration Timeout (ms)", "sectionTitle": "View Syncer Query Hydration Timeout (ms)", @@ -9526,7 +9540,7 @@ "kind": "section" }, { - "id": "701-zero-cache-config#websocket-compression", + "id": "702-zero-cache-config#websocket-compression", "title": "zero-cache Config", "searchTitle": "Websocket Compression", "sectionTitle": "Websocket Compression", @@ -9536,7 +9550,7 @@ "kind": "section" }, { - "id": "702-zero-cache-config#websocket-compression-options", + "id": "703-zero-cache-config#websocket-compression-options", "title": "zero-cache Config", "searchTitle": "Websocket Compression Options", "sectionTitle": "Websocket Compression Options", @@ -9546,7 +9560,7 @@ "kind": "section" }, { - "id": "703-zero-cache-config#websocket-max-payload-bytes", + "id": "704-zero-cache-config#websocket-max-payload-bytes", "title": "zero-cache Config", "searchTitle": "Websocket Max Payload Bytes", "sectionTitle": "Websocket Max Payload Bytes", @@ -9556,7 +9570,7 @@ "kind": "section" }, { - "id": "704-zero-cache-config#yield-threshold-ms", + "id": "705-zero-cache-config#yield-threshold-ms", "title": "zero-cache Config", "searchTitle": "Yield Threshold (ms)", "sectionTitle": "Yield Threshold (ms)", @@ -9566,7 +9580,7 @@ "kind": "section" }, { - "id": "705-zero-cache-config#deprecated-flags", + "id": "706-zero-cache-config#deprecated-flags", "title": "zero-cache Config", "searchTitle": "Deprecated Flags", "sectionTitle": "Deprecated Flags", @@ -9576,7 +9590,7 @@ "kind": "section" }, { - "id": "706-zero-cache-config#change-max-connections", + "id": "707-zero-cache-config#change-max-connections", "title": "zero-cache Config", "searchTitle": "Change Max Connections", "sectionTitle": "Change Max Connections", @@ -9586,7 +9600,7 @@ "kind": "section" }, { - "id": "707-zero-cache-config#change-streamer-mode", + "id": "708-zero-cache-config#change-streamer-mode", "title": "zero-cache Config", "searchTitle": "Change Streamer Mode", "sectionTitle": "Change Streamer Mode", @@ -9596,7 +9610,7 @@ "kind": "section" }, { - "id": "708-zero-cache-config#litestream-minimum-checkpoint-page-count", + "id": "709-zero-cache-config#litestream-minimum-checkpoint-page-count", "title": "zero-cache Config", "searchTitle": "Litestream Minimum Checkpoint Page Count", "sectionTitle": "Litestream Minimum Checkpoint Page Count", @@ -9606,7 +9620,7 @@ "kind": "section" }, { - "id": "709-zero-cache-config#auth-jwk", + "id": "710-zero-cache-config#auth-jwk", "title": "zero-cache Config", "searchTitle": "Auth JWK", "sectionTitle": "Auth JWK", @@ -9616,7 +9630,7 @@ "kind": "section" }, { - "id": "710-zero-cache-config#auth-jwks-url", + "id": "711-zero-cache-config#auth-jwks-url", "title": "zero-cache Config", "searchTitle": "Auth JWKS URL", "sectionTitle": "Auth JWKS URL", @@ -9626,7 +9640,7 @@ "kind": "section" }, { - "id": "711-zero-cache-config#auth-secret", + "id": "712-zero-cache-config#auth-secret", "title": "zero-cache Config", "searchTitle": "Auth Secret", "sectionTitle": "Auth Secret", @@ -9746,7 +9760,7 @@ "kind": "page" }, { - "id": "712-zql#create-a-builder", + "id": "713-zql#create-a-builder", "title": "ZQL", "searchTitle": "Create a Builder", "sectionTitle": "Create a Builder", @@ -9756,7 +9770,7 @@ "kind": "section" }, { - "id": "713-zql#select", + "id": "714-zql#select", "title": "ZQL", "searchTitle": "Select", "sectionTitle": "Select", @@ -9766,7 +9780,7 @@ "kind": "section" }, { - "id": "714-zql#ordering", + "id": "715-zql#ordering", "title": "ZQL", "searchTitle": "Ordering", "sectionTitle": "Ordering", @@ -9776,7 +9790,7 @@ "kind": "section" }, { - "id": "715-zql#limit", + "id": "716-zql#limit", "title": "ZQL", "searchTitle": "Limit", "sectionTitle": "Limit", @@ -9786,7 +9800,7 @@ "kind": "section" }, { - "id": "716-zql#paging", + "id": "717-zql#paging", "title": "ZQL", "searchTitle": "Paging", "sectionTitle": "Paging", @@ -9796,7 +9810,7 @@ "kind": "section" }, { - "id": "717-zql#getting-a-single-result", + "id": "718-zql#getting-a-single-result", "title": "ZQL", "searchTitle": "Getting a Single Result", "sectionTitle": "Getting a Single Result", @@ -9806,7 +9820,7 @@ "kind": "section" }, { - "id": "718-zql#relationships", + "id": "719-zql#relationships", "title": "ZQL", "searchTitle": "Relationships", "sectionTitle": "Relationships", @@ -9816,7 +9830,7 @@ "kind": "section" }, { - "id": "719-zql#refining-relationships", + "id": "720-zql#refining-relationships", "title": "ZQL", "searchTitle": "Refining Relationships", "sectionTitle": "Refining Relationships", @@ -9826,7 +9840,7 @@ "kind": "section" }, { - "id": "720-zql#nested-relationships", + "id": "721-zql#nested-relationships", "title": "ZQL", "searchTitle": "Nested Relationships", "sectionTitle": "Nested Relationships", @@ -9836,7 +9850,7 @@ "kind": "section" }, { - "id": "721-zql#where", + "id": "722-zql#where", "title": "ZQL", "searchTitle": "Where", "sectionTitle": "Where", @@ -9846,7 +9860,7 @@ "kind": "section" }, { - "id": "722-zql#comparison-operators", + "id": "723-zql#comparison-operators", "title": "ZQL", "searchTitle": "Comparison Operators", "sectionTitle": "Comparison Operators", @@ -9856,7 +9870,7 @@ "kind": "section" }, { - "id": "723-zql#equals-is-the-default-comparison-operator", + "id": "724-zql#equals-is-the-default-comparison-operator", "title": "ZQL", "searchTitle": "Equals is the Default Comparison Operator", "sectionTitle": "Equals is the Default Comparison Operator", @@ -9866,7 +9880,7 @@ "kind": "section" }, { - "id": "724-zql#comparing-to-null", + "id": "725-zql#comparing-to-null", "title": "ZQL", "searchTitle": "Comparing to null", "sectionTitle": "Comparing to null", @@ -9876,7 +9890,7 @@ "kind": "section" }, { - "id": "725-zql#comparing-to-undefined", + "id": "726-zql#comparing-to-undefined", "title": "ZQL", "searchTitle": "Comparing to undefined", "sectionTitle": "Comparing to undefined", @@ -9886,7 +9900,7 @@ "kind": "section" }, { - "id": "726-zql#compound-filters", + "id": "727-zql#compound-filters", "title": "ZQL", "searchTitle": "Compound Filters", "sectionTitle": "Compound Filters", @@ -9896,7 +9910,7 @@ "kind": "section" }, { - "id": "727-zql#comparing-literal-values", + "id": "728-zql#comparing-literal-values", "title": "ZQL", "searchTitle": "Comparing Literal Values", "sectionTitle": "Comparing Literal Values", @@ -9906,7 +9920,7 @@ "kind": "section" }, { - "id": "728-zql#relationship-filters", + "id": "729-zql#relationship-filters", "title": "ZQL", "searchTitle": "Relationship Filters", "sectionTitle": "Relationship Filters", @@ -9916,7 +9930,7 @@ "kind": "section" }, { - "id": "729-zql#type-helpers", + "id": "730-zql#type-helpers", "title": "ZQL", "searchTitle": "Type Helpers", "sectionTitle": "Type Helpers", @@ -9926,7 +9940,7 @@ "kind": "section" }, { - "id": "730-zql#planning", + "id": "731-zql#planning", "title": "ZQL", "searchTitle": "Planning", "sectionTitle": "Planning", @@ -9936,7 +9950,7 @@ "kind": "section" }, { - "id": "731-zql#inspecting-query-plans", + "id": "732-zql#inspecting-query-plans", "title": "ZQL", "searchTitle": "Inspecting Query Plans", "sectionTitle": "Inspecting Query Plans", @@ -9946,7 +9960,7 @@ "kind": "section" }, { - "id": "732-zql#manually-flipping-joins", + "id": "733-zql#manually-flipping-joins", "title": "ZQL", "searchTitle": "Manually Flipping Joins", "sectionTitle": "Manually Flipping Joins", @@ -9956,7 +9970,7 @@ "kind": "section" }, { - "id": "733-zql#scalar-subqueries", + "id": "734-zql#scalar-subqueries", "title": "ZQL", "searchTitle": "Scalar Subqueries", "sectionTitle": "Scalar Subqueries", @@ -9966,7 +9980,7 @@ "kind": "section" }, { - "id": "734-zql#why-it-matters", + "id": "735-zql#why-it-matters", "title": "ZQL", "searchTitle": "Why It Matters", "sectionTitle": "Why It Matters", @@ -9976,7 +9990,7 @@ "kind": "section" }, { - "id": "735-zql#trade-offs", + "id": "736-zql#trade-offs", "title": "ZQL", "searchTitle": "Trade-offs", "sectionTitle": "Trade-offs", @@ -9986,7 +10000,7 @@ "kind": "section" }, { - "id": "736-zql#future-work", + "id": "737-zql#future-work", "title": "ZQL", "searchTitle": "Future Work", "sectionTitle": "Future Work", From 1f223e5fdb899f0b24aec239ed63caf04f139101 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Thu, 15 Oct 2026 07:33:03 +0000 Subject: [PATCH 11/20] docs(release-notes): plainer wording for the 1.11 notes Fixes now describe the bug, as in earlier notes, instead of repeating "no longer". Shorter sentences, no colon lead-ins. Same facts, links and credits. --- assets/search-index.json | 42 ++++----- contents/docs/release-notes/1.11.mdx | 130 +++++++++++++-------------- 2 files changed, 86 insertions(+), 86 deletions(-) diff --git a/assets/search-index.json b/assets/search-index.json index 8600e506..667a4e8d 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -5687,7 +5687,7 @@ "title": "Zero 1.11", "searchTitle": "Zero 1.11", "url": "/docs/release-notes/1.11", - "content": "Installation npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Overview Zero 1.11 shows cached query results sooner, recovers from client storage failures, and makes Litestream v5 the default for backups. It also speeds up queries on the client and server and fixes a long list of correctness and reliability issues. Features Queries whose results a previous session stored locally now start with the new 'cached' result type, so apps can show stored data right away and still tell it apart from data confirmed by the server. (#6578) (thanks @pawarren!) Building the same query again usually returns the same Query object, so useQuery no longer rebuilds and rehashes its query on every render, and queries built inline can be used as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero now starts in a new 'initializing' connection state while it loads local data. Slow local loading no longer counts against the connect timeout. (#6561) When local storage fails to open, fills up, or reports I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore reports which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Upgrading from 1.10 keeps each client's local data and its pending mutations. Future protocol changes no longer start clients over with an empty local database. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when they fail with an error that tx.retryOn() accepts, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. Replicas restored from another replica's backup continue it incrementally, periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits until replication has caught up before reporting ready. (1c5ddcdb4) Inactive client groups are now kept for 7 days instead of 48 hours, so clients that return within a week avoid a full resync. (#6484) New tools for diagnosing slow queries: zero-cache logs which queries are slow and why, with index suggestions, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) Opt-in limits protect zero-cache from expensive queries: a hydration budget for reconnecting clients and a per-query hydration timeout. (#6477, #6499) New OpenTelemetry metrics for WAL usage, index-build progress in replication status events, and restore progress in a new view-syncer status event. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image shrank from about 1.09 GB to 807 MB. (#6695) Performance Client Cold Start Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache brings every client group's queries up to date with it. Zero 1.11 does this about 2.5x faster: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU. React Native On React Native, Zero's query engine is about 1.6x faster in Zero 1.11. Across 56 query engine benchmarks on Hermes, the geometric mean speedup is 1.63x and 48 benchmarks are more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components build their queries on every render, and Zero hashes each query to identify it. Building the same query again usually returns the same object, with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size. Slower in Zero 1.11: Client Writes With Many Joined Queries One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped (#6645, #6708). When every change reaches some parent, as here, keeping that index up to date is extra work. Fixes Query Results Queries using or with a flipped exists no longer return wrong rows. (#6636) Joins no longer leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery are now synced, and changes to them propagate. (913c0665f) Catching up a client after a large update no longer fails with Missing row, and a schema change during rehydration no longer fails with table has no column named. (#6662, #6616) Queries whose SQLite plan nests one multi-index OR inside another no longer crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache no longer calls ZERO_QUERY_URL again for unchanged custom queries when it rebuilds its query pipelines. Permission changes still re-transform every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments no longer share one view. A SolidJS view rebuilt after its query changes uses the current TTL. (#6461, #6497) In mutators, a row just inserted without its optional columns now reads back with those columns as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) no longer return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients no longer log ChunkNotFoundError during persist, and a write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database now deletes the right file. (#6656) (thanks @pawarren!) Clients no longer go offline after the app resumes from the background or the computer wakes from sleep, and React Native connect timeouts retry instead of leaving the client stuck in an error state. (#6510, #6485) (thanks @pawarren!) Slow local startup no longer makes a healthy server look unreachable. (#6475) A mutator that throws while being replayed no longer disconnects the client in a loop. The mutation is still sent to the server. (#6555) Client memory no longer grows with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing are logged as warnings or info instead of errors, and fatal errors now include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability Fixed memory and resource leaks in zero-cache around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) Fixed view-syncer crashes and Internal errors when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg no longer crash when Postgres closes a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector no longer runs a view-syncer out of memory on large tables. (#6596) Error messages no longer include row values, query literals or mutation arguments. (#6413) Replication, Backfills, and Backups Replication no longer rolls back or stops after an interrupted stream, and a replication-manager whose slot was invalidated now resyncs instead of crash-looping. (#6562, #6590) Backfills no longer apply rows before replication has caught up to them, resume when a view-syncer moves to another replication-manager, and no longer leak a Postgres connection that holds back vacuum. (#6685, #6684, #6513) Replication slots are more robust: creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag is now measured correctly on Aurora. (e6a2590db) After initial sync, the replication-manager waits for a backup that covers its replica before serving, so restoring view-syncers no longer fall back to Postgres. (#6465) Fewer unnecessary query pipeline resets, and dropping an upstream index now refreshes table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager keeps serving until the new one takes over. (21f349135) Change-database connections no longer leak after a failed catch-up, and a change-database reset no longer hangs. (#6529, #6632) Litestream's upload concurrency now scales with available memory, avoiding out-of-memory crashes in small containers. (240c05730) Configuration values that start with a dash, such as some shard IDs, are now parsed correctly. (81e6ac570) In the zero Docker image, initial sync and backfills no longer leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe) Breaking Changes Legacy queries: zero-cache no longer accepts queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: code that switches over every result type or connection state must handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options, which no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours instead of 48, replication lag is reported every 10 seconds instead of 30, and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 instead of 2. Startup can take longer while zero-cache waits for a backup and for replication to catch up; give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: mutator return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates for tx.retryOn() to work. Using tx after its transaction ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL no longer apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they had not yet sent are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Mutations that were not yet sent are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after an upgrade, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE; use ZERO_CHANGE_STREAMER_URI. (698286b55) Events: a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", + "content": "Installation npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Overview Zero 1.11 can show cached query results right away, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication. Features Queries can now start with results that a previous session saved on the device. These come back with the new 'cached' result type, so you can show them right away and still tell them apart from data the server has confirmed. (#6578) (thanks @pawarren!) Building the same query again usually gives you the same Query object. useQuery no longer rebuilds and rehashes its query on every render, and queries built inline work as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero starts in a new 'initializing' connection state while it loads local data, and slow loading doesn't count against the connect timeout anymore. (#6561) If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore tells you which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Clients upgrading from 1.10 keep their local data and pending mutations. Future protocol changes won't reset clients to an empty local database either. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when tx.retryOn() accepts the error, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. A replica restored from another replica's backup continues that backup incrementally. Periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits for replication to catch up before it reports ready. (1c5ddcdb4) Inactive client groups are kept for 7 days instead of 48 hours, so users who come back within a week don't need a full resync. (#6484) zero-cache now logs slow queries with the reason and suggested indexes, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) A new hydration budget caps the time a view-syncer spends re-hydrating a reconnecting client's inactive queries, and a per-query hydration timeout aborts any query that takes too long to hydrate. Both are off by default. (#6477, #6499) New OpenTelemetry metrics track WAL usage. Replication status events now include index-build progress, and a new view-syncer status event reports restore progress. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image is down to 807 MB from about 1.09 GB. (#6695) Performance Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x **less CPU**. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.6x faster on React Native. That's the geometric mean of 56 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead. Fixes Query Results Queries using or with a flipped exists could return wrong rows. (#6636) Joins could leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery weren't synced, and changes to them didn't propagate. (913c0665f) Catching up a client after a large update could fail with Missing row, and a schema change during rehydration could fail with table has no column named. (#6662, #6616) A SQLite plan that nests one multi-index OR inside another could crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache called ZERO_QUERY_URL again for unchanged custom queries whenever it rebuilt its query pipelines. A permission change still re-transforms every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments could share one view, and a SolidJS view rebuilt after its query changed kept the old TTL. (#6461, #6497) In mutators, a row inserted without its optional columns didn't read those columns back as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) could return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients logged ChunkNotFoundError during persist. A write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database didn't delete the right file. (#6656) (thanks @pawarren!) Clients could go offline after the app resumed from the background or the computer woke from sleep, and React Native connect timeouts left the client stuck in an error state instead of retrying. (#6510, #6485) (thanks @pawarren!) Slow local startup could make a healthy server look unreachable. (#6475) A mutator that threw while being replayed disconnected the client in a loop. The client now stays connected, and the mutation is still sent to the server. (#6555) Client memory grew with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil didn't suspend until the first rows arrived, contrary to the docs (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing were logged as errors. They're now warnings or info, and fatal errors include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability zero-cache leaked memory and other resources around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) View-syncers could crash or return Internal error when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg crashed when Postgres closed a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector could run a view-syncer out of memory on large tables. (#6596) Error messages could include row values, query literals and mutation arguments. (#6413) Replication, Backfills, and Backups Replication could roll back or stop after an interrupted stream, and a replication-manager whose slot was invalidated crash-looped instead of resyncing. (#6562, #6590) Backfills could apply rows before replication had caught up to them, didn't resume when a view-syncer moved to another replication-manager, and leaked a Postgres connection that held back vacuum. (#6685, #6684, #6513) Replication slots are more robust. Slot creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag was measured wrong on Aurora. (e6a2590db) After initial sync, the replication-manager could serve before a backup covered its replica, so restoring view-syncers fell back to Postgres. It now waits for that backup. (#6465) Query pipelines were reset more often than needed, and dropping an upstream index didn't refresh table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers now fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager now keeps serving until the new one takes over. (21f349135) Change-database connections leaked after a failed catch-up, and a change-database reset could hang. (#6529, #6632) Litestream could run out of memory in small containers. Its upload concurrency now scales with available memory. (240c05730) Configuration values that start with a dash, such as some shard IDs, were parsed wrong. (81e6ac570) In the zero Docker image, initial sync and backfills could leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe) Breaking Changes Legacy queries: zero-cache now rejects queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: if your code switches over every result type or connection state, handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options; they no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours (was 48), replication lag is reported every 10 seconds (was 30), and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 (was 2). Startup can take longer while zero-cache waits for a backup and for replication to catch up, so give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates, or tx.retryOn() won't work. Using tx after its transaction has ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL don't apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they hadn't sent yet are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Unsent mutations are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after upgrading, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE. Use ZERO_CHANGE_STREAMER_URI instead. (698286b55) Events: there's a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", "headings": [ { "text": "Installation", @@ -5722,8 +5722,8 @@ "id": "building-and-hashing-queries" }, { - "text": "Slower in Zero 1.11: Client Writes With Many Joined Queries", - "id": "slower-in-zero-111-client-writes-with-many-joined-queries" + "text": "Client Writes With Many Joined Queries Got Slower", + "id": "client-writes-with-many-joined-queries-got-slower" }, { "text": "Fixes", @@ -5773,7 +5773,7 @@ "sectionTitle": "Overview", "sectionId": "overview", "url": "/docs/release-notes/1.11", - "content": "Zero 1.11 shows cached query results sooner, recovers from client storage failures, and makes Litestream v5 the default for backups. It also speeds up queries on the client and server and fixes a long list of correctness and reliability issues.", + "content": "Zero 1.11 can show cached query results right away, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication.", "kind": "section" }, { @@ -5783,7 +5783,7 @@ "sectionTitle": "Features", "sectionId": "features", "url": "/docs/release-notes/1.11", - "content": "Queries whose results a previous session stored locally now start with the new 'cached' result type, so apps can show stored data right away and still tell it apart from data confirmed by the server. (#6578) (thanks @pawarren!) Building the same query again usually returns the same Query object, so useQuery no longer rebuilds and rehashes its query on every render, and queries built inline can be used as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero now starts in a new 'initializing' connection state while it loads local data. Slow local loading no longer counts against the connect timeout. (#6561) When local storage fails to open, fills up, or reports I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore reports which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Upgrading from 1.10 keeps each client's local data and its pending mutations. Future protocol changes no longer start clients over with an empty local database. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when they fail with an error that tx.retryOn() accepts, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. Replicas restored from another replica's backup continue it incrementally, periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits until replication has caught up before reporting ready. (1c5ddcdb4) Inactive client groups are now kept for 7 days instead of 48 hours, so clients that return within a week avoid a full resync. (#6484) New tools for diagnosing slow queries: zero-cache logs which queries are slow and why, with index suggestions, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) Opt-in limits protect zero-cache from expensive queries: a hydration budget for reconnecting clients and a per-query hydration timeout. (#6477, #6499) New OpenTelemetry metrics for WAL usage, index-build progress in replication status events, and restore progress in a new view-syncer status event. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image shrank from about 1.09 GB to 807 MB. (#6695)", + "content": "Queries can now start with results that a previous session saved on the device. These come back with the new 'cached' result type, so you can show them right away and still tell them apart from data the server has confirmed. (#6578) (thanks @pawarren!) Building the same query again usually gives you the same Query object. useQuery no longer rebuilds and rehashes its query on every render, and queries built inline work as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero starts in a new 'initializing' connection state while it loads local data, and slow loading doesn't count against the connect timeout anymore. (#6561) If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore tells you which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Clients upgrading from 1.10 keep their local data and pending mutations. Future protocol changes won't reset clients to an empty local database either. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when tx.retryOn() accepts the error, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. A replica restored from another replica's backup continues that backup incrementally. Periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits for replication to catch up before it reports ready. (1c5ddcdb4) Inactive client groups are kept for 7 days instead of 48 hours, so users who come back within a week don't need a full resync. (#6484) zero-cache now logs slow queries with the reason and suggested indexes, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) A new hydration budget caps the time a view-syncer spends re-hydrating a reconnecting client's inactive queries, and a per-query hydration timeout aborts any query that takes too long to hydrate. Both are off by default. (#6477, #6499) New OpenTelemetry metrics track WAL usage. Replication status events now include index-build progress, and a new view-syncer status event reports restore progress. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image is down to 807 MB from about 1.09 GB. (#6695)", "kind": "section" }, { @@ -5793,7 +5793,7 @@ "sectionTitle": "Performance", "sectionId": "performance", "url": "/docs/release-notes/1.11", - "content": "Client Cold Start Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache brings every client group's queries up to date with it. Zero 1.11 does this about 2.5x faster: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU. React Native On React Native, Zero's query engine is about 1.6x faster in Zero 1.11. Across 56 query engine benchmarks on Hermes, the geometric mean speedup is 1.63x and 48 benchmarks are more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components build their queries on every render, and Zero hashes each query to identify it. Building the same query again usually returns the same object, with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size. Slower in Zero 1.11: Client Writes With Many Joined Queries One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped (#6645, #6708). When every change reaches some parent, as here, keeping that index up to date is extra work.", + "content": "Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x **less CPU**. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.6x faster on React Native. That's the geometric mean of 56 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead.", "kind": "section" }, { @@ -5803,7 +5803,7 @@ "sectionTitle": "Client Cold Start", "sectionId": "client-cold-start", "url": "/docs/release-notes/1.11", - "content": "Apps usually register their queries before Zero has loaded local data, for example when useQuery runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native.", + "content": "Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native.", "kind": "section" }, { @@ -5813,7 +5813,7 @@ "sectionTitle": "Server-Side Incremental Updates", "sectionId": "server-side-incremental-updates", "url": "/docs/release-notes/1.11", - "content": "When a transaction replicates, zero-cache brings every client group's queries up to date with it. Zero 1.11 does this about 2.5x faster: with 100 client groups on one sync worker, each syncing a query with a related subquery, advancing through a transaction takes about 2.5x less CPU.", + "content": "When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x **less CPU**. We measured 100 client groups on one sync worker, each syncing a query with a related subquery.", "kind": "section" }, { @@ -5823,7 +5823,7 @@ "sectionTitle": "React Native", "sectionId": "react-native", "url": "/docs/release-notes/1.11", - "content": "On React Native, Zero's query engine is about 1.6x faster in Zero 1.11. Across 56 query engine benchmarks on Hermes, the geometric mean speedup is 1.63x and 48 benchmarks are more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster.", + "content": "Zero's query engine is about 1.6x faster on React Native. That's the geometric mean of 56 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster.", "kind": "section" }, { @@ -5833,17 +5833,17 @@ "sectionTitle": "Building and Hashing Queries", "sectionId": "building-and-hashing-queries", "url": "/docs/release-notes/1.11", - "content": "Components build their queries on every render, and Zero hashes each query to identify it. Building the same query again usually returns the same object, with its hash already computed (#6469), so building and hashing a query is about 7x to 17x faster, depending on its size.", + "content": "Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size.", "kind": "section" }, { - "id": "454-release-notes/1.11#slower-in-zero-111-client-writes-with-many-joined-queries", + "id": "454-release-notes/1.11#client-writes-with-many-joined-queries-got-slower", "title": "Zero 1.11", - "searchTitle": "Slower in Zero 1.11: Client Writes With Many Joined Queries", - "sectionTitle": "Slower in Zero 1.11: Client Writes With Many Joined Queries", - "sectionId": "slower-in-zero-111-client-writes-with-many-joined-queries", + "searchTitle": "Client Writes With Many Joined Queries Got Slower", + "sectionTitle": "Client Writes With Many Joined Queries Got Slower", + "sectionId": "client-writes-with-many-joined-queries-got-slower", "url": "/docs/release-notes/1.11", - "content": "One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped (#6645, #6708). When every change reaches some parent, as here, keeping that index up to date is extra work.", + "content": "This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead.", "kind": "section" }, { @@ -5853,7 +5853,7 @@ "sectionTitle": "Fixes", "sectionId": "fixes", "url": "/docs/release-notes/1.11", - "content": "Query Results Queries using or with a flipped exists no longer return wrong rows. (#6636) Joins no longer leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery are now synced, and changes to them propagate. (913c0665f) Catching up a client after a large update no longer fails with Missing row, and a schema change during rehydration no longer fails with table has no column named. (#6662, #6616) Queries whose SQLite plan nests one multi-index OR inside another no longer crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache no longer calls ZERO_QUERY_URL again for unchanged custom queries when it rebuilds its query pipelines. Permission changes still re-transform every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments no longer share one view. A SolidJS view rebuilt after its query changes uses the current TTL. (#6461, #6497) In mutators, a row just inserted without its optional columns now reads back with those columns as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) no longer return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients no longer log ChunkNotFoundError during persist, and a write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database now deletes the right file. (#6656) (thanks @pawarren!) Clients no longer go offline after the app resumes from the background or the computer wakes from sleep, and React Native connect timeouts retry instead of leaving the client stuck in an error state. (#6510, #6485) (thanks @pawarren!) Slow local startup no longer makes a healthy server look unreachable. (#6475) A mutator that throws while being replayed no longer disconnects the client in a loop. The mutation is still sent to the server. (#6555) Client memory no longer grows with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing are logged as warnings or info instead of errors, and fatal errors now include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability Fixed memory and resource leaks in zero-cache around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) Fixed view-syncer crashes and Internal errors when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg no longer crash when Postgres closes a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector no longer runs a view-syncer out of memory on large tables. (#6596) Error messages no longer include row values, query literals or mutation arguments. (#6413) Replication, Backfills, and Backups Replication no longer rolls back or stops after an interrupted stream, and a replication-manager whose slot was invalidated now resyncs instead of crash-looping. (#6562, #6590) Backfills no longer apply rows before replication has caught up to them, resume when a view-syncer moves to another replication-manager, and no longer leak a Postgres connection that holds back vacuum. (#6685, #6684, #6513) Replication slots are more robust: creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag is now measured correctly on Aurora. (e6a2590db) After initial sync, the replication-manager waits for a backup that covers its replica before serving, so restoring view-syncers no longer fall back to Postgres. (#6465) Fewer unnecessary query pipeline resets, and dropping an upstream index now refreshes table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager keeps serving until the new one takes over. (21f349135) Change-database connections no longer leak after a failed catch-up, and a change-database reset no longer hangs. (#6529, #6632) Litestream's upload concurrency now scales with available memory, avoiding out-of-memory crashes in small containers. (240c05730) Configuration values that start with a dash, such as some shard IDs, are now parsed correctly. (81e6ac570) In the zero Docker image, initial sync and backfills no longer leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe)", + "content": "Query Results Queries using or with a flipped exists could return wrong rows. (#6636) Joins could leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery weren't synced, and changes to them didn't propagate. (913c0665f) Catching up a client after a large update could fail with Missing row, and a schema change during rehydration could fail with table has no column named. (#6662, #6616) A SQLite plan that nests one multi-index OR inside another could crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache called ZERO_QUERY_URL again for unchanged custom queries whenever it rebuilt its query pipelines. A permission change still re-transforms every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments could share one view, and a SolidJS view rebuilt after its query changed kept the old TTL. (#6461, #6497) In mutators, a row inserted without its optional columns didn't read those columns back as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) could return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients logged ChunkNotFoundError during persist. A write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database didn't delete the right file. (#6656) (thanks @pawarren!) Clients could go offline after the app resumed from the background or the computer woke from sleep, and React Native connect timeouts left the client stuck in an error state instead of retrying. (#6510, #6485) (thanks @pawarren!) Slow local startup could make a healthy server look unreachable. (#6475) A mutator that threw while being replayed disconnected the client in a loop. The client now stays connected, and the mutation is still sent to the server. (#6555) Client memory grew with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil didn't suspend until the first rows arrived, contrary to the docs (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing were logged as errors. They're now warnings or info, and fatal errors include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability zero-cache leaked memory and other resources around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) View-syncers could crash or return Internal error when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg crashed when Postgres closed a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector could run a view-syncer out of memory on large tables. (#6596) Error messages could include row values, query literals and mutation arguments. (#6413) Replication, Backfills, and Backups Replication could roll back or stop after an interrupted stream, and a replication-manager whose slot was invalidated crash-looped instead of resyncing. (#6562, #6590) Backfills could apply rows before replication had caught up to them, didn't resume when a view-syncer moved to another replication-manager, and leaked a Postgres connection that held back vacuum. (#6685, #6684, #6513) Replication slots are more robust. Slot creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag was measured wrong on Aurora. (e6a2590db) After initial sync, the replication-manager could serve before a backup covered its replica, so restoring view-syncers fell back to Postgres. It now waits for that backup. (#6465) Query pipelines were reset more often than needed, and dropping an upstream index didn't refresh table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers now fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager now keeps serving until the new one takes over. (21f349135) Change-database connections leaked after a failed catch-up, and a change-database reset could hang. (#6529, #6632) Litestream could run out of memory in small containers. Its upload concurrency now scales with available memory. (240c05730) Configuration values that start with a dash, such as some shard IDs, were parsed wrong. (81e6ac570) In the zero Docker image, initial sync and backfills could leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe)", "kind": "section" }, { @@ -5863,7 +5863,7 @@ "sectionTitle": "Query Results", "sectionId": "query-results", "url": "/docs/release-notes/1.11", - "content": "Queries using or with a flipped exists no longer return wrong rows. (#6636) Joins no longer leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery are now synced, and changes to them propagate. (913c0665f) Catching up a client after a large update no longer fails with Missing row, and a schema change during rehydration no longer fails with table has no column named. (#6662, #6616) Queries whose SQLite plan nests one multi-index OR inside another no longer crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache no longer calls ZERO_QUERY_URL again for unchanged custom queries when it rebuilds its query pipelines. Permission changes still re-transform every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments no longer share one view. A SolidJS view rebuilt after its query changes uses the current TTL. (#6461, #6497) In mutators, a row just inserted without its optional columns now reads back with those columns as null. (d4816752c)", + "content": "Queries using or with a flipped exists could return wrong rows. (#6636) Joins could leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery weren't synced, and changes to them didn't propagate. (913c0665f) Catching up a client after a large update could fail with Missing row, and a schema change during rehydration could fail with table has no column named. (#6662, #6616) A SQLite plan that nests one multi-index OR inside another could crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache called ZERO_QUERY_URL again for unchanged custom queries whenever it rebuilt its query pipelines. A permission change still re-transforms every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments could share one view, and a SolidJS view rebuilt after its query changed kept the old TTL. (#6461, #6497) In mutators, a row inserted without its optional columns didn't read those columns back as null. (d4816752c)", "kind": "section" }, { @@ -5873,7 +5873,7 @@ "sectionTitle": "Clients and React Native", "sectionId": "clients-and-react-native", "url": "/docs/release-notes/1.11", - "content": "SQLite-backed stores (expo-sqlite, op-sqlite) no longer return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients no longer log ChunkNotFoundError during persist, and a write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database now deletes the right file. (#6656) (thanks @pawarren!) Clients no longer go offline after the app resumes from the background or the computer wakes from sleep, and React Native connect timeouts retry instead of leaving the client stuck in an error state. (#6510, #6485) (thanks @pawarren!) Slow local startup no longer makes a healthy server look unreachable. (#6475) A mutator that throws while being replayed no longer disconnects the client in a loop. The mutation is still sent to the server. (#6555) Client memory no longer grows with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing are logged as warnings or info instead of errors, and fatal errors now include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!)", + "content": "SQLite-backed stores (expo-sqlite, op-sqlite) could return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients logged ChunkNotFoundError during persist. A write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database didn't delete the right file. (#6656) (thanks @pawarren!) Clients could go offline after the app resumed from the background or the computer woke from sleep, and React Native connect timeouts left the client stuck in an error state instead of retrying. (#6510, #6485) (thanks @pawarren!) Slow local startup could make a healthy server look unreachable. (#6475) A mutator that threw while being replayed disconnected the client in a loop. The client now stays connected, and the mutation is still sent to the server. (#6555) Client memory grew with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil didn't suspend until the first rows arrived, contrary to the docs (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing were logged as errors. They're now warnings or info, and fatal errors include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!)", "kind": "section" }, { @@ -5883,7 +5883,7 @@ "sectionTitle": "zero-cache Reliability", "sectionId": "zero-cache-reliability", "url": "/docs/release-notes/1.11", - "content": "Fixed memory and resource leaks in zero-cache around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) Fixed view-syncer crashes and Internal errors when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg no longer crash when Postgres closes a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector no longer runs a view-syncer out of memory on large tables. (#6596) Error messages no longer include row values, query literals or mutation arguments. (#6413)", + "content": "zero-cache leaked memory and other resources around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) View-syncers could crash or return Internal error when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg crashed when Postgres closed a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector could run a view-syncer out of memory on large tables. (#6596) Error messages could include row values, query literals and mutation arguments. (#6413)", "kind": "section" }, { @@ -5893,7 +5893,7 @@ "sectionTitle": "Replication, Backfills, and Backups", "sectionId": "replication-backfills-and-backups", "url": "/docs/release-notes/1.11", - "content": "Replication no longer rolls back or stops after an interrupted stream, and a replication-manager whose slot was invalidated now resyncs instead of crash-looping. (#6562, #6590) Backfills no longer apply rows before replication has caught up to them, resume when a view-syncer moves to another replication-manager, and no longer leak a Postgres connection that holds back vacuum. (#6685, #6684, #6513) Replication slots are more robust: creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag is now measured correctly on Aurora. (e6a2590db) After initial sync, the replication-manager waits for a backup that covers its replica before serving, so restoring view-syncers no longer fall back to Postgres. (#6465) Fewer unnecessary query pipeline resets, and dropping an upstream index now refreshes table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager keeps serving until the new one takes over. (21f349135) Change-database connections no longer leak after a failed catch-up, and a change-database reset no longer hangs. (#6529, #6632) Litestream's upload concurrency now scales with available memory, avoiding out-of-memory crashes in small containers. (240c05730) Configuration values that start with a dash, such as some shard IDs, are now parsed correctly. (81e6ac570) In the zero Docker image, initial sync and backfills no longer leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e)", + "content": "Replication could roll back or stop after an interrupted stream, and a replication-manager whose slot was invalidated crash-looped instead of resyncing. (#6562, #6590) Backfills could apply rows before replication had caught up to them, didn't resume when a view-syncer moved to another replication-manager, and leaked a Postgres connection that held back vacuum. (#6685, #6684, #6513) Replication slots are more robust. Slot creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag was measured wrong on Aurora. (e6a2590db) After initial sync, the replication-manager could serve before a backup covered its replica, so restoring view-syncers fell back to Postgres. It now waits for that backup. (#6465) Query pipelines were reset more often than needed, and dropping an upstream index didn't refresh table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers now fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager now keeps serving until the new one takes over. (21f349135) Change-database connections leaked after a failed catch-up, and a change-database reset could hang. (#6529, #6632) Litestream could run out of memory in small containers. Its upload concurrency now scales with available memory. (240c05730) Configuration values that start with a dash, such as some shard IDs, were parsed wrong. (81e6ac570) In the zero Docker image, initial sync and backfills could leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e)", "kind": "section" }, { @@ -5913,7 +5913,7 @@ "sectionTitle": "Breaking Changes", "sectionId": "breaking-changes", "url": "/docs/release-notes/1.11", - "content": "Legacy queries: zero-cache no longer accepts queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: code that switches over every result type or connection state must handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options, which no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours instead of 48, replication lag is reported every 10 seconds instead of 30, and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 instead of 2. Startup can take longer while zero-cache waits for a backup and for replication to catch up; give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: mutator return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates for tx.retryOn() to work. Using tx after its transaction ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL no longer apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they had not yet sent are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Mutations that were not yet sent are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after an upgrade, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE; use ZERO_CHANGE_STREAMER_URI. (698286b55) Events: a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", + "content": "Legacy queries: zero-cache now rejects queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: if your code switches over every result type or connection state, handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options; they no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours (was 48), replication lag is reported every 10 seconds (was 30), and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 (was 2). Startup can take longer while zero-cache waits for a backup and for replication to catch up, so give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates, or tx.retryOn() won't work. Using tx after its transaction has ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL don't apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they hadn't sent yet are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Unsent mutations are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after upgrading, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE. Use ZERO_CHANGE_STREAMER_URI instead. (698286b55) Events: there's a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", "kind": "section" }, { diff --git a/contents/docs/release-notes/1.11.mdx b/contents/docs/release-notes/1.11.mdx index a08be92e..e0372695 100644 --- a/contents/docs/release-notes/1.11.mdx +++ b/contents/docs/release-notes/1.11.mdx @@ -20,30 +20,30 @@ npm install @rocicorp/zero@1.11 ## Overview -Zero 1.11 shows cached query results sooner, recovers from client storage failures, and makes Litestream v5 the default for backups. It also speeds up queries on the client and server and fixes a long list of correctness and reliability issues. +Zero 1.11 can show cached query results right away, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication. ## Features -- Queries whose results a previous session stored locally now start with the new [`'cached'` result type](/docs/queries#cached-results), so apps can show stored data right away and still tell it apart from data confirmed by the server. ([#6578](https://github.com/rocicorp/mono/pull/6578)) (thanks [@pawarren](https://github.com/pawarren)!) -- Building the same query again usually returns the same `Query` object, so `useQuery` no longer rebuilds and rehashes its query on every render, and queries built inline can be used as [React dependencies](/docs/react#query-identity). ([#6469](https://github.com/rocicorp/mono/pull/6469), [#6496](https://github.com/rocicorp/mono/pull/6496)) (thanks [@charpeni](https://github.com/charpeni)!) -- Zero now starts in a new [`'initializing'` connection state](/docs/connection#initializing) while it loads local data. Slow local loading no longer counts against the connect timeout. ([#6561](https://github.com/rocicorp/mono/pull/6561)) -- When local storage fails to open, fills up, or reports I/O errors, Zero reports it once through the new [`onStorageFailure`](/docs/connection#storage-failures) option and keeps running in memory. `zero.kvStore` reports which store is in use. ([#6585](https://github.com/rocicorp/mono/pull/6585), [#6657](https://github.com/rocicorp/mono/pull/6657), [#6686](https://github.com/rocicorp/mono/pull/6686)) (thanks [@pawarren](https://github.com/pawarren) and [@Karavil](https://github.com/Karavil)!) -- Upgrading from 1.10 keeps each client's local data and its pending mutations. Future protocol changes no longer start clients over with an empty local database. ([d597c69e8](https://github.com/rocicorp/mono/commit/d597c69e8), [28e581483](https://github.com/rocicorp/mono/commit/28e581483)) -- Server mutators can [return a JSON value](/docs/mutators#returning-data) to the client, [run again](/docs/mutators#retrying-mutators) in a new transaction when they fail with an error that `tx.retryOn()` accepts, and run at a chosen [transaction isolation level](/docs/server-zql#isolation-level). ([7756eb9bb](https://github.com/rocicorp/mono/commit/7756eb9bb), [4fe91c0d8](https://github.com/rocicorp/mono/commit/4fe91c0d8), [cb2821f3a](https://github.com/rocicorp/mono/commit/cb2821f3a)) (thanks [@pawarren](https://github.com/pawarren)!) -- [Litestream v5 backups](/docs/zero-cache-config#litestream-backup-using-v5) are now the default. Replicas restored from another replica's backup continue it incrementally, periodic snapshots keep backups compact, and the replica's WAL stays bounded. ([fb536f686](https://github.com/rocicorp/mono/commit/fb536f686), [c1a586810](https://github.com/rocicorp/mono/commit/c1a586810), [#6476](https://github.com/rocicorp/mono/pull/6476), [#6539](https://github.com/rocicorp/mono/pull/6539), [#6717](https://github.com/rocicorp/mono/pull/6717)) -- A `zero-cache` that comes online now [waits until replication has caught up](/docs/zero-cache-config#replication-lag-initial-readiness-threshold-ms) before reporting ready. ([1c5ddcdb4](https://github.com/rocicorp/mono/commit/1c5ddcdb4)) -- Inactive client groups are now kept for [7 days instead of 48 hours](/docs/zero-cache-config#cvr-garbage-collection-inactivity-threshold-hours), so clients that return within a week avoid a full resync. ([#6484](https://github.com/rocicorp/mono/pull/6484)) -- New tools for diagnosing slow queries: `zero-cache` logs which queries are slow and why, with index suggestions, and periodically logs per-query statistics. A new [operator password](/docs/zero-cache-config#operator-password) gives access to CPU profiles (`/profz`) and replica planner statistics (`/plannerz`) without the admin password. ([#6446](https://github.com/rocicorp/mono/pull/6446), [#6639](https://github.com/rocicorp/mono/pull/6639), [#6673](https://github.com/rocicorp/mono/pull/6673), [#6675](https://github.com/rocicorp/mono/pull/6675), [#6699](https://github.com/rocicorp/mono/pull/6699)) -- Opt-in limits protect `zero-cache` from expensive queries: a [hydration budget](/docs/zero-cache-config#view-syncer-hydration-budget-ms) for reconnecting clients and a [per-query hydration timeout](/docs/zero-cache-config#view-syncer-query-hydration-timeout-ms). ([#6477](https://github.com/rocicorp/mono/pull/6477), [#6499](https://github.com/rocicorp/mono/pull/6499)) -- New [OpenTelemetry metrics](/docs/otel) for WAL usage, index-build progress in replication status events, and restore progress in a new view-syncer status event. ([#6591](https://github.com/rocicorp/mono/pull/6591), [#6621](https://github.com/rocicorp/mono/pull/6621), [#6714](https://github.com/rocicorp/mono/pull/6714)) +- Queries can now start with results that a previous session saved on the device. These come back with the new [`'cached'` result type](/docs/queries#cached-results), so you can show them right away and still tell them apart from data the server has confirmed. ([#6578](https://github.com/rocicorp/mono/pull/6578)) (thanks [@pawarren](https://github.com/pawarren)!) +- Building the same query again usually gives you the same `Query` object. `useQuery` no longer rebuilds and rehashes its query on every render, and queries built inline work as [React dependencies](/docs/react#query-identity). ([#6469](https://github.com/rocicorp/mono/pull/6469), [#6496](https://github.com/rocicorp/mono/pull/6496)) (thanks [@charpeni](https://github.com/charpeni)!) +- Zero starts in a new [`'initializing'` connection state](/docs/connection#initializing) while it loads local data, and slow loading doesn't count against the connect timeout anymore. ([#6561](https://github.com/rocicorp/mono/pull/6561)) +- If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new [`onStorageFailure`](/docs/connection#storage-failures) option and keeps running in memory. `zero.kvStore` tells you which store is in use. ([#6585](https://github.com/rocicorp/mono/pull/6585), [#6657](https://github.com/rocicorp/mono/pull/6657), [#6686](https://github.com/rocicorp/mono/pull/6686)) (thanks [@pawarren](https://github.com/pawarren) and [@Karavil](https://github.com/Karavil)!) +- Clients upgrading from 1.10 keep their local data and pending mutations. Future protocol changes won't reset clients to an empty local database either. ([d597c69e8](https://github.com/rocicorp/mono/commit/d597c69e8), [28e581483](https://github.com/rocicorp/mono/commit/28e581483)) +- Server mutators can [return a JSON value](/docs/mutators#returning-data) to the client, [run again](/docs/mutators#retrying-mutators) in a new transaction when `tx.retryOn()` accepts the error, and run at a chosen [transaction isolation level](/docs/server-zql#isolation-level). ([7756eb9bb](https://github.com/rocicorp/mono/commit/7756eb9bb), [4fe91c0d8](https://github.com/rocicorp/mono/commit/4fe91c0d8), [cb2821f3a](https://github.com/rocicorp/mono/commit/cb2821f3a)) (thanks [@pawarren](https://github.com/pawarren)!) +- [Litestream v5 backups](/docs/zero-cache-config#litestream-backup-using-v5) are now the default. A replica restored from another replica's backup continues that backup incrementally. Periodic snapshots keep backups compact, and the replica's WAL stays bounded. ([fb536f686](https://github.com/rocicorp/mono/commit/fb536f686), [c1a586810](https://github.com/rocicorp/mono/commit/c1a586810), [#6476](https://github.com/rocicorp/mono/pull/6476), [#6539](https://github.com/rocicorp/mono/pull/6539), [#6717](https://github.com/rocicorp/mono/pull/6717)) +- A `zero-cache` that comes online now [waits for replication to catch up](/docs/zero-cache-config#replication-lag-initial-readiness-threshold-ms) before it reports ready. ([1c5ddcdb4](https://github.com/rocicorp/mono/commit/1c5ddcdb4)) +- Inactive client groups are kept for [7 days instead of 48 hours](/docs/zero-cache-config#cvr-garbage-collection-inactivity-threshold-hours), so users who come back within a week don't need a full resync. ([#6484](https://github.com/rocicorp/mono/pull/6484)) +- `zero-cache` now logs slow queries with the reason and suggested indexes, and periodically logs per-query statistics. A new [operator password](/docs/zero-cache-config#operator-password) gives access to CPU profiles (`/profz`) and replica planner statistics (`/plannerz`) without the admin password. ([#6446](https://github.com/rocicorp/mono/pull/6446), [#6639](https://github.com/rocicorp/mono/pull/6639), [#6673](https://github.com/rocicorp/mono/pull/6673), [#6675](https://github.com/rocicorp/mono/pull/6675), [#6699](https://github.com/rocicorp/mono/pull/6699)) +- A new [hydration budget](/docs/zero-cache-config#view-syncer-hydration-budget-ms) caps the time a view-syncer spends re-hydrating a reconnecting client's inactive queries, and a [per-query hydration timeout](/docs/zero-cache-config#view-syncer-query-hydration-timeout-ms) aborts any query that takes too long to hydrate. Both are off by default. ([#6477](https://github.com/rocicorp/mono/pull/6477), [#6499](https://github.com/rocicorp/mono/pull/6499)) +- New [OpenTelemetry metrics](/docs/otel) track WAL usage. Replication status events now include index-build progress, and a new view-syncer status event reports restore progress. ([#6591](https://github.com/rocicorp/mono/pull/6591), [#6621](https://github.com/rocicorp/mono/pull/6621), [#6714](https://github.com/rocicorp/mono/pull/6714)) - Server mutations can send their logs to your own `LogSink`. ([#6677](https://github.com/rocicorp/mono/pull/6677)) (thanks [@pawarren](https://github.com/pawarren)!) -- The `zero` Docker image shrank from about 1.09 GB to 807 MB. ([#6695](https://github.com/rocicorp/mono/pull/6695)) +- The `zero` Docker image is down to 807 MB from about 1.09 GB. ([#6695](https://github.com/rocicorp/mono/pull/6695)) ## Performance ### Client Cold Start -Apps usually register their queries before Zero has loaded local data, for example when `useQuery` runs during startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits until the data has loaded and hydrates each query once ([#6472](https://github.com/rocicorp/mono/pull/6472)). With 13 queries and about 30,000 local rows, startup is about **7x faster**, and about **8x faster** on React Native. +Apps usually register queries before Zero has finished loading local data, for example when `useQuery` runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once ([#6472](https://github.com/rocicorp/mono/pull/6472)). With 13 queries and about 30,000 local rows, startup is about **7x faster**, and about **8x faster** on React Native. -### Slower in Zero 1.11: Client Writes With Many Joined Queries +### Client Writes With Many Joined Queries Got Slower -One benchmark got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about **1.2x longer** in Zero 1.11. Joins now keep an index of their parent rows so that changes no parent can see are skipped ([#6645](https://github.com/rocicorp/mono/pull/6645), [#6708](https://github.com/rocicorp/mono/pull/6708)). When every change reaches some parent, as here, keeping that index up to date is extra work. +This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about **1.2x longer**. Joins now keep an index of their parent rows so they can skip changes no parent can see ([#6645](https://github.com/rocicorp/mono/pull/6645), [#6708](https://github.com/rocicorp/mono/pull/6708)). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead. Date: Thu, 15 Oct 2026 07:34:34 +0000 Subject: [PATCH 12/20] docs(release-notes): fix bold markup in the 1.11 incremental updates sentence --- assets/search-index.json | 6 +++--- contents/docs/release-notes/1.11.mdx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/search-index.json b/assets/search-index.json index 667a4e8d..c28b7d3a 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -5687,7 +5687,7 @@ "title": "Zero 1.11", "searchTitle": "Zero 1.11", "url": "/docs/release-notes/1.11", - "content": "Installation npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Overview Zero 1.11 can show cached query results right away, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication. Features Queries can now start with results that a previous session saved on the device. These come back with the new 'cached' result type, so you can show them right away and still tell them apart from data the server has confirmed. (#6578) (thanks @pawarren!) Building the same query again usually gives you the same Query object. useQuery no longer rebuilds and rehashes its query on every render, and queries built inline work as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero starts in a new 'initializing' connection state while it loads local data, and slow loading doesn't count against the connect timeout anymore. (#6561) If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore tells you which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Clients upgrading from 1.10 keep their local data and pending mutations. Future protocol changes won't reset clients to an empty local database either. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when tx.retryOn() accepts the error, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. A replica restored from another replica's backup continues that backup incrementally. Periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits for replication to catch up before it reports ready. (1c5ddcdb4) Inactive client groups are kept for 7 days instead of 48 hours, so users who come back within a week don't need a full resync. (#6484) zero-cache now logs slow queries with the reason and suggested indexes, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) A new hydration budget caps the time a view-syncer spends re-hydrating a reconnecting client's inactive queries, and a per-query hydration timeout aborts any query that takes too long to hydrate. Both are off by default. (#6477, #6499) New OpenTelemetry metrics track WAL usage. Replication status events now include index-build progress, and a new view-syncer status event reports restore progress. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image is down to 807 MB from about 1.09 GB. (#6695) Performance Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x **less CPU**. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.6x faster on React Native. That's the geometric mean of 56 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead. Fixes Query Results Queries using or with a flipped exists could return wrong rows. (#6636) Joins could leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery weren't synced, and changes to them didn't propagate. (913c0665f) Catching up a client after a large update could fail with Missing row, and a schema change during rehydration could fail with table has no column named. (#6662, #6616) A SQLite plan that nests one multi-index OR inside another could crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache called ZERO_QUERY_URL again for unchanged custom queries whenever it rebuilt its query pipelines. A permission change still re-transforms every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments could share one view, and a SolidJS view rebuilt after its query changed kept the old TTL. (#6461, #6497) In mutators, a row inserted without its optional columns didn't read those columns back as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) could return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients logged ChunkNotFoundError during persist. A write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database didn't delete the right file. (#6656) (thanks @pawarren!) Clients could go offline after the app resumed from the background or the computer woke from sleep, and React Native connect timeouts left the client stuck in an error state instead of retrying. (#6510, #6485) (thanks @pawarren!) Slow local startup could make a healthy server look unreachable. (#6475) A mutator that threw while being replayed disconnected the client in a loop. The client now stays connected, and the mutation is still sent to the server. (#6555) Client memory grew with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil didn't suspend until the first rows arrived, contrary to the docs (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing were logged as errors. They're now warnings or info, and fatal errors include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability zero-cache leaked memory and other resources around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) View-syncers could crash or return Internal error when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg crashed when Postgres closed a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector could run a view-syncer out of memory on large tables. (#6596) Error messages could include row values, query literals and mutation arguments. (#6413) Replication, Backfills, and Backups Replication could roll back or stop after an interrupted stream, and a replication-manager whose slot was invalidated crash-looped instead of resyncing. (#6562, #6590) Backfills could apply rows before replication had caught up to them, didn't resume when a view-syncer moved to another replication-manager, and leaked a Postgres connection that held back vacuum. (#6685, #6684, #6513) Replication slots are more robust. Slot creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag was measured wrong on Aurora. (e6a2590db) After initial sync, the replication-manager could serve before a backup covered its replica, so restoring view-syncers fell back to Postgres. It now waits for that backup. (#6465) Query pipelines were reset more often than needed, and dropping an upstream index didn't refresh table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers now fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager now keeps serving until the new one takes over. (21f349135) Change-database connections leaked after a failed catch-up, and a change-database reset could hang. (#6529, #6632) Litestream could run out of memory in small containers. Its upload concurrency now scales with available memory. (240c05730) Configuration values that start with a dash, such as some shard IDs, were parsed wrong. (81e6ac570) In the zero Docker image, initial sync and backfills could leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe) Breaking Changes Legacy queries: zero-cache now rejects queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: if your code switches over every result type or connection state, handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options; they no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours (was 48), replication lag is reported every 10 seconds (was 30), and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 (was 2). Startup can take longer while zero-cache waits for a backup and for replication to catch up, so give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates, or tx.retryOn() won't work. Using tx after its transaction has ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL don't apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they hadn't sent yet are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Unsent mutations are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after upgrading, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE. Use ZERO_CHANGE_STREAMER_URI instead. (698286b55) Events: there's a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", + "content": "Installation npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Overview Zero 1.11 can show cached query results right away, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication. Features Queries can now start with results that a previous session saved on the device. These come back with the new 'cached' result type, so you can show them right away and still tell them apart from data the server has confirmed. (#6578) (thanks @pawarren!) Building the same query again usually gives you the same Query object. useQuery no longer rebuilds and rehashes its query on every render, and queries built inline work as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero starts in a new 'initializing' connection state while it loads local data, and slow loading doesn't count against the connect timeout anymore. (#6561) If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore tells you which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Clients upgrading from 1.10 keep their local data and pending mutations. Future protocol changes won't reset clients to an empty local database either. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when tx.retryOn() accepts the error, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. A replica restored from another replica's backup continues that backup incrementally. Periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits for replication to catch up before it reports ready. (1c5ddcdb4) Inactive client groups are kept for 7 days instead of 48 hours, so users who come back within a week don't need a full resync. (#6484) zero-cache now logs slow queries with the reason and suggested indexes, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) A new hydration budget caps the time a view-syncer spends re-hydrating a reconnecting client's inactive queries, and a per-query hydration timeout aborts any query that takes too long to hydrate. Both are off by default. (#6477, #6499) New OpenTelemetry metrics track WAL usage. Replication status events now include index-build progress, and a new view-syncer status event reports restore progress. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image is down to 807 MB from about 1.09 GB. (#6695) Performance Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x less CPU. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.6x faster on React Native. That's the geometric mean of 56 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead. Fixes Query Results Queries using or with a flipped exists could return wrong rows. (#6636) Joins could leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery weren't synced, and changes to them didn't propagate. (913c0665f) Catching up a client after a large update could fail with Missing row, and a schema change during rehydration could fail with table has no column named. (#6662, #6616) A SQLite plan that nests one multi-index OR inside another could crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache called ZERO_QUERY_URL again for unchanged custom queries whenever it rebuilt its query pipelines. A permission change still re-transforms every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments could share one view, and a SolidJS view rebuilt after its query changed kept the old TTL. (#6461, #6497) In mutators, a row inserted without its optional columns didn't read those columns back as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) could return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients logged ChunkNotFoundError during persist. A write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database didn't delete the right file. (#6656) (thanks @pawarren!) Clients could go offline after the app resumed from the background or the computer woke from sleep, and React Native connect timeouts left the client stuck in an error state instead of retrying. (#6510, #6485) (thanks @pawarren!) Slow local startup could make a healthy server look unreachable. (#6475) A mutator that threw while being replayed disconnected the client in a loop. The client now stays connected, and the mutation is still sent to the server. (#6555) Client memory grew with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil didn't suspend until the first rows arrived, contrary to the docs (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing were logged as errors. They're now warnings or info, and fatal errors include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability zero-cache leaked memory and other resources around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) View-syncers could crash or return Internal error when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg crashed when Postgres closed a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector could run a view-syncer out of memory on large tables. (#6596) Error messages could include row values, query literals and mutation arguments. (#6413) Replication, Backfills, and Backups Replication could roll back or stop after an interrupted stream, and a replication-manager whose slot was invalidated crash-looped instead of resyncing. (#6562, #6590) Backfills could apply rows before replication had caught up to them, didn't resume when a view-syncer moved to another replication-manager, and leaked a Postgres connection that held back vacuum. (#6685, #6684, #6513) Replication slots are more robust. Slot creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag was measured wrong on Aurora. (e6a2590db) After initial sync, the replication-manager could serve before a backup covered its replica, so restoring view-syncers fell back to Postgres. It now waits for that backup. (#6465) Query pipelines were reset more often than needed, and dropping an upstream index didn't refresh table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers now fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager now keeps serving until the new one takes over. (21f349135) Change-database connections leaked after a failed catch-up, and a change-database reset could hang. (#6529, #6632) Litestream could run out of memory in small containers. Its upload concurrency now scales with available memory. (240c05730) Configuration values that start with a dash, such as some shard IDs, were parsed wrong. (81e6ac570) In the zero Docker image, initial sync and backfills could leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe) Breaking Changes Legacy queries: zero-cache now rejects queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: if your code switches over every result type or connection state, handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options; they no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours (was 48), replication lag is reported every 10 seconds (was 30), and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 (was 2). Startup can take longer while zero-cache waits for a backup and for replication to catch up, so give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates, or tx.retryOn() won't work. Using tx after its transaction has ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL don't apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they hadn't sent yet are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Unsent mutations are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after upgrading, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE. Use ZERO_CHANGE_STREAMER_URI instead. (698286b55) Events: there's a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", "headings": [ { "text": "Installation", @@ -5793,7 +5793,7 @@ "sectionTitle": "Performance", "sectionId": "performance", "url": "/docs/release-notes/1.11", - "content": "Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x **less CPU**. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.6x faster on React Native. That's the geometric mean of 56 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead.", + "content": "Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x less CPU. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.6x faster on React Native. That's the geometric mean of 56 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead.", "kind": "section" }, { @@ -5813,7 +5813,7 @@ "sectionTitle": "Server-Side Incremental Updates", "sectionId": "server-side-incremental-updates", "url": "/docs/release-notes/1.11", - "content": "When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x **less CPU**. We measured 100 client groups on one sync worker, each syncing a query with a related subquery.", + "content": "When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x less CPU. We measured 100 client groups on one sync worker, each syncing a query with a related subquery.", "kind": "section" }, { diff --git a/contents/docs/release-notes/1.11.mdx b/contents/docs/release-notes/1.11.mdx index e0372695..b5b0593e 100644 --- a/contents/docs/release-notes/1.11.mdx +++ b/contents/docs/release-notes/1.11.mdx @@ -68,7 +68,7 @@ Apps usually register queries before Zero has finished loading local data, for e ### Server-Side Incremental Updates -When a transaction replicates, `zero-cache` has to bring every client group's queries up to date. Zero 1.11 does this with about **2.5x \*\***less CPU\*\*. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. +When a transaction replicates, `zero-cache` has to bring every client group's queries up to date. Zero 1.11 does this with about **2.5x less CPU**. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. Date: Thu, 15 Oct 2026 08:12:35 +0000 Subject: [PATCH 13/20] docs: plainer wording in the 1.11 feature docs Turns the "A few things to know" and "Keep in mind" lists into prose, drops the bold lead-in bullets in Storage Failures, and trims repeated "X, not Y" phrasing. Headings, anchors, code and facts are unchanged. --- assets/search-index.json | 42 ++++++++++++++--------------- contents/docs/connection.mdx | 25 +++++++---------- contents/docs/mutators.mdx | 26 ++++++++---------- contents/docs/queries.mdx | 14 +++++----- contents/docs/react.mdx | 6 ++--- contents/docs/zero-cache-config.mdx | 2 +- 6 files changed, 53 insertions(+), 62 deletions(-) diff --git a/assets/search-index.json b/assets/search-index.json index c28b7d3a..dc5144ab 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -781,7 +781,7 @@ "title": "Connection Status", "searchTitle": "Connection Status", "url": "/docs/connection", - "content": "Overview Zero manages a persistent connection to zero-cache with the following lifecycle: Before this lifecycle starts, Zero is in the initializing state while it loads local data. It then moves to connecting and never returns to initializing. Usage The current connection state is available in the zero.connection.state property. This is subscribable and also has reactive hooks for React and SolidJS: import {useConnectionState} from '@rocicorp/zero/react' function ConnectionStatus() { const state = useConnectionState() switch (state.name) { case 'initializing': return
Loading...
case 'connecting': return
Connecting...
case 'connected': return
Connected
case 'disconnected': return
Offline
case 'error': return
Error
case 'needs-auth': return
Session expired
default: return null } }import {useConnectionState} from '@rocicorp/zero/solid' function ConnectionStatus() { const state = useConnectionState() return (
Loading...
Connecting...
Connected
Offline
Error
Session expired
) }zero.connection.state.subscribe(state => { switch (state.name) { case 'initializing': console.log('Loading local data...') break case 'connecting': console.log(`Connecting... ${state.reason}`) break case 'connected': console.log('Connected') break case 'disconnected': console.log(`Disconnected ${state.reason}`) break case 'error': console.log(`Error ${state.reason}`) break case 'needs-auth': console.log('Session expired') break default: return null } }) Offline Zero does not support offline writes. When the client is in the disconnected, error, or needs-auth states, reads from synced data continue to work, but writes are rejected. Offline UI While Zero is in the disconnected, error, or needs-auth states, you should prevent the user from inputting data to your application to avoid data loss. Zero automates this as best it can by rejecting writes in these states. But there can still be cases where the user can lose work – for example by typing into a textarea that is only written to Zero when the user presses a button. The easiest way to implement this is with a modal overlay that covers the entire screen and tells the user to reconnect. However, you could also continue to let the user use the app read-only, and only disable inputs. Details Initializing Zero starts in the initializing state. While initializing, Zero loads its local data. It has not contacted zero-cache yet. How long this takes depends on the device and the amount of local data, not on the network. On a slow device with a lot of local data, it can take tens of seconds. Once the local data is loaded, Zero transitions to connecting. Zero is only initializing once, right after it is created. When Zero reconnects later, it goes straight to connecting. Show a loading indicator while initializing, not an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting, not when you create Zero. There is no timeout while initializing, and the time spent here does not count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing. To limit this, add your own timeout. Reads and writes are allowed while initializing. Queries return results once the local data is loaded. Writes are queued and are sent when Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state. Storage Failures Zero saves synced data on the device: in IndexedDB in the browser, or in SQLite on React Native. This storage can fail. For example, the device can run out of space, or the database can fail to open. When this happens, Zero calls onStorageFailure once with a StorageFailureError. Its kind says what failed: In the browser, Zero reports a storage failure only when IndexedDB fails to open: full if the browser says the disk is full, otherwise cannot-open. Zero keeps working after a storage failure: If storage fails while Zero is starting, Zero runs in memory for the rest of the session, as if you had set kvStore: 'mem'. Nothing is saved on the device. If storage fails later, Zero stops saving data on the device. Queries and mutations keep working on the data in memory, and mutations are still sent to zero-cache. A read that needs data Zero hasn't loaded into memory can still fail. Zero tries the device's storage again only when the app creates a new Zero instance, for example after a restart. By default, Zero only logs the failure. Use onStorageFailure to tell the user what to do: import type {ZeroOptions} from '@rocicorp/zero' const opts: ZeroOptions = { // ... onStorageFailure: failure => { // showBanner stands in for your app's own UI. if (failure.kind === 'full') { showBanner( 'Your device is out of space. Free up some space, then restart the app.' ) } else { showBanner( 'Data could not be saved on this device. Restart the app to try again.' ) } } } StorageFailureError is exported from @rocicorp/zero. Its cause is the original error from the browser or the SQLite driver. A custom kvStore can throw a StorageFailureError to get the same handling. Checking the Store zero.kvStore.kind tells you which storage Zero is using: 'idb', 'mem', 'expo-sqlite', or 'op-sqlite'. A custom store reports its own kind, if it sets one. if (zero.kvStore.kind === 'mem') { // Data is not saved on this device. } Without IndexedDB, kind is 'mem' from the start and onStorageFailure isn't called. When IndexedDB fails to open, kind changes to 'mem' only when Zero first reads or writes the store, so check kind when you need it, not once at startup. Why Zero Doesn't Support Offline Writes Supporting offline writes in collaborative applications is inherently difficult, and no sync engine or CRDT algorithm can automatically solve it for you. Despite what their marketing says 😉. Example Imagine two users are editing an article about cats. One goes offline and does a bunch of work on the article, while the other decides that the article should actually be about dogs and rewrites it. When the offline user reconnects, there is no way that any software algorithm can automatically resolve their conflict. One or the other of them is going to be upset. This is a trivial data model with a single field, and is already unsolvable. Real-world applications are much worse: Foreign keys and other constraints can pass while offline, but break when the user reconnects. Custom business logic and authorization rules can pass while offline, but break when the user reconnects. The application's schema can change while offline, and the user's data may not be processable by the new schema. Just take your own schema and ask yourself what should really happen if one user takes their device offline for a week and makes arbitrarily complex changes while other users are working online. Tradeoffs It is of course possible to create applications that support offline writes well (Git exists!). But it requires significant tradeoffs. For example, you could: Disallow destructive operations (i.e., users can create tasks while offline, but cannot edit or delete them). Support custom UX to allow users to fork and merge conflicts when they occur. Restrict offline writes to a single device. Accept potential user data loss. Zero's Position While we recognize that offline writes would be useful, the reality is that for most of the apps we want to support, the user is online the vast majority of the time and the cost to support offline is extremely high. There is simply more value in making the online experience great first, and that's where we're focused right now. We would like to revisit this in the future, but it's not a priority right now.", + "content": "Overview Zero manages a persistent connection to zero-cache with the following lifecycle: Before any of this, Zero is initializing while it loads local data. After that it moves to connecting and never goes back. Usage The current connection state is available in the zero.connection.state property. This is subscribable and also has reactive hooks for React and SolidJS: import {useConnectionState} from '@rocicorp/zero/react' function ConnectionStatus() { const state = useConnectionState() switch (state.name) { case 'initializing': return
Loading...
case 'connecting': return
Connecting...
case 'connected': return
Connected
case 'disconnected': return
Offline
case 'error': return
Error
case 'needs-auth': return
Session expired
default: return null } }import {useConnectionState} from '@rocicorp/zero/solid' function ConnectionStatus() { const state = useConnectionState() return (
Loading...
Connecting...
Connected
Offline
Error
Session expired
) }zero.connection.state.subscribe(state => { switch (state.name) { case 'initializing': console.log('Loading local data...') break case 'connecting': console.log(`Connecting... ${state.reason}`) break case 'connected': console.log('Connected') break case 'disconnected': console.log(`Disconnected ${state.reason}`) break case 'error': console.log(`Error ${state.reason}`) break case 'needs-auth': console.log('Session expired') break default: return null } }) Offline Zero does not support offline writes. When the client is in the disconnected, error, or needs-auth states, reads from synced data continue to work, but writes are rejected. Offline UI While Zero is in the disconnected, error, or needs-auth states, you should prevent the user from inputting data to your application to avoid data loss. Zero automates this as best it can by rejecting writes in these states. But there can still be cases where the user can lose work – for example by typing into a textarea that is only written to Zero when the user presses a button. The easiest way to implement this is with a modal overlay that covers the entire screen and tells the user to reconnect. However, you could also continue to let the user use the app read-only, and only disable inputs. Details Initializing Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take tens of seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state. Storage Failures Zero stores synced data on the device, in IndexedDB in the browser or SQLite on React Native. Sometimes that fails: the device runs out of space, or the database won't open. When it does, Zero calls onStorageFailure once with a StorageFailureError. Its kind says what went wrong: In the browser, Zero reports a storage failure only when IndexedDB fails to open: full if the browser says the disk is full, otherwise cannot-open. Zero keeps working either way. If storage fails while Zero is starting, Zero runs in memory for the rest of the session, as if you had set kvStore: 'mem', and nothing is saved on the device. If it fails later, Zero stops saving to the device. Queries and mutations keep working on the data in memory and mutations are still sent to zero-cache, but a read that needs data Zero hasn't loaded into memory can still fail. Zero only tries the device's storage again when the app creates a new Zero instance, for example after a restart. By default, Zero only logs the failure. Use onStorageFailure to tell the user what to do: import type {ZeroOptions} from '@rocicorp/zero' const opts: ZeroOptions = { // ... onStorageFailure: failure => { // showBanner stands in for your app's own UI. if (failure.kind === 'full') { showBanner( 'Your device is out of space. Free up some space, then restart the app.' ) } else { showBanner( 'Data could not be saved on this device. Restart the app to try again.' ) } } } StorageFailureError is exported from @rocicorp/zero. Its cause is the original error from the browser or the SQLite driver. A custom kvStore can throw a StorageFailureError to get the same handling. Checking the Store zero.kvStore.kind tells you which storage Zero is using: 'idb', 'mem', 'expo-sqlite', or 'op-sqlite'. A custom store reports its own kind, if it sets one. if (zero.kvStore.kind === 'mem') { // Data is not saved on this device. } Without IndexedDB, kind is 'mem' from the start and onStorageFailure isn't called. When IndexedDB fails to open, kind changes to 'mem' only when Zero first reads or writes the store, so check kind when you need it, not once at startup. Why Zero Doesn't Support Offline Writes Supporting offline writes in collaborative applications is inherently difficult, and no sync engine or CRDT algorithm can automatically solve it for you. Despite what their marketing says 😉. Example Imagine two users are editing an article about cats. One goes offline and does a bunch of work on the article, while the other decides that the article should actually be about dogs and rewrites it. When the offline user reconnects, there is no way that any software algorithm can automatically resolve their conflict. One or the other of them is going to be upset. This is a trivial data model with a single field, and is already unsolvable. Real-world applications are much worse: Foreign keys and other constraints can pass while offline, but break when the user reconnects. Custom business logic and authorization rules can pass while offline, but break when the user reconnects. The application's schema can change while offline, and the user's data may not be processable by the new schema. Just take your own schema and ask yourself what should really happen if one user takes their device offline for a week and makes arbitrarily complex changes while other users are working online. Tradeoffs It is of course possible to create applications that support offline writes well (Git exists!). But it requires significant tradeoffs. For example, you could: Disallow destructive operations (i.e., users can create tasks while offline, but cannot edit or delete them). Support custom UX to allow users to fork and merge conflicts when they occur. Restrict offline writes to a single device. Accept potential user data loss. Zero's Position While we recognize that offline writes would be useful, the reality is that for most of the apps we want to support, the user is online the vast majority of the time and the cost to support offline is extremely high. There is simply more value in making the online experience great first, and that's where we're focused right now. We would like to revisit this in the future, but it's not a priority right now.", "headings": [ { "text": "Overview", @@ -865,7 +865,7 @@ "sectionTitle": "Overview", "sectionId": "overview", "url": "/docs/connection", - "content": "Zero manages a persistent connection to zero-cache with the following lifecycle: Before this lifecycle starts, Zero is in the initializing state while it loads local data. It then moves to connecting and never returns to initializing.", + "content": "Zero manages a persistent connection to zero-cache with the following lifecycle: Before any of this, Zero is initializing while it loads local data. After that it moves to connecting and never goes back.", "kind": "section" }, { @@ -905,7 +905,7 @@ "sectionTitle": "Details", "sectionId": "details", "url": "/docs/connection", - "content": "Initializing Zero starts in the initializing state. While initializing, Zero loads its local data. It has not contacted zero-cache yet. How long this takes depends on the device and the amount of local data, not on the network. On a slow device with a lot of local data, it can take tens of seconds. Once the local data is loaded, Zero transitions to connecting. Zero is only initializing once, right after it is created. When Zero reconnects later, it goes straight to connecting. Show a loading indicator while initializing, not an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting, not when you create Zero. There is no timeout while initializing, and the time spent here does not count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing. To limit this, add your own timeout. Reads and writes are allowed while initializing. Queries return results once the local data is loaded. Writes are queued and are sent when Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state.", + "content": "Initializing Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take tens of seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state.", "kind": "section" }, { @@ -915,7 +915,7 @@ "sectionTitle": "Initializing", "sectionId": "initializing", "url": "/docs/connection", - "content": "Zero starts in the initializing state. While initializing, Zero loads its local data. It has not contacted zero-cache yet. How long this takes depends on the device and the amount of local data, not on the network. On a slow device with a lot of local data, it can take tens of seconds. Once the local data is loaded, Zero transitions to connecting. Zero is only initializing once, right after it is created. When Zero reconnects later, it goes straight to connecting. Show a loading indicator while initializing, not an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting, not when you create Zero. There is no timeout while initializing, and the time spent here does not count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing. To limit this, add your own timeout. Reads and writes are allowed while initializing. Queries return results once the local data is loaded. Writes are queued and are sent when Zero connects.", + "content": "Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take tens of seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects.", "kind": "section" }, { @@ -985,7 +985,7 @@ "sectionTitle": "Storage Failures", "sectionId": "storage-failures", "url": "/docs/connection", - "content": "Zero saves synced data on the device: in IndexedDB in the browser, or in SQLite on React Native. This storage can fail. For example, the device can run out of space, or the database can fail to open. When this happens, Zero calls onStorageFailure once with a StorageFailureError. Its kind says what failed: In the browser, Zero reports a storage failure only when IndexedDB fails to open: full if the browser says the disk is full, otherwise cannot-open. Zero keeps working after a storage failure: If storage fails while Zero is starting, Zero runs in memory for the rest of the session, as if you had set kvStore: 'mem'. Nothing is saved on the device. If storage fails later, Zero stops saving data on the device. Queries and mutations keep working on the data in memory, and mutations are still sent to zero-cache. A read that needs data Zero hasn't loaded into memory can still fail. Zero tries the device's storage again only when the app creates a new Zero instance, for example after a restart. By default, Zero only logs the failure. Use onStorageFailure to tell the user what to do: import type {ZeroOptions} from '@rocicorp/zero' const opts: ZeroOptions = { // ... onStorageFailure: failure => { // showBanner stands in for your app's own UI. if (failure.kind === 'full') { showBanner( 'Your device is out of space. Free up some space, then restart the app.' ) } else { showBanner( 'Data could not be saved on this device. Restart the app to try again.' ) } } } StorageFailureError is exported from @rocicorp/zero. Its cause is the original error from the browser or the SQLite driver. A custom kvStore can throw a StorageFailureError to get the same handling. Checking the Store zero.kvStore.kind tells you which storage Zero is using: 'idb', 'mem', 'expo-sqlite', or 'op-sqlite'. A custom store reports its own kind, if it sets one. if (zero.kvStore.kind === 'mem') { // Data is not saved on this device. } Without IndexedDB, kind is 'mem' from the start and onStorageFailure isn't called. When IndexedDB fails to open, kind changes to 'mem' only when Zero first reads or writes the store, so check kind when you need it, not once at startup.", + "content": "Zero stores synced data on the device, in IndexedDB in the browser or SQLite on React Native. Sometimes that fails: the device runs out of space, or the database won't open. When it does, Zero calls onStorageFailure once with a StorageFailureError. Its kind says what went wrong: In the browser, Zero reports a storage failure only when IndexedDB fails to open: full if the browser says the disk is full, otherwise cannot-open. Zero keeps working either way. If storage fails while Zero is starting, Zero runs in memory for the rest of the session, as if you had set kvStore: 'mem', and nothing is saved on the device. If it fails later, Zero stops saving to the device. Queries and mutations keep working on the data in memory and mutations are still sent to zero-cache, but a read that needs data Zero hasn't loaded into memory can still fail. Zero only tries the device's storage again when the app creates a new Zero instance, for example after a restart. By default, Zero only logs the failure. Use onStorageFailure to tell the user what to do: import type {ZeroOptions} from '@rocicorp/zero' const opts: ZeroOptions = { // ... onStorageFailure: failure => { // showBanner stands in for your app's own UI. if (failure.kind === 'full') { showBanner( 'Your device is out of space. Free up some space, then restart the app.' ) } else { showBanner( 'Data could not be saved on this device. Restart the app to try again.' ) } } } StorageFailureError is exported from @rocicorp/zero. Its cause is the original error from the browser or the SQLite driver. A custom kvStore can throw a StorageFailureError to get the same handling. Checking the Store zero.kvStore.kind tells you which storage Zero is using: 'idb', 'mem', 'expo-sqlite', or 'op-sqlite'. A custom store reports its own kind, if it sets one. if (zero.kvStore.kind === 'mem') { // Data is not saved on this device. } Without IndexedDB, kind is 'mem' from the start and onStorageFailure isn't called. When IndexedDB fails to open, kind changes to 'mem' only when Zero first reads or writes the store, so check kind when you need it, not once at startup.", "kind": "section" }, { @@ -1920,7 +1920,7 @@ "title": "Mutators", "searchTitle": "Mutators", "url": "/docs/mutators", - "content": "Mutators are how you write data with Zero. Here's a simple example: // src/mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' export const mutators = defineMutators({ updateIssue: defineMutator( z.object({ id: z.string(), title: z.string() }), async ({tx, args: {id, title}}) => { if (title.length > 100) { throw new Error(`Title is too long`) } await tx.mutate.issue.update({ id, title }) } ) }) Architecture A copy of each mutator exists on both the client and on your server: Often the implementations will be the same, and you can just share their code. This is easy with full-stack frameworks like TanStack Start or Next.js. But the implementations don't have to be the same, or even compute the same result. For example, the server can add extra checks to enforce permissions, or send notifications or interact with other systems. Life of a Mutation When a mutator is invoked, it initially runs on the client, against the client-side datastore. Any changes are immediately applied to open queries and the user sees the changes. In the background, Zero sends a mutation (a record of the mutator having run with certain arguments) to your server's push endpoint. Your push endpoint runs the push protocol, executing the server-side mutator in a transaction against your database and recording the fact that the mutation ran. The @rocicorp/zero package contains utilities to make it easy to implement this endpoint in TypeScript. The changes to the database are then replicated to zero-cache using logical replication. zero-cache calculates the updates to active queries and sends rows that have changed to each client. It also sends information about the mutations that have been applied to the database. Clients receive row updates and apply them to their local cache. Any pending mutations which have been applied to the server have their local effects rolled back. Client-side queries are updated and the user sees the changes. Defining Mutators Basics Create a mutator using defineMutator. The only required argument is a MutatorFn, which must be async: import {defineMutator} from '@rocicorp/zero' const myMutator = defineMutator(async () => { // ... }) Mutators almost always complete in the same frame on the client, within milliseconds. The reason they are marked async is because on the server, reading from the tx object goes over the network to Postgres. Writing Data The MutatorFn receives a tx parameter which can be used to write data with a CRUD-style API. Each table in your Zero schema has a corresponding field on tx.mutate: const myMutator = defineMutator(async ({tx}) => { // This is here because there's a `user` table in your schema. await tx.mutate.user.insert(...) }) Mutators almost always run in the same frame on the client, against local data. The reason mutators are marked async is because on the server, reading from the tx object goes over the network to Postgres. Also, in edge cases on the client, reads and writes can go to local storage (IndexedDB or SQLite). Insert Create new records with insert: tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: 'js' }) If the Zero primary key already exists, insert will succeed without changing the row - use upsert to update an existing row. Optional fields can be set to null to explicitly set the new field to null. They can also be set to undefined to take the default value (which is often null but can also be some generated value server-side): // Sets language to `null` specifically tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: null }) // Sets language to the default server-side value. // Could be null, or some generated or constant default value too. tx.mutate.user.insert({ id: 'user-123', username: 'sam' }) // Same as above tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: undefined }) Upsert Create new records or update existing ones with upsert: tx.mutate.user.upsert({ id: samID, username: 'sam', language: 'ts' }) upsert supports the same null / undefined semantics for optional fields that insert does (see above). Update Update an existing record. Does nothing if the specified record (by PK) does not exist. You can pass a partial object, leaving fields out that you don’t want to change. For example here we leave the username the same: // Leaves username field to previous value. tx.mutate.user.update({ id: samID, language: 'golang' }) // Same as above tx.mutate.user.update({ id: samID, username: undefined, language: 'haskell' }) // Reset language field to `null` tx.mutate.user.update({ id: samID, language: null }) Delete Delete an existing record. Does nothing if specified record does not exist. tx.mutate.user.delete({ id: samID }) Arguments The MutatorFn can take a single args parameter. To enable this, pass a validator to defineMutator: import {defineMutator} from '@rocicorp/zero' const initStats = defineMutator( z.object({issueCount: z.number()}), async ({tx, args: {issueCount}}) => { if (issueCount < 0) { throw new Error(`issueCount cannot be negative`) } await tx.mutate.stats.insert({ id: 'global', issueCount }) } ) We use Zod in these examples, but you can use any validation library that implements Standard Schema. It's most common for mutators to be a pure function of the database state plus arguments. But it's not required. Impure mutators can be useful, e.g., to consult some external system on the server for authorization or validation. Reading Data You can read data within a mutator by passing ZQL to tx.run: const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { const issue = await tx.run( zql.issue.where('id', id).one() ) if (issue?.status === 'closed') { throw new Error(`Cannot update closed issue`) } await tx.mutate.issue.update({ id, title }) } ) You have the full power of ZQL at your disposal, including relationships, filters, ordering, and limits. Reads and writes within a mutator are transactional, meaning that the datastore is guaranteed to not change while your mutator is running. And if the mutator throws, the entire mutation is rolled back. Unlike zero.run(), there is no type parameter that can be used to wait for server results inside mutators. This is because waiting for server results in mutators makes no sense – it would defeat the purpose of running optimistically to begin with. When a mutator runs on the client (tx.location === \"client\"), ZQL reads only return data already cached on the client. When mutators run on the server (tx.location === \"server\"), ZQL reads always return all data. Context Mutator parameters are supplied by the client application and passed to the server automatically by Zero. This makes them unsuitable for credentials, since the user could modify them. For this reason, Zero mutators also support the concept of a context object. Access your context with the ctx parameter to your mutator: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, ctx: {userID}, args: {id, title}}) => { // Note: User cannot control ctx.userID, so this // enforces authorship of created issue. await tx.mutate.issue.insert({ id, title, authorID: userID }) } ) If you don't want to register your Context and Schema types globally, you can use defineMutatorWithType and defineMutatorsWithType: import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {DrizzleTransaction} from '@rocicorp/zero/server/adapters/drizzle' import type {drizzleClient} from 'db-provider.ts' const defineMutator = defineMutatorWithType< Schema, ZeroContext, DrizzleTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {KyselyTransaction} from '@rocicorp/zero/server/adapters/kysely' import type {Database} from 'db-provider.ts' const defineMutator = defineMutatorWithType< Schema, ZeroContext, KyselyTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {PrismaTransaction} from '@rocicorp/zero/server/adapters/prisma' import type {PrismaClient} from '@prisma/client' const defineMutator = defineMutatorWithType< Schema, ZeroContext, PrismaTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {NodePgTransaction} from '@rocicorp/zero/server/adapters/pg' const defineMutator = defineMutatorWithType< Schema, ZeroContext, NodePgTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {PostgresJsTransaction} from '@rocicorp/zero/server/adapters/postgresjs' const defineMutator = defineMutatorWithType< Schema, ZeroContext, PostgresJsTransaction >() const defineMutators = defineMutatorsWithType() Mutator Registries The result of defineMutator is a MutatorDefinition. By itself this isn't super useful. You need to register it using defineMutators: export const mutators = defineMutators({ issue: { update: updateIssue } }) Typically these are done together in one step: export const mutators = defineMutators({ issue: { update: defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.update({ id, title }) } ) } }) The result of defineMutators is called a MutatorRegistry. Each field in the registry is a callable Mutator that you can use to perform mutations: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: 'issue-123', title: 'New title' }) ) Mutator Names Each Mutator has a mutatorName which is computed by defineMutators. When you run a mutator, Zero sends this name along with the arguments to your server to execute the server-side mutation. console.log(mutators.issue.update.mutatorName) // \"issue.update\" mutators.ts By convention, mutators are listed in a central mutators.ts file. This allows them to be easily used on both the client and server: import {defineMutators, defineMutator} from '@rocicorp/zero' import {zql} from './schema.ts' import {z} from 'zod' export const mutators = defineMutators({ posts: { create: defineMutator( z.object({ id: z.string(), title: z.string() }), async ({ tx, context: {userID}, args: {id, title} }) => { await tx.mutate.post.insert({ id, title, authorID: userID }) } ), update: defineMutator( z.object({ id: z.string(), title: z.string().optional() }), async ({ tx, context: {userID}, args: {id, title} }) => { const prev = await tx.run( zql.post.where('id', id).one() ) if (prev?.authorID !== userID) { throw new Error(`Access denied`) } await tx.mutate.post.update({ id, title, authorID: userID }) } ) } }) You can use as many levels of nesting as you want to organize your mutators. As your application grows, you can move mutators to different files to keep them organized: // posts.ts export const postMutators = { create: defineMutator( z.object({ id: z.string(), title: z.string(), }), async ({tx, context: {userID}, args: {id, title}}) => { await tx.mutate.post.insert({ id, title, authorID: userID, }) }, ), } // user.ts export const userMutators = { updateRole: defineMutator( z.object({ role: z.string(), }), async ({tx, ctx: {userID}, args: {role}}) => { await tx.mutate.user.update({ id: userID, role, }) }, ), } // mutators.ts import {postMutators} from 'zero/mutators/posts.ts' import {userMutators} from 'zero/mutators/users.ts' export const mutators = defineMutators{{ posts: postMutators, users: userMutators, }) defineMutators establishes the full name for each mutator (i.e., posts.create, users.updateRole), which is later sent to the server. So this should only be used once at the top level of your mutators.ts file. Registration Before you can use your mutators, you need to register them with Zero: import {ZeroProvider} from '@rocicorp/zero/react' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } return ( )import {ZeroProvider} from '@rocicorp/zero/solid' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } return ( )import {Zero} from '@rocicorp/zero' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } const zero = new Zero(opts) Mutators need to be registered with Zero because Zero calls them during sync for conflict resolution. If you invoke a mutator that is not registered, Zero will throw an error. Server Setup In order for mutations to sync, you must provide an implementation of the mutate endpoint on your server. zero-cache calls this endpoint to process each mutation. Registering the Endpoint Use ZERO_MUTATE_URL to tell zero-cache where to find your mutate implementation: export ZERO_MUTATE_URL=\"http://localhost:3000/api/zero/mutate\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleMutateRequest and mustGetMutator functions to implement the endpoint. Plug in whatever dbProvider you set up (see server-zql or the install guide). // src/routes/api/zero/mutate.ts import {createFileRoute} from '@tanstack/react-router' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request, userID: null }) return Response.json(result) } } } })// app/api/zero/mutate/route.ts import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(request: Request) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request, userID: null }) return Response.json(result) }// src/routes/api/zero/mutate.ts import type {APIEvent} from '@solidjs/start/server' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(event: APIEvent) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {Hono} from 'hono' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from './db-provider.ts' const app = new Hono() app.post('/api/zero/mutate', async c => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request: c.req.raw, userID: null }) return c.json(result) }) Zero includes several built-in database adapters. You can also easily create your own. See ZQL on the Server for more information. handleMutateRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetMutator looks up the mutator in the registry and throws an error if not found. The mutator.fn function is your mutator implementation wrapped in the validator you provided. These examples have only public mutators, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the mutate handler. See Authentication. Handling Errors The handleMutateRequest function skips any mutations that throw: const result = await handleMutateRequest({ dbProvider, handler: transact => transact(async (tx, name, args) => { // The mutation is skipped and the next mutation runs as normal. // The optimistic mutation on the client will be reverted. throw new Error('bonk') }), request: c.req.raw, userID: null }) handleMutateRequest catches such errors and turns them into a structured response that gets sent back to the client. You can recover the errors and show UI if you want. It is also of course possible for the entire push endpoint to return an HTTP error, or to not reply at all: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async () => { throw new Error('zonk') // will trigger resend } } } })export async function POST() { throw new Error('zonk') // will trigger resend }export async function POST() { throw new Error('zonk') // will trigger resend }app.post('/api/zero/mutate', async c => { // This will cause the client to resend all queued mutations. throw new Error('zonk') }) Responses other than 200, 401, or 403 enter the error state. zero-cache will retry on 5xx up to four times before returning an error. If Zero receives HTTP 401 or 403, the client will enter the needs auth state and require a manual reconnect. Use zero.connection.connect() for cookie auth or zero.connection.connect({auth: newToken}) for token auth, then Zero will retry all queued mutations. If you want a different behavior, it is possible to implement the mutate endpoint yourself and handle errors differently. Retrying Mutators Some errors go away if you try again. For example, at the repeatable read or serializable isolation level, Postgres can abort a transaction that conflicts with a concurrent one. The error is a serialization failure, code 40001. Call tx.retryOn() in a server mutator to have Zero run the mutator again, in a fresh transaction, when it fails with an error you choose: function isSerializationFailure(error: unknown) { return ( error instanceof Error && 'code' in error && error.code === '40001' ) } const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { if (tx.location === 'server') { tx.retryOn(isSerializationFailure) } await tx.mutate.issue.update({id, title}) } ) retryOn() takes a predicate. It gets the error and the attempt number: 1 when deciding the first re-run, 2 for the second, and so on. Return true to run the mutator again. If it returns anything else or throws, Zero records the error and skips the mutation, as with any other error. The predicate sees errors the mutator throws, and also errors Postgres raises at COMMIT, which the mutator can't catch. Everything the failed run wrote is rolled back, and the re-run reads fresh data. The error is passed as it was thrown. node-postgres and postgres.js errors have the Postgres error code in code. Other libraries can wrap the error, so check what yours throws. The predicate can also return a promise. Zero waits for it, so you can use it to back off before the next run: tx.retryOn(async (error, attempt) => { if (!isSerializationFailure(error)) { return false } // Wait 50ms, then 100ms, then 150ms, and so on. await new Promise(resolve => setTimeout(resolve, attempt * 50) ) return true }) A few things to know: retryOn() covers only the current run. The re-run calls it again because it runs the mutator again. Call it before anything that can fail. You can call retryOn() more than once. Zero runs the mutator again if any predicate returns true. Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run fails, Zero records the error. The predicate runs after the transaction has ended, so it can't use tx. Reading or writing through tx there throws. Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. retryOn() is only available on the server, when tx.location === 'server'. The built-in adapters and ZQLDatabase support retryOn(). If you pass your own Database to handleMutateRequest, it must pass the addRetryPredicate it receives in TransactionProviderInput on to the transaction it creates with CRUDMutatorFactory.createTransaction(). Otherwise tx.retryOn() throws. Custom Mutate URL By default, Zero sends mutations to the URL specified in the ZERO_MUTATE_URL parameter. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in the ZERO_MUTATE_URL parameter: export ZERO_MUTATE_URL=\"https://api.example.com/mutate,https://api.staging.example.com/mutate\" Then choose one of those URLs by passing it to mutateURL on the Zero constructor: const opts: ZeroOptions = { // ... mutateURL: 'https://api.staging.example.com/mutate' } URL Patterns The strings listed in ZERO_MUTATE_URL can also be URLPatterns: export ZERO_MUTATE_URL=\"https://mybranch-*.preview.myapp.com/mutate\" For more information, see the URLPattern section of the Queries docs. It works the same way for mutations. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Server-Specific Code To implement server-specific code, just run different mutators in your mutate endpoint. Server authority to the rescue! defineMutators accepts a baseMutators parameter that makes this easy. The returned mutator registry will contain all the mutators from baseMutators, plus any new ones you define or override: // server-mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' import {mutators as sharedMutators} from 'mutators.ts' export const serverMutators = defineMutators( sharedMutators, { posts: { // Overrides the shared mutator definition with same name. update: defineMutator( z.object({ id: z.string(), title: z.string().optional(), priority: z.number().optional() }), async ({ tx, ctx: {userID}, args: {id, title, priority} }) => { // Run the shared mutator first. await sharedMutators.posts.update.fn({ tx, ctx, args }) // Record a history of this operation happening in an audit log table. await tx.mutate.auditLog.insert({ issueId: id, action: 'update-title', timestamp: Date.getTime() }) } ) } } ) For simple things, we also expose a location field on the transaction object that you can use to branch your code: const myMutator = defineMutator(async ({tx}) => { if (tx.location === 'client') { // Client-side code } else { // Server-side code } }) Running Mutators Once you have registered your mutators, you can invoke them with zero.mutate: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: crypto.randomUUID(), title: 'New title' }) ) Client-generated random IDs from crypto.randomUUID(), uuid, ulid, or nanoid work much better with sync engines like Zero. See IDs for more details. Waiting for Results We typically recommend that you \"fire and forget\" mutators. Optimistic mutations make sense when the common case is that a mutation succeeds. If a mutation frequently fails, then showing the user an optimistic result isn't very useful, because it will likely be wrong. That said there are cases where it is nice to know when a write succeeded on either the client or server. One example is if you need to read a row directly after writing it. Zero's local writes are very fast (almost always < 1 frame), but because Zero is backed by IndexedDB, writes are still technically asynchronous and reads directly after a write may not return the new data. You can use the .client promise in this case to wait for a write to complete on the client side: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) // issue-123 not guaranteed to be present here. read1 may be undefined. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await client write – almost always less than 1 frame, and same // macrotask, so no browser paint will occur here. const res = await write.client if (res.type === 'error') { console.error('Mutator failed on client', res.error) } // issue-123 definitely can be read now. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) You can also await .server for the server result: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) const clientRes = await write.client if (clientRes.type === 'error') { throw new Error(`Mutator failed on client`, { cause: clientRes.error }) } // optimistic write guaranteed to be present here, but not // server write. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await the server result/acknowledgment. This requires a round trip. const serverRes = await write.server if (serverRes.type === 'error') { throw new Error(`Mutator failed on server`, { cause: serverRes.error }) } // The server acknowledged the mutation, but its Postgres changes // may not have replicated to this client yet. This read can still // reflect optimistic rather than authoritative state. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) If the client-side mutator fails, .server also resolves to an error result. Awaiting .server therefore covers both client- and server-side failures. Returning Data A mutator can return a JSON value. The caller gets it in the data field of the success result. The client and server runs can return different values. For example, the server can return a value that only the database knows: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.insert({id, title}) if (tx.location === 'client') { // The client doesn't know the short ID yet. return {shortID: null} } // Postgres assigns shortID when the row is inserted. const issue = await tx.run( zql.issue.where('id', id).one() ) return {shortID: issue?.shortID ?? null} } ) The data of the .client result is what the client run returned. The data of the .server result is what the server run returned: const write = zero.mutate( mutators.issue.create({ id: crypto.randomUUID(), title: 'New title' }) ) const serverRes = await write.server if (serverRes.type === 'success') { // `data` is a plain JSON value, so check its shape. const {shortID} = z .object({shortID: z.number().nullable()}) .parse(serverRes.data) console.log('Short ID:', shortID) } A few things to know: data is typed as ReadonlyJSONValue | undefined, not as the mutator's return type. Check its shape before you use it. If the mutator returns nothing, data is undefined. The server value only reaches the client if your mutate handler returns the mutator's result, as return mutator.fn({args, tx}) does in the examples above. Zero saves the server's return value in your database along with the mutation's result, so the client still gets it if the response from your mutate endpoint is lost. A mutator that returns nothing doesn't write this. Permissions Because mutators are just normal TypeScript functions that run server-side, there is no need for a special permissions system. You can implement whatever permission checks you want using plain TypeScript code. See Permissions for more information. Dropping Down to Raw SQL The ServerTransaction interface has a dbTransaction property that exposes the underlying database connection. This allows you to run raw SQL queries directly against the database. This is useful for complex queries, or for using Postgres features that Zero doesn't support yet: const markAllAsRead = defineMutator( z.object({ userId: z.string() }), async ({tx, args: {userId}}) => { // shared stuff ... if (tx.location === 'server') { // `tx` is now narrowed to `ServerTransaction`. // Do special server-only stuff with raw SQL. await tx.dbTransaction.query( ` UPDATE notification SET read = true WHERE user_id = $1 `, [userId] ) } } ) See ZQL on the Server for more information. Notifications and Async Work The best way to handle notifications and async work is a transactional outbox. This ensures that notifications actually do eventually get sent, without holding open database transactions to talk over the network. This can be implemented very easily in Zero by writing notifications to an outbox table as part of your mutator, then processing that table periodically with a background job. However sometimes it's still nice to do a quick and dirty async send as part of a mutation, for example early on in development, or to record metrics. For this, the createMutators pattern is useful: // server-mutators.ts import {defineMutator} from '@rocicorp/zero' import z from 'zod' import {zql} from 'schema.ts' import {mutators as clientMutators} from 'mutators.ts' // Instead of defining server mutators as a constant, // define them as a function of a list of async tasks. export function createMutators( asyncTasks: Array<() => Promise> ) { return defineMutators(clientMutators, { issue: { update: defineMutator( z.object({ id: z.string(), title: z.string() }), async (tx, {id, title}) => { await tx.mutate.issue.update({id, title}) asyncTasks.push(() => sendEmailToSubscribers(id)) } ) } }) } Then in your mutate handler: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ tx, args }) }), request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled( asyncTasks.map(task => task()) ) return Response.json(result) } } } })export async function POST(request: Request) { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({tx, args}) }), request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return Response.json(result) }export async function POST(event: APIEvent) { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({tx, args}) }), request: event.request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return Response.json(result) }app.post('/api/zero/mutate', async c => { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ tx, args }) }), request: c.req.raw, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return c.json(result) }) Custom Mutate Implementation You can manually implement the mutate endpoint in any programming language. This will be documented in the future, but you can refer to the handleMutateRequest source code for an example for now.", + "content": "Mutators are how you write data with Zero. Here's a simple example: // src/mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' export const mutators = defineMutators({ updateIssue: defineMutator( z.object({ id: z.string(), title: z.string() }), async ({tx, args: {id, title}}) => { if (title.length > 100) { throw new Error(`Title is too long`) } await tx.mutate.issue.update({ id, title }) } ) }) Architecture A copy of each mutator exists on both the client and on your server: Often the implementations will be the same, and you can just share their code. This is easy with full-stack frameworks like TanStack Start or Next.js. But the implementations don't have to be the same, or even compute the same result. For example, the server can add extra checks to enforce permissions, or send notifications or interact with other systems. Life of a Mutation When a mutator is invoked, it initially runs on the client, against the client-side datastore. Any changes are immediately applied to open queries and the user sees the changes. In the background, Zero sends a mutation (a record of the mutator having run with certain arguments) to your server's push endpoint. Your push endpoint runs the push protocol, executing the server-side mutator in a transaction against your database and recording the fact that the mutation ran. The @rocicorp/zero package contains utilities to make it easy to implement this endpoint in TypeScript. The changes to the database are then replicated to zero-cache using logical replication. zero-cache calculates the updates to active queries and sends rows that have changed to each client. It also sends information about the mutations that have been applied to the database. Clients receive row updates and apply them to their local cache. Any pending mutations which have been applied to the server have their local effects rolled back. Client-side queries are updated and the user sees the changes. Defining Mutators Basics Create a mutator using defineMutator. The only required argument is a MutatorFn, which must be async: import {defineMutator} from '@rocicorp/zero' const myMutator = defineMutator(async () => { // ... }) Mutators almost always complete in the same frame on the client, within milliseconds. The reason they are marked async is because on the server, reading from the tx object goes over the network to Postgres. Writing Data The MutatorFn receives a tx parameter which can be used to write data with a CRUD-style API. Each table in your Zero schema has a corresponding field on tx.mutate: const myMutator = defineMutator(async ({tx}) => { // This is here because there's a `user` table in your schema. await tx.mutate.user.insert(...) }) Mutators almost always run in the same frame on the client, against local data. The reason mutators are marked async is because on the server, reading from the tx object goes over the network to Postgres. Also, in edge cases on the client, reads and writes can go to local storage (IndexedDB or SQLite). Insert Create new records with insert: tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: 'js' }) If the Zero primary key already exists, insert will succeed without changing the row - use upsert to update an existing row. Optional fields can be set to null to explicitly set the new field to null. They can also be set to undefined to take the default value (which is often null but can also be some generated value server-side): // Sets language to `null` specifically tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: null }) // Sets language to the default server-side value. // Could be null, or some generated or constant default value too. tx.mutate.user.insert({ id: 'user-123', username: 'sam' }) // Same as above tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: undefined }) Upsert Create new records or update existing ones with upsert: tx.mutate.user.upsert({ id: samID, username: 'sam', language: 'ts' }) upsert supports the same null / undefined semantics for optional fields that insert does (see above). Update Update an existing record. Does nothing if the specified record (by PK) does not exist. You can pass a partial object, leaving fields out that you don’t want to change. For example here we leave the username the same: // Leaves username field to previous value. tx.mutate.user.update({ id: samID, language: 'golang' }) // Same as above tx.mutate.user.update({ id: samID, username: undefined, language: 'haskell' }) // Reset language field to `null` tx.mutate.user.update({ id: samID, language: null }) Delete Delete an existing record. Does nothing if specified record does not exist. tx.mutate.user.delete({ id: samID }) Arguments The MutatorFn can take a single args parameter. To enable this, pass a validator to defineMutator: import {defineMutator} from '@rocicorp/zero' const initStats = defineMutator( z.object({issueCount: z.number()}), async ({tx, args: {issueCount}}) => { if (issueCount < 0) { throw new Error(`issueCount cannot be negative`) } await tx.mutate.stats.insert({ id: 'global', issueCount }) } ) We use Zod in these examples, but you can use any validation library that implements Standard Schema. It's most common for mutators to be a pure function of the database state plus arguments. But it's not required. Impure mutators can be useful, e.g., to consult some external system on the server for authorization or validation. Reading Data You can read data within a mutator by passing ZQL to tx.run: const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { const issue = await tx.run( zql.issue.where('id', id).one() ) if (issue?.status === 'closed') { throw new Error(`Cannot update closed issue`) } await tx.mutate.issue.update({ id, title }) } ) You have the full power of ZQL at your disposal, including relationships, filters, ordering, and limits. Reads and writes within a mutator are transactional, meaning that the datastore is guaranteed to not change while your mutator is running. And if the mutator throws, the entire mutation is rolled back. Unlike zero.run(), there is no type parameter that can be used to wait for server results inside mutators. This is because waiting for server results in mutators makes no sense – it would defeat the purpose of running optimistically to begin with. When a mutator runs on the client (tx.location === \"client\"), ZQL reads only return data already cached on the client. When mutators run on the server (tx.location === \"server\"), ZQL reads always return all data. Context Mutator parameters are supplied by the client application and passed to the server automatically by Zero. This makes them unsuitable for credentials, since the user could modify them. For this reason, Zero mutators also support the concept of a context object. Access your context with the ctx parameter to your mutator: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, ctx: {userID}, args: {id, title}}) => { // Note: User cannot control ctx.userID, so this // enforces authorship of created issue. await tx.mutate.issue.insert({ id, title, authorID: userID }) } ) If you don't want to register your Context and Schema types globally, you can use defineMutatorWithType and defineMutatorsWithType: import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {DrizzleTransaction} from '@rocicorp/zero/server/adapters/drizzle' import type {drizzleClient} from 'db-provider.ts' const defineMutator = defineMutatorWithType< Schema, ZeroContext, DrizzleTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {KyselyTransaction} from '@rocicorp/zero/server/adapters/kysely' import type {Database} from 'db-provider.ts' const defineMutator = defineMutatorWithType< Schema, ZeroContext, KyselyTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {PrismaTransaction} from '@rocicorp/zero/server/adapters/prisma' import type {PrismaClient} from '@prisma/client' const defineMutator = defineMutatorWithType< Schema, ZeroContext, PrismaTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {NodePgTransaction} from '@rocicorp/zero/server/adapters/pg' const defineMutator = defineMutatorWithType< Schema, ZeroContext, NodePgTransaction >() const defineMutators = defineMutatorsWithType()import { defineMutatorWithType, defineMutatorsWithType } from '@rocicorp/zero' import type {ZeroContext} from 'context.ts' import type {Schema} from 'schema.ts' import type {PostgresJsTransaction} from '@rocicorp/zero/server/adapters/postgresjs' const defineMutator = defineMutatorWithType< Schema, ZeroContext, PostgresJsTransaction >() const defineMutators = defineMutatorsWithType() Mutator Registries The result of defineMutator is a MutatorDefinition. By itself this isn't super useful. You need to register it using defineMutators: export const mutators = defineMutators({ issue: { update: updateIssue } }) Typically these are done together in one step: export const mutators = defineMutators({ issue: { update: defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.update({ id, title }) } ) } }) The result of defineMutators is called a MutatorRegistry. Each field in the registry is a callable Mutator that you can use to perform mutations: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: 'issue-123', title: 'New title' }) ) Mutator Names Each Mutator has a mutatorName which is computed by defineMutators. When you run a mutator, Zero sends this name along with the arguments to your server to execute the server-side mutation. console.log(mutators.issue.update.mutatorName) // \"issue.update\" mutators.ts By convention, mutators are listed in a central mutators.ts file. This allows them to be easily used on both the client and server: import {defineMutators, defineMutator} from '@rocicorp/zero' import {zql} from './schema.ts' import {z} from 'zod' export const mutators = defineMutators({ posts: { create: defineMutator( z.object({ id: z.string(), title: z.string() }), async ({ tx, context: {userID}, args: {id, title} }) => { await tx.mutate.post.insert({ id, title, authorID: userID }) } ), update: defineMutator( z.object({ id: z.string(), title: z.string().optional() }), async ({ tx, context: {userID}, args: {id, title} }) => { const prev = await tx.run( zql.post.where('id', id).one() ) if (prev?.authorID !== userID) { throw new Error(`Access denied`) } await tx.mutate.post.update({ id, title, authorID: userID }) } ) } }) You can use as many levels of nesting as you want to organize your mutators. As your application grows, you can move mutators to different files to keep them organized: // posts.ts export const postMutators = { create: defineMutator( z.object({ id: z.string(), title: z.string(), }), async ({tx, context: {userID}, args: {id, title}}) => { await tx.mutate.post.insert({ id, title, authorID: userID, }) }, ), } // user.ts export const userMutators = { updateRole: defineMutator( z.object({ role: z.string(), }), async ({tx, ctx: {userID}, args: {role}}) => { await tx.mutate.user.update({ id: userID, role, }) }, ), } // mutators.ts import {postMutators} from 'zero/mutators/posts.ts' import {userMutators} from 'zero/mutators/users.ts' export const mutators = defineMutators{{ posts: postMutators, users: userMutators, }) defineMutators establishes the full name for each mutator (i.e., posts.create, users.updateRole), which is later sent to the server. So this should only be used once at the top level of your mutators.ts file. Registration Before you can use your mutators, you need to register them with Zero: import {ZeroProvider} from '@rocicorp/zero/react' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } return ( )import {ZeroProvider} from '@rocicorp/zero/solid' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } return ( )import {Zero} from '@rocicorp/zero' import type {ZeroOptions} from '@rocicorp/zero' import {mutators} from 'zero/mutators.ts' const opts: ZeroOptions = { // ... cacheURL, schema, etc. mutators } const zero = new Zero(opts) Mutators need to be registered with Zero because Zero calls them during sync for conflict resolution. If you invoke a mutator that is not registered, Zero will throw an error. Server Setup In order for mutations to sync, you must provide an implementation of the mutate endpoint on your server. zero-cache calls this endpoint to process each mutation. Registering the Endpoint Use ZERO_MUTATE_URL to tell zero-cache where to find your mutate implementation: export ZERO_MUTATE_URL=\"http://localhost:3000/api/zero/mutate\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleMutateRequest and mustGetMutator functions to implement the endpoint. Plug in whatever dbProvider you set up (see server-zql or the install guide). // src/routes/api/zero/mutate.ts import {createFileRoute} from '@tanstack/react-router' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request, userID: null }) return Response.json(result) } } } })// app/api/zero/mutate/route.ts import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(request: Request) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request, userID: null }) return Response.json(result) }// src/routes/api/zero/mutate.ts import type {APIEvent} from '@solidjs/start/server' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(event: APIEvent) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {Hono} from 'hono' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from './db-provider.ts' const app = new Hono() app.post('/api/zero/mutate', async c => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request: c.req.raw, userID: null }) return c.json(result) }) Zero includes several built-in database adapters. You can also easily create your own. See ZQL on the Server for more information. handleMutateRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetMutator looks up the mutator in the registry and throws an error if not found. The mutator.fn function is your mutator implementation wrapped in the validator you provided. These examples have only public mutators, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the mutate handler. See Authentication. Handling Errors The handleMutateRequest function skips any mutations that throw: const result = await handleMutateRequest({ dbProvider, handler: transact => transact(async (tx, name, args) => { // The mutation is skipped and the next mutation runs as normal. // The optimistic mutation on the client will be reverted. throw new Error('bonk') }), request: c.req.raw, userID: null }) handleMutateRequest catches such errors and turns them into a structured response that gets sent back to the client. You can recover the errors and show UI if you want. It is also of course possible for the entire push endpoint to return an HTTP error, or to not reply at all: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async () => { throw new Error('zonk') // will trigger resend } } } })export async function POST() { throw new Error('zonk') // will trigger resend }export async function POST() { throw new Error('zonk') // will trigger resend }app.post('/api/zero/mutate', async c => { // This will cause the client to resend all queued mutations. throw new Error('zonk') }) Responses other than 200, 401, or 403 enter the error state. zero-cache will retry on 5xx up to four times before returning an error. If Zero receives HTTP 401 or 403, the client will enter the needs auth state and require a manual reconnect. Use zero.connection.connect() for cookie auth or zero.connection.connect({auth: newToken}) for token auth, then Zero will retry all queued mutations. If you want a different behavior, it is possible to implement the mutate endpoint yourself and handle errors differently. Retrying Mutators Some errors go away on a second try. At the repeatable read and serializable isolation levels, for example, Postgres can abort a transaction that conflicts with a concurrent one. It fails with a serialization failure, code 40001. To have Zero run a server mutator again in a fresh transaction when it fails with an error like that, call tx.retryOn(): function isSerializationFailure(error: unknown) { return ( error instanceof Error && 'code' in error && error.code === '40001' ) } const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { if (tx.location === 'server') { tx.retryOn(isSerializationFailure) } await tx.mutate.issue.update({id, title}) } ) retryOn() takes a predicate. It gets the error and the attempt number: 1 when deciding the first re-run, 2 for the second, and so on. Return true to run the mutator again. If it returns anything else or throws, Zero records the error and skips the mutation, as with any other error. The predicate sees errors the mutator throws, and also errors Postgres raises at COMMIT, which the mutator can't catch. Everything the failed run wrote is rolled back, and the re-run reads fresh data. The error is passed as it was thrown. node-postgres and postgres.js errors have the Postgres error code in code. Other libraries can wrap the error, so check what yours throws. The predicate can also return a promise. Zero waits for it, so you can use it to back off before the next run: tx.retryOn(async (error, attempt) => { if (!isSerializationFailure(error)) { return false } // Wait 50ms, then 100ms, then 150ms, and so on. await new Promise(resolve => setTimeout(resolve, attempt * 50) ) return true }) retryOn() only applies to the current run. The re-run starts the mutator from the top and calls retryOn() again, so call it before anything that can fail. You can call it more than once, and Zero runs the mutator again if any predicate returns true. Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run also fails, Zero records the error. The predicate runs after the transaction has ended, so it can't use tx: reading or writing through tx there throws. Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. retryOn() only exists on the server, when tx.location === 'server'. The built-in adapters and ZQLDatabase support retryOn(). If you pass your own Database to handleMutateRequest, it must pass the addRetryPredicate it receives in TransactionProviderInput on to the transaction it creates with CRUDMutatorFactory.createTransaction(). Otherwise tx.retryOn() throws. Custom Mutate URL By default, Zero sends mutations to the URL specified in the ZERO_MUTATE_URL parameter. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in the ZERO_MUTATE_URL parameter: export ZERO_MUTATE_URL=\"https://api.example.com/mutate,https://api.staging.example.com/mutate\" Then choose one of those URLs by passing it to mutateURL on the Zero constructor: const opts: ZeroOptions = { // ... mutateURL: 'https://api.staging.example.com/mutate' } URL Patterns The strings listed in ZERO_MUTATE_URL can also be URLPatterns: export ZERO_MUTATE_URL=\"https://mybranch-*.preview.myapp.com/mutate\" For more information, see the URLPattern section of the Queries docs. It works the same way for mutations. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Server-Specific Code To implement server-specific code, just run different mutators in your mutate endpoint. Server authority to the rescue! defineMutators accepts a baseMutators parameter that makes this easy. The returned mutator registry will contain all the mutators from baseMutators, plus any new ones you define or override: // server-mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' import {mutators as sharedMutators} from 'mutators.ts' export const serverMutators = defineMutators( sharedMutators, { posts: { // Overrides the shared mutator definition with same name. update: defineMutator( z.object({ id: z.string(), title: z.string().optional(), priority: z.number().optional() }), async ({ tx, ctx: {userID}, args: {id, title, priority} }) => { // Run the shared mutator first. await sharedMutators.posts.update.fn({ tx, ctx, args }) // Record a history of this operation happening in an audit log table. await tx.mutate.auditLog.insert({ issueId: id, action: 'update-title', timestamp: Date.getTime() }) } ) } } ) For simple things, we also expose a location field on the transaction object that you can use to branch your code: const myMutator = defineMutator(async ({tx}) => { if (tx.location === 'client') { // Client-side code } else { // Server-side code } }) Running Mutators Once you have registered your mutators, you can invoke them with zero.mutate: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: crypto.randomUUID(), title: 'New title' }) ) Client-generated random IDs from crypto.randomUUID(), uuid, ulid, or nanoid work much better with sync engines like Zero. See IDs for more details. Waiting for Results We typically recommend that you \"fire and forget\" mutators. Optimistic mutations make sense when the common case is that a mutation succeeds. If a mutation frequently fails, then showing the user an optimistic result isn't very useful, because it will likely be wrong. That said there are cases where it is nice to know when a write succeeded on either the client or server. One example is if you need to read a row directly after writing it. Zero's local writes are very fast (almost always < 1 frame), but because Zero is backed by IndexedDB, writes are still technically asynchronous and reads directly after a write may not return the new data. You can use the .client promise in this case to wait for a write to complete on the client side: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) // issue-123 not guaranteed to be present here. read1 may be undefined. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await client write – almost always less than 1 frame, and same // macrotask, so no browser paint will occur here. const res = await write.client if (res.type === 'error') { console.error('Mutator failed on client', res.error) } // issue-123 definitely can be read now. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) You can also await .server for the server result: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) const clientRes = await write.client if (clientRes.type === 'error') { throw new Error(`Mutator failed on client`, { cause: clientRes.error }) } // optimistic write guaranteed to be present here, but not // server write. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await the server result/acknowledgment. This requires a round trip. const serverRes = await write.server if (serverRes.type === 'error') { throw new Error(`Mutator failed on server`, { cause: serverRes.error }) } // The server acknowledged the mutation, but its Postgres changes // may not have replicated to this client yet. This read can still // reflect optimistic rather than authoritative state. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) If the client-side mutator fails, .server also resolves to an error result. Awaiting .server therefore covers both client- and server-side failures. Returning Data A mutator can return a JSON value. The caller gets it in the data field of the success result. The client and server runs can return different values. For example, the server can return a value that only the database knows: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.insert({id, title}) if (tx.location === 'client') { // The client doesn't know the short ID yet. return {shortID: null} } // Postgres assigns shortID when the row is inserted. const issue = await tx.run( zql.issue.where('id', id).one() ) return {shortID: issue?.shortID ?? null} } ) The .client result's data is what the client run returned, and the .server result's data is what the server run returned: const write = zero.mutate( mutators.issue.create({ id: crypto.randomUUID(), title: 'New title' }) ) const serverRes = await write.server if (serverRes.type === 'success') { // `data` is a plain JSON value, so check its shape. const {shortID} = z .object({shortID: z.number().nullable()}) .parse(serverRes.data) console.log('Short ID:', shortID) } data is typed as ReadonlyJSONValue | undefined rather than as the mutator's return type, so check its shape before you use it. A mutator that returns nothing gives undefined. The server's value only reaches the client if your mutate handler returns the mutator's result, as return mutator.fn({args, tx}) does in the examples above. Zero also saves that value in your database with the mutation's result, so the client still gets it if the response from your mutate endpoint is lost. Nothing is saved for a mutator that returns nothing. Permissions Because mutators are just normal TypeScript functions that run server-side, there is no need for a special permissions system. You can implement whatever permission checks you want using plain TypeScript code. See Permissions for more information. Dropping Down to Raw SQL The ServerTransaction interface has a dbTransaction property that exposes the underlying database connection. This allows you to run raw SQL queries directly against the database. This is useful for complex queries, or for using Postgres features that Zero doesn't support yet: const markAllAsRead = defineMutator( z.object({ userId: z.string() }), async ({tx, args: {userId}}) => { // shared stuff ... if (tx.location === 'server') { // `tx` is now narrowed to `ServerTransaction`. // Do special server-only stuff with raw SQL. await tx.dbTransaction.query( ` UPDATE notification SET read = true WHERE user_id = $1 `, [userId] ) } } ) See ZQL on the Server for more information. Notifications and Async Work The best way to handle notifications and async work is a transactional outbox. This ensures that notifications actually do eventually get sent, without holding open database transactions to talk over the network. This can be implemented very easily in Zero by writing notifications to an outbox table as part of your mutator, then processing that table periodically with a background job. However sometimes it's still nice to do a quick and dirty async send as part of a mutation, for example early on in development, or to record metrics. For this, the createMutators pattern is useful: // server-mutators.ts import {defineMutator} from '@rocicorp/zero' import z from 'zod' import {zql} from 'schema.ts' import {mutators as clientMutators} from 'mutators.ts' // Instead of defining server mutators as a constant, // define them as a function of a list of async tasks. export function createMutators( asyncTasks: Array<() => Promise> ) { return defineMutators(clientMutators, { issue: { update: defineMutator( z.object({ id: z.string(), title: z.string() }), async (tx, {id, title}) => { await tx.mutate.issue.update({id, title}) asyncTasks.push(() => sendEmailToSubscribers(id)) } ) } }) } Then in your mutate handler: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ tx, args }) }), request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled( asyncTasks.map(task => task()) ) return Response.json(result) } } } })export async function POST(request: Request) { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({tx, args}) }), request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return Response.json(result) }export async function POST(event: APIEvent) { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({tx, args}) }), request: event.request, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return Response.json(result) }app.post('/api/zero/mutate', async c => { const asyncTasks: Array<() => Promise> = [] const mutators = createMutators(asyncTasks) const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ tx, args }) }), request: c.req.raw, userID: null }) // Run all async tasks // If any fail, do not block the response, since the // mutation result has already been written to the database. await Promise.allSettled(asyncTasks.map(task => task())) return c.json(result) }) Custom Mutate Implementation You can manually implement the mutate endpoint in any programming language. This will be documented in the future, but you can refer to the handleMutateRequest source code for an example for now.", "headings": [ { "text": "Architecture", @@ -2216,7 +2216,7 @@ "sectionTitle": "Server Setup", "sectionId": "server-setup", "url": "/docs/mutators", - "content": "In order for mutations to sync, you must provide an implementation of the mutate endpoint on your server. zero-cache calls this endpoint to process each mutation. Registering the Endpoint Use ZERO_MUTATE_URL to tell zero-cache where to find your mutate implementation: export ZERO_MUTATE_URL=\"http://localhost:3000/api/zero/mutate\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleMutateRequest and mustGetMutator functions to implement the endpoint. Plug in whatever dbProvider you set up (see server-zql or the install guide). // src/routes/api/zero/mutate.ts import {createFileRoute} from '@tanstack/react-router' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request, userID: null }) return Response.json(result) } } } })// app/api/zero/mutate/route.ts import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(request: Request) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request, userID: null }) return Response.json(result) }// src/routes/api/zero/mutate.ts import type {APIEvent} from '@solidjs/start/server' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(event: APIEvent) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {Hono} from 'hono' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from './db-provider.ts' const app = new Hono() app.post('/api/zero/mutate', async c => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request: c.req.raw, userID: null }) return c.json(result) }) Zero includes several built-in database adapters. You can also easily create your own. See ZQL on the Server for more information. handleMutateRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetMutator looks up the mutator in the registry and throws an error if not found. The mutator.fn function is your mutator implementation wrapped in the validator you provided. These examples have only public mutators, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the mutate handler. See Authentication. Handling Errors The handleMutateRequest function skips any mutations that throw: const result = await handleMutateRequest({ dbProvider, handler: transact => transact(async (tx, name, args) => { // The mutation is skipped and the next mutation runs as normal. // The optimistic mutation on the client will be reverted. throw new Error('bonk') }), request: c.req.raw, userID: null }) handleMutateRequest catches such errors and turns them into a structured response that gets sent back to the client. You can recover the errors and show UI if you want. It is also of course possible for the entire push endpoint to return an HTTP error, or to not reply at all: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async () => { throw new Error('zonk') // will trigger resend } } } })export async function POST() { throw new Error('zonk') // will trigger resend }export async function POST() { throw new Error('zonk') // will trigger resend }app.post('/api/zero/mutate', async c => { // This will cause the client to resend all queued mutations. throw new Error('zonk') }) Responses other than 200, 401, or 403 enter the error state. zero-cache will retry on 5xx up to four times before returning an error. If Zero receives HTTP 401 or 403, the client will enter the needs auth state and require a manual reconnect. Use zero.connection.connect() for cookie auth or zero.connection.connect({auth: newToken}) for token auth, then Zero will retry all queued mutations. If you want a different behavior, it is possible to implement the mutate endpoint yourself and handle errors differently. Retrying Mutators Some errors go away if you try again. For example, at the repeatable read or serializable isolation level, Postgres can abort a transaction that conflicts with a concurrent one. The error is a serialization failure, code 40001. Call tx.retryOn() in a server mutator to have Zero run the mutator again, in a fresh transaction, when it fails with an error you choose: function isSerializationFailure(error: unknown) { return ( error instanceof Error && 'code' in error && error.code === '40001' ) } const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { if (tx.location === 'server') { tx.retryOn(isSerializationFailure) } await tx.mutate.issue.update({id, title}) } ) retryOn() takes a predicate. It gets the error and the attempt number: 1 when deciding the first re-run, 2 for the second, and so on. Return true to run the mutator again. If it returns anything else or throws, Zero records the error and skips the mutation, as with any other error. The predicate sees errors the mutator throws, and also errors Postgres raises at COMMIT, which the mutator can't catch. Everything the failed run wrote is rolled back, and the re-run reads fresh data. The error is passed as it was thrown. node-postgres and postgres.js errors have the Postgres error code in code. Other libraries can wrap the error, so check what yours throws. The predicate can also return a promise. Zero waits for it, so you can use it to back off before the next run: tx.retryOn(async (error, attempt) => { if (!isSerializationFailure(error)) { return false } // Wait 50ms, then 100ms, then 150ms, and so on. await new Promise(resolve => setTimeout(resolve, attempt * 50) ) return true }) A few things to know: retryOn() covers only the current run. The re-run calls it again because it runs the mutator again. Call it before anything that can fail. You can call retryOn() more than once. Zero runs the mutator again if any predicate returns true. Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run fails, Zero records the error. The predicate runs after the transaction has ended, so it can't use tx. Reading or writing through tx there throws. Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. retryOn() is only available on the server, when tx.location === 'server'. The built-in adapters and ZQLDatabase support retryOn(). If you pass your own Database to handleMutateRequest, it must pass the addRetryPredicate it receives in TransactionProviderInput on to the transaction it creates with CRUDMutatorFactory.createTransaction(). Otherwise tx.retryOn() throws. Custom Mutate URL By default, Zero sends mutations to the URL specified in the ZERO_MUTATE_URL parameter. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in the ZERO_MUTATE_URL parameter: export ZERO_MUTATE_URL=\"https://api.example.com/mutate,https://api.staging.example.com/mutate\" Then choose one of those URLs by passing it to mutateURL on the Zero constructor: const opts: ZeroOptions = { // ... mutateURL: 'https://api.staging.example.com/mutate' } URL Patterns The strings listed in ZERO_MUTATE_URL can also be URLPatterns: export ZERO_MUTATE_URL=\"https://mybranch-*.preview.myapp.com/mutate\" For more information, see the URLPattern section of the Queries docs. It works the same way for mutations. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Server-Specific Code To implement server-specific code, just run different mutators in your mutate endpoint. Server authority to the rescue! defineMutators accepts a baseMutators parameter that makes this easy. The returned mutator registry will contain all the mutators from baseMutators, plus any new ones you define or override: // server-mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' import {mutators as sharedMutators} from 'mutators.ts' export const serverMutators = defineMutators( sharedMutators, { posts: { // Overrides the shared mutator definition with same name. update: defineMutator( z.object({ id: z.string(), title: z.string().optional(), priority: z.number().optional() }), async ({ tx, ctx: {userID}, args: {id, title, priority} }) => { // Run the shared mutator first. await sharedMutators.posts.update.fn({ tx, ctx, args }) // Record a history of this operation happening in an audit log table. await tx.mutate.auditLog.insert({ issueId: id, action: 'update-title', timestamp: Date.getTime() }) } ) } } ) For simple things, we also expose a location field on the transaction object that you can use to branch your code: const myMutator = defineMutator(async ({tx}) => { if (tx.location === 'client') { // Client-side code } else { // Server-side code } })", + "content": "In order for mutations to sync, you must provide an implementation of the mutate endpoint on your server. zero-cache calls this endpoint to process each mutation. Registering the Endpoint Use ZERO_MUTATE_URL to tell zero-cache where to find your mutate implementation: export ZERO_MUTATE_URL=\"http://localhost:3000/api/zero/mutate\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleMutateRequest and mustGetMutator functions to implement the endpoint. Plug in whatever dbProvider you set up (see server-zql or the install guide). // src/routes/api/zero/mutate.ts import {createFileRoute} from '@tanstack/react-router' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async ({request}) => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request, userID: null }) return Response.json(result) } } } })// app/api/zero/mutate/route.ts import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(request: Request) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request, userID: null }) return Response.json(result) }// src/routes/api/zero/mutate.ts import type {APIEvent} from '@solidjs/start/server' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from 'db-provider.ts' export async function POST(event: APIEvent) { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({args, tx}) }), request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {Hono} from 'hono' import {handleMutateRequest} from '@rocicorp/zero/server' import {mustGetMutator} from '@rocicorp/zero' import {mutators} from 'mutators.ts' import {dbProvider} from './db-provider.ts' const app = new Hono() app.post('/api/zero/mutate', async c => { const result = await handleMutateRequest({ dbProvider, handler: transact => transact((tx, name, args) => { const mutator = mustGetMutator(mutators, name) return mutator.fn({ args, tx }) }), request: c.req.raw, userID: null }) return c.json(result) }) Zero includes several built-in database adapters. You can also easily create your own. See ZQL on the Server for more information. handleMutateRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetMutator looks up the mutator in the registry and throws an error if not found. The mutator.fn function is your mutator implementation wrapped in the validator you provided. These examples have only public mutators, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the mutate handler. See Authentication. Handling Errors The handleMutateRequest function skips any mutations that throw: const result = await handleMutateRequest({ dbProvider, handler: transact => transact(async (tx, name, args) => { // The mutation is skipped and the next mutation runs as normal. // The optimistic mutation on the client will be reverted. throw new Error('bonk') }), request: c.req.raw, userID: null }) handleMutateRequest catches such errors and turns them into a structured response that gets sent back to the client. You can recover the errors and show UI if you want. It is also of course possible for the entire push endpoint to return an HTTP error, or to not reply at all: export const Route = createFileRoute('/api/zero/mutate')({ server: { handlers: { POST: async () => { throw new Error('zonk') // will trigger resend } } } })export async function POST() { throw new Error('zonk') // will trigger resend }export async function POST() { throw new Error('zonk') // will trigger resend }app.post('/api/zero/mutate', async c => { // This will cause the client to resend all queued mutations. throw new Error('zonk') }) Responses other than 200, 401, or 403 enter the error state. zero-cache will retry on 5xx up to four times before returning an error. If Zero receives HTTP 401 or 403, the client will enter the needs auth state and require a manual reconnect. Use zero.connection.connect() for cookie auth or zero.connection.connect({auth: newToken}) for token auth, then Zero will retry all queued mutations. If you want a different behavior, it is possible to implement the mutate endpoint yourself and handle errors differently. Retrying Mutators Some errors go away on a second try. At the repeatable read and serializable isolation levels, for example, Postgres can abort a transaction that conflicts with a concurrent one. It fails with a serialization failure, code 40001. To have Zero run a server mutator again in a fresh transaction when it fails with an error like that, call tx.retryOn(): function isSerializationFailure(error: unknown) { return ( error instanceof Error && 'code' in error && error.code === '40001' ) } const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { if (tx.location === 'server') { tx.retryOn(isSerializationFailure) } await tx.mutate.issue.update({id, title}) } ) retryOn() takes a predicate. It gets the error and the attempt number: 1 when deciding the first re-run, 2 for the second, and so on. Return true to run the mutator again. If it returns anything else or throws, Zero records the error and skips the mutation, as with any other error. The predicate sees errors the mutator throws, and also errors Postgres raises at COMMIT, which the mutator can't catch. Everything the failed run wrote is rolled back, and the re-run reads fresh data. The error is passed as it was thrown. node-postgres and postgres.js errors have the Postgres error code in code. Other libraries can wrap the error, so check what yours throws. The predicate can also return a promise. Zero waits for it, so you can use it to back off before the next run: tx.retryOn(async (error, attempt) => { if (!isSerializationFailure(error)) { return false } // Wait 50ms, then 100ms, then 150ms, and so on. await new Promise(resolve => setTimeout(resolve, attempt * 50) ) return true }) retryOn() only applies to the current run. The re-run starts the mutator from the top and calls retryOn() again, so call it before anything that can fail. You can call it more than once, and Zero runs the mutator again if any predicate returns true. Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run also fails, Zero records the error. The predicate runs after the transaction has ended, so it can't use tx: reading or writing through tx there throws. Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. retryOn() only exists on the server, when tx.location === 'server'. The built-in adapters and ZQLDatabase support retryOn(). If you pass your own Database to handleMutateRequest, it must pass the addRetryPredicate it receives in TransactionProviderInput on to the transaction it creates with CRUDMutatorFactory.createTransaction(). Otherwise tx.retryOn() throws. Custom Mutate URL By default, Zero sends mutations to the URL specified in the ZERO_MUTATE_URL parameter. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in the ZERO_MUTATE_URL parameter: export ZERO_MUTATE_URL=\"https://api.example.com/mutate,https://api.staging.example.com/mutate\" Then choose one of those URLs by passing it to mutateURL on the Zero constructor: const opts: ZeroOptions = { // ... mutateURL: 'https://api.staging.example.com/mutate' } URL Patterns The strings listed in ZERO_MUTATE_URL can also be URLPatterns: export ZERO_MUTATE_URL=\"https://mybranch-*.preview.myapp.com/mutate\" For more information, see the URLPattern section of the Queries docs. It works the same way for mutations. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Server-Specific Code To implement server-specific code, just run different mutators in your mutate endpoint. Server authority to the rescue! defineMutators accepts a baseMutators parameter that makes this easy. The returned mutator registry will contain all the mutators from baseMutators, plus any new ones you define or override: // server-mutators.ts import {defineMutators, defineMutator} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' import {mutators as sharedMutators} from 'mutators.ts' export const serverMutators = defineMutators( sharedMutators, { posts: { // Overrides the shared mutator definition with same name. update: defineMutator( z.object({ id: z.string(), title: z.string().optional(), priority: z.number().optional() }), async ({ tx, ctx: {userID}, args: {id, title, priority} }) => { // Run the shared mutator first. await sharedMutators.posts.update.fn({ tx, ctx, args }) // Record a history of this operation happening in an audit log table. await tx.mutate.auditLog.insert({ issueId: id, action: 'update-title', timestamp: Date.getTime() }) } ) } } ) For simple things, we also expose a location field on the transaction object that you can use to branch your code: const myMutator = defineMutator(async ({tx}) => { if (tx.location === 'client') { // Client-side code } else { // Server-side code } })", "kind": "section" }, { @@ -2256,7 +2256,7 @@ "sectionTitle": "Retrying Mutators", "sectionId": "retrying-mutators", "url": "/docs/mutators", - "content": "Some errors go away if you try again. For example, at the repeatable read or serializable isolation level, Postgres can abort a transaction that conflicts with a concurrent one. The error is a serialization failure, code 40001. Call tx.retryOn() in a server mutator to have Zero run the mutator again, in a fresh transaction, when it fails with an error you choose: function isSerializationFailure(error: unknown) { return ( error instanceof Error && 'code' in error && error.code === '40001' ) } const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { if (tx.location === 'server') { tx.retryOn(isSerializationFailure) } await tx.mutate.issue.update({id, title}) } ) retryOn() takes a predicate. It gets the error and the attempt number: 1 when deciding the first re-run, 2 for the second, and so on. Return true to run the mutator again. If it returns anything else or throws, Zero records the error and skips the mutation, as with any other error. The predicate sees errors the mutator throws, and also errors Postgres raises at COMMIT, which the mutator can't catch. Everything the failed run wrote is rolled back, and the re-run reads fresh data. The error is passed as it was thrown. node-postgres and postgres.js errors have the Postgres error code in code. Other libraries can wrap the error, so check what yours throws. The predicate can also return a promise. Zero waits for it, so you can use it to back off before the next run: tx.retryOn(async (error, attempt) => { if (!isSerializationFailure(error)) { return false } // Wait 50ms, then 100ms, then 150ms, and so on. await new Promise(resolve => setTimeout(resolve, attempt * 50) ) return true }) A few things to know: retryOn() covers only the current run. The re-run calls it again because it runs the mutator again. Call it before anything that can fail. You can call retryOn() more than once. Zero runs the mutator again if any predicate returns true. Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run fails, Zero records the error. The predicate runs after the transaction has ended, so it can't use tx. Reading or writing through tx there throws. Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. retryOn() is only available on the server, when tx.location === 'server'. The built-in adapters and ZQLDatabase support retryOn(). If you pass your own Database to handleMutateRequest, it must pass the addRetryPredicate it receives in TransactionProviderInput on to the transaction it creates with CRUDMutatorFactory.createTransaction(). Otherwise tx.retryOn() throws.", + "content": "Some errors go away on a second try. At the repeatable read and serializable isolation levels, for example, Postgres can abort a transaction that conflicts with a concurrent one. It fails with a serialization failure, code 40001. To have Zero run a server mutator again in a fresh transaction when it fails with an error like that, call tx.retryOn(): function isSerializationFailure(error: unknown) { return ( error instanceof Error && 'code' in error && error.code === '40001' ) } const updateIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { if (tx.location === 'server') { tx.retryOn(isSerializationFailure) } await tx.mutate.issue.update({id, title}) } ) retryOn() takes a predicate. It gets the error and the attempt number: 1 when deciding the first re-run, 2 for the second, and so on. Return true to run the mutator again. If it returns anything else or throws, Zero records the error and skips the mutation, as with any other error. The predicate sees errors the mutator throws, and also errors Postgres raises at COMMIT, which the mutator can't catch. Everything the failed run wrote is rolled back, and the re-run reads fresh data. The error is passed as it was thrown. node-postgres and postgres.js errors have the Postgres error code in code. Other libraries can wrap the error, so check what yours throws. The predicate can also return a promise. Zero waits for it, so you can use it to back off before the next run: tx.retryOn(async (error, attempt) => { if (!isSerializationFailure(error)) { return false } // Wait 50ms, then 100ms, then 150ms, and so on. await new Promise(resolve => setTimeout(resolve, attempt * 50) ) return true }) retryOn() only applies to the current run. The re-run starts the mutator from the top and calls retryOn() again, so call it before anything that can fail. You can call it more than once, and Zero runs the mutator again if any predicate returns true. Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run also fails, Zero records the error. The predicate runs after the transaction has ended, so it can't use tx: reading or writing through tx there throws. Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. retryOn() only exists on the server, when tx.location === 'server'. The built-in adapters and ZQLDatabase support retryOn(). If you pass your own Database to handleMutateRequest, it must pass the addRetryPredicate it receives in TransactionProviderInput on to the transaction it creates with CRUDMutatorFactory.createTransaction(). Otherwise tx.retryOn() throws.", "kind": "section" }, { @@ -2296,7 +2296,7 @@ "sectionTitle": "Running Mutators", "sectionId": "running-mutators", "url": "/docs/mutators", - "content": "Once you have registered your mutators, you can invoke them with zero.mutate: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: crypto.randomUUID(), title: 'New title' }) ) Client-generated random IDs from crypto.randomUUID(), uuid, ulid, or nanoid work much better with sync engines like Zero. See IDs for more details. Waiting for Results We typically recommend that you \"fire and forget\" mutators. Optimistic mutations make sense when the common case is that a mutation succeeds. If a mutation frequently fails, then showing the user an optimistic result isn't very useful, because it will likely be wrong. That said there are cases where it is nice to know when a write succeeded on either the client or server. One example is if you need to read a row directly after writing it. Zero's local writes are very fast (almost always < 1 frame), but because Zero is backed by IndexedDB, writes are still technically asynchronous and reads directly after a write may not return the new data. You can use the .client promise in this case to wait for a write to complete on the client side: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) // issue-123 not guaranteed to be present here. read1 may be undefined. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await client write – almost always less than 1 frame, and same // macrotask, so no browser paint will occur here. const res = await write.client if (res.type === 'error') { console.error('Mutator failed on client', res.error) } // issue-123 definitely can be read now. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) You can also await .server for the server result: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) const clientRes = await write.client if (clientRes.type === 'error') { throw new Error(`Mutator failed on client`, { cause: clientRes.error }) } // optimistic write guaranteed to be present here, but not // server write. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await the server result/acknowledgment. This requires a round trip. const serverRes = await write.server if (serverRes.type === 'error') { throw new Error(`Mutator failed on server`, { cause: serverRes.error }) } // The server acknowledged the mutation, but its Postgres changes // may not have replicated to this client yet. This read can still // reflect optimistic rather than authoritative state. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) If the client-side mutator fails, .server also resolves to an error result. Awaiting .server therefore covers both client- and server-side failures. Returning Data A mutator can return a JSON value. The caller gets it in the data field of the success result. The client and server runs can return different values. For example, the server can return a value that only the database knows: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.insert({id, title}) if (tx.location === 'client') { // The client doesn't know the short ID yet. return {shortID: null} } // Postgres assigns shortID when the row is inserted. const issue = await tx.run( zql.issue.where('id', id).one() ) return {shortID: issue?.shortID ?? null} } ) The data of the .client result is what the client run returned. The data of the .server result is what the server run returned: const write = zero.mutate( mutators.issue.create({ id: crypto.randomUUID(), title: 'New title' }) ) const serverRes = await write.server if (serverRes.type === 'success') { // `data` is a plain JSON value, so check its shape. const {shortID} = z .object({shortID: z.number().nullable()}) .parse(serverRes.data) console.log('Short ID:', shortID) } A few things to know: data is typed as ReadonlyJSONValue | undefined, not as the mutator's return type. Check its shape before you use it. If the mutator returns nothing, data is undefined. The server value only reaches the client if your mutate handler returns the mutator's result, as return mutator.fn({args, tx}) does in the examples above. Zero saves the server's return value in your database along with the mutation's result, so the client still gets it if the response from your mutate endpoint is lost. A mutator that returns nothing doesn't write this.", + "content": "Once you have registered your mutators, you can invoke them with zero.mutate: import {mutators} from 'mutators.ts' zero.mutate( mutators.issue.update({ id: crypto.randomUUID(), title: 'New title' }) ) Client-generated random IDs from crypto.randomUUID(), uuid, ulid, or nanoid work much better with sync engines like Zero. See IDs for more details. Waiting for Results We typically recommend that you \"fire and forget\" mutators. Optimistic mutations make sense when the common case is that a mutation succeeds. If a mutation frequently fails, then showing the user an optimistic result isn't very useful, because it will likely be wrong. That said there are cases where it is nice to know when a write succeeded on either the client or server. One example is if you need to read a row directly after writing it. Zero's local writes are very fast (almost always < 1 frame), but because Zero is backed by IndexedDB, writes are still technically asynchronous and reads directly after a write may not return the new data. You can use the .client promise in this case to wait for a write to complete on the client side: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) // issue-123 not guaranteed to be present here. read1 may be undefined. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await client write – almost always less than 1 frame, and same // macrotask, so no browser paint will occur here. const res = await write.client if (res.type === 'error') { console.error('Mutator failed on client', res.error) } // issue-123 definitely can be read now. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) You can also await .server for the server result: const write = zero.mutate( mutators.issue.insert({ id: crypto.randomUUID(), title: 'New title' }) ) const clientRes = await write.client if (clientRes.type === 'error') { throw new Error(`Mutator failed on client`, { cause: clientRes.error }) } // optimistic write guaranteed to be present here, but not // server write. const read1 = await zero.run( queries.issue.byId('issue-123').one() ) // Await the server result/acknowledgment. This requires a round trip. const serverRes = await write.server if (serverRes.type === 'error') { throw new Error(`Mutator failed on server`, { cause: serverRes.error }) } // The server acknowledged the mutation, but its Postgres changes // may not have replicated to this client yet. This read can still // reflect optimistic rather than authoritative state. const read2 = await zero.run( queries.issue.byId('issue-123').one() ) If the client-side mutator fails, .server also resolves to an error result. Awaiting .server therefore covers both client- and server-side failures. Returning Data A mutator can return a JSON value. The caller gets it in the data field of the success result. The client and server runs can return different values. For example, the server can return a value that only the database knows: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.insert({id, title}) if (tx.location === 'client') { // The client doesn't know the short ID yet. return {shortID: null} } // Postgres assigns shortID when the row is inserted. const issue = await tx.run( zql.issue.where('id', id).one() ) return {shortID: issue?.shortID ?? null} } ) The .client result's data is what the client run returned, and the .server result's data is what the server run returned: const write = zero.mutate( mutators.issue.create({ id: crypto.randomUUID(), title: 'New title' }) ) const serverRes = await write.server if (serverRes.type === 'success') { // `data` is a plain JSON value, so check its shape. const {shortID} = z .object({shortID: z.number().nullable()}) .parse(serverRes.data) console.log('Short ID:', shortID) } data is typed as ReadonlyJSONValue | undefined rather than as the mutator's return type, so check its shape before you use it. A mutator that returns nothing gives undefined. The server's value only reaches the client if your mutate handler returns the mutator's result, as return mutator.fn({args, tx}) does in the examples above. Zero also saves that value in your database with the mutation's result, so the client still gets it if the response from your mutate endpoint is lost. Nothing is saved for a mutator that returns nothing.", "kind": "section" }, { @@ -2316,7 +2316,7 @@ "sectionTitle": "Returning Data", "sectionId": "returning-data", "url": "/docs/mutators", - "content": "A mutator can return a JSON value. The caller gets it in the data field of the success result. The client and server runs can return different values. For example, the server can return a value that only the database knows: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.insert({id, title}) if (tx.location === 'client') { // The client doesn't know the short ID yet. return {shortID: null} } // Postgres assigns shortID when the row is inserted. const issue = await tx.run( zql.issue.where('id', id).one() ) return {shortID: issue?.shortID ?? null} } ) The data of the .client result is what the client run returned. The data of the .server result is what the server run returned: const write = zero.mutate( mutators.issue.create({ id: crypto.randomUUID(), title: 'New title' }) ) const serverRes = await write.server if (serverRes.type === 'success') { // `data` is a plain JSON value, so check its shape. const {shortID} = z .object({shortID: z.number().nullable()}) .parse(serverRes.data) console.log('Short ID:', shortID) } A few things to know: data is typed as ReadonlyJSONValue | undefined, not as the mutator's return type. Check its shape before you use it. If the mutator returns nothing, data is undefined. The server value only reaches the client if your mutate handler returns the mutator's result, as return mutator.fn({args, tx}) does in the examples above. Zero saves the server's return value in your database along with the mutation's result, so the client still gets it if the response from your mutate endpoint is lost. A mutator that returns nothing doesn't write this.", + "content": "A mutator can return a JSON value. The caller gets it in the data field of the success result. The client and server runs can return different values. For example, the server can return a value that only the database knows: const createIssue = defineMutator( z.object({id: z.string(), title: z.string()}), async ({tx, args: {id, title}}) => { await tx.mutate.issue.insert({id, title}) if (tx.location === 'client') { // The client doesn't know the short ID yet. return {shortID: null} } // Postgres assigns shortID when the row is inserted. const issue = await tx.run( zql.issue.where('id', id).one() ) return {shortID: issue?.shortID ?? null} } ) The .client result's data is what the client run returned, and the .server result's data is what the server run returned: const write = zero.mutate( mutators.issue.create({ id: crypto.randomUUID(), title: 'New title' }) ) const serverRes = await write.server if (serverRes.type === 'success') { // `data` is a plain JSON value, so check its shape. const {shortID} = z .object({shortID: z.number().nullable()}) .parse(serverRes.data) console.log('Short ID:', shortID) } data is typed as ReadonlyJSONValue | undefined rather than as the mutator's return type, so check its shape before you use it. A mutator that returns nothing gives undefined. The server's value only reaches the client if your mutate handler returns the mutator's result, as return mutator.fn({args, tx}) does in the examples above. Zero also saves that value in your database with the mutation's result, so the client still gets it if the response from your mutate endpoint is lost. Nothing is saved for a mutator that returns nothing.", "kind": "section" }, { @@ -2726,7 +2726,7 @@ "title": "Queries", "searchTitle": "Queries", "url": "/docs/queries", - "content": "Queries are how you read and sync data with Zero. Here's a simple example: // src/queries.ts import {defineQueries, defineQuery} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' export const queries = defineQueries({ postsByAuthor: defineQuery( z.object({authorID: z.string()}), ({args: {authorID}}) => zql.post.where('authorID', authorID) ) }) Architecture A copy of each query exists on both the client and on your server: Often the implementations will be the same, and you can just share their code. This is easy with full-stack frameworks like TanStack Start or Next.js. But the implementations don't have to be the same, or even compute the same result. For example, the server can add extra filters to enforce permissions that the client query does not. Life of a Query When a query is invoked, it initially runs on the client, against the client-side datastore. Any matching data is returned immediately and the user sees instant results. In the background, the name and arguments for the query are sent to zero-cache. Zero-cache calls the queries endpoint on your server to get the ZQL for the query. Your server looks up its implementation of the query, invokes it, and returns the resulting ZQL expression to zero-cache. Zero-cache then runs this ZQL against the server-side data. The initial server result is sent back to the client and the client query updates in response. zero-cache receives updates from Postgres via logical replication. It updates affected queries and sends row changes back to the client, which updates the client query, and the user sees the changes. Defining Queries Basics Create a query using defineQuery. The only required argument is a QueryFn, which must return a ZQL expression: import {zql} from 'schema.ts' const allPostsQueryDef = defineQuery(() => zql.post) Arguments The QueryFn can take a single args parameter. To enable this, pass a validator to defineQuery: import {zql} from 'schema.ts' const postsByAuthor = defineQuery( z.object({authorID: z.string().optional()}), ({args: {authorID}}) => { let q = zql.post if (authorID !== undefined) { q = q.where('authorID', authorID) } return q } ) We use Zod in these examples, but you can use any validation library that implements Standard Schema. Zero queries run on both the client and on your server. In the server case, the parameters come from the client and are untrusted. The validator ensures the data passed to your query is of the expected type. Query Registries The result of defineQuery is a QueryDefinition. By itself this isn't super useful. You need to register it using defineQueries: export const queries = defineQueries({ posts: { all: allPostsQueryDef } }) Typically these are done together in one step: export const queries = defineQueries({ posts: { all: defineQuery(() => zql.post) } }) The result of defineQueries is called a QueryRegistry. Each field in the registry is a callable Query that you can use to read data: import {zero} from 'zero.ts' import {queries} from 'queries.ts' const allPosts = await zero.run(queries.posts.all()) Query Names Each Query has a queryName which is computed by defineQueries. This name is later sent to your server to identify the query to run: console.log(queries.posts.all.queryName) // \"posts.all\" Context Query parameters are supplied by the client application and passed to the server automatically by Zero. This makes them unsuitable for credentials, since the user could modify them. For this reason, Zero queries also support the concept of a context object. Access your context with the ctx parameter to your query: const myPostsQuery = defineQuery(({ctx: {userID}}) => { // User cannot control context.userID, so this safely // restricts the query to the user's own posts. return zql.post.where('authorID', userID) }) If you don't want to register your Context and Schema types globally, you can use defineQueryWithType and defineQueriesWithType: import { defineQueriesWithType, defineQueryWithType } from '@rocicorp/zero' import type {Schema} from 'schema.ts' import type {ZeroContext} from 'context.ts' const defineQuery = defineQueryWithType< Schema, ZeroContext >() const defineQueries = defineQueriesWithType() queries.ts By convention, all queries for an application are listed in a central queries.ts file. This allows them to be easily used on both the client and server: import {defineQueries, defineQuery} from '@rocicorp/zero' import {z} from 'zod' import {zql} from './schema.ts' export const queries = defineQueries({ posts: { get: defineQuery(z.string(), id => zql.post.where('id', id) ), byAuthor: defineQuery( z.object({ authorID: z.string(), includeDrafts: z.boolean().optional() }), ({args: {authorID, includeDrafts}}) => { let q = zql.post.where('authorID', authorID) if (!includeDrafts) { q = q.where('isDraft', false) } return q } ) } }) You can use as many levels of nesting as you want to organize your queries. As your application grows, you can move queries to different files to keep them organized: // posts.ts export const postQueries = { get: defineQuery(z.string(), id => zql.post.where('id', id) ) // ... } // users.ts export const userQueries = { byRole: defineQuery(z.string(), role => zql.user.where('role', role) ) // ... } // queries.ts import {postQueries} from './posts.ts' import {userQueries} from './users.ts' export const queries = defineQueries({ posts: postQueries, users: userQueries }) Because defineQueries establishes the full name for each query (i.e., posts.get, users.byRole), it should only be used once at the top level of your queries.ts file. Server Setup In order for queries to sync, you must provide an implementation of the query endpoint on your server. zero-cache calls this endpoint to resolve each query to ZQL that it can run. Registering the Endpoint Use ZERO_QUERY_URL to tell zero-cache where to find your query implementation: export ZERO_QUERY_URL=\"http://localhost:3000/api/zero/query\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleQueryRequest and mustGetQuery functions to implement the endpoint. // src/routes/api/zero/query.ts import {createFileRoute} from '@tanstack/react-router' import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export const Route = createFileRoute('/api/zero/query')({ server: { handlers: { POST: async ({request}) => { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request, userID: null }) return Response.json(result) } } } })// app/api/zero/query/route.ts import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export async function POST(request: Request) { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request, userID: null }) return Response.json(result) }// src/routes/api/zero/query.ts import type {APIEvent} from '@solidjs/start/server' import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export async function POST(event: APIEvent) { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' app.post('/api/zero/query', async c => { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request: c.req.raw, userID: null }) return c.json(result) }) handleQueryRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetQuery looks up the query in the registry and throws an error if not found. The query.fn function is your query implementation wrapped in the validator you provided. These examples have only public queries, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the query handler. See Authentication. Custom Query URL By default, Zero sends queries to the URL specified in the ZERO_QUERY_URL parameter in the zero-cache config. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in ZERO_QUERY_URL: ZERO_QUERY_URL='https://api.example.com/query,https://api.staging.example.com/query' Then choose one of those URLs by passing it to queryURL on the Zero constructor: const zero = new Zero({ schema, queries, queryURL: 'https://api.staging.example.com/query' }) URL Patterns The strings listed in ZERO_QUERY_URL can also be URLPatterns: ZERO_QUERY_URL=\"https://mybranch-*.preview.myapp.com/query\" This queries URL will allow clients to choose URLs like: https://mybranch-aaa.preview.myapp.com/query ✅ https://mybranch-bbb.preview.myapp.com/query ✅ But rejects URLs like: https://preview.myapp.com/query ❌ (missing subdomain) https://malicious.com/query ❌ (different domain) https://mybranch-123.preview.myapp.com/query/extra ❌ (extra path) https://mybranch-123.preview.myapp.com/other ❌ (different path) Because URLPattern is a web standard, you can test them right in your browser: For more information, see the URLPattern docs. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Running Queries Reactively The most common way to use queries is with the useQuery reactive hooks from the React or SolidJS bindings (or the equivalent low-level API): import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(queries.posts.get('user123')) return posts.map(post => (
{post.title}
)) }import {useQuery} from '@rocicorp/zero/solid' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(() => queries.posts.get('user123') ) return ( {post =>
{post.title}
}
) }import {queries} from 'zero/queries.ts' import {zero} from 'zero.ts' const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) for (let post of postsView.data) { console.log(post.title) } // updates as the underlying data changes postsView.addListener(posts => { console.log('posts', posts) }) These functions allow you to automatically re-render UI when a query changes. Conditionally Sometimes the inputs needed to construct a query are not available on the first render. For example, auth state or a route param might still be loading after a page refresh. Both React and Solid support conditional queries by passing undefined until the query can be constructed: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function Username({userID}: {userID: string | undefined}) { const [user] = useQuery( userID ? queries.users.getUser({ userID }) : undefined ) return user ?
{user.username}
: null }import {useQuery} from '@rocicorp/zero/solid' import {Show} from 'solid-js' import {queries} from 'zero/queries.ts' function Username(props: {userID: string | undefined}) { const [user] = useQuery(() => props.userID ? queries.users.getUser({ userID: props.userID }) : undefined ) return ( {user =>
{user().username}
}
) } Once You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} ) To also accept a result the server confirmed during a previous session, pass {type: 'cached'}. It resolves as soon as the result is cached or complete, so it doesn't wait for the server when the client already stores a confirmed result: const results = await zero.run( queries.issues.byPriority('high'), {type: 'cached'} ) For Preloading Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) ) preload() returns cleanup, which ends the preload (Zero keeps its rows for the query's TTL), and two promises: complete resolves once the server has returned the result, and cached resolves once the result is cached or complete. Missing Data Because Zero returns local results immediately and server results asynchronously, displaying \"not found\" / 404 UI can be slightly tricky. If you just use a simple existence check, you will often see the 404 UI flicker while the server result loads: const [issue] = useQuery(queries.issues.get('some-id')) // ❌ This causes flickering of the UI if (!issue) { return
404 Not Found
} else { return
{issue.title}
}const [issue] = useQuery(() => queries.issues.get('some-id') ) return ( {resolved => ( 404 Not Found} >
{resolved.title}
)}
)const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) postsView.addListener(posts => { // ❌ This is updated as data comes in console.log('posts', posts) }) To do this correctly, only display the \"not found\" UI when the result type is complete. This way the 404 page is slow but pages with data are still just as fast: const [issue, issueResult] = useQuery( queries.issues.get('some-id') ) if (!issue && issueResult.type === 'complete') { return
404 Not Found
} if (!issue) { return null } return
{issue.title}
const [issue, issueResult] = useQuery(() => queries.issues.get('some-id') ) return ( {resolved =>
{resolved.title}
}
404 Not Found
)const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) postsView.addListener((posts, resultType) => { if (resultType === 'complete') { console.log('posts', posts) } }) Partial Data Zero immediately returns the data for a query it has on the client, then falls back to the server for any missing data. Sometimes it's useful to know the difference between these two types of results. To do so, use the result from useQuery: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) if (issuesResult.type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) if (issuesResult().type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if (resultType === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') } }) The possible values of result.type are: unknown: the data Zero has on the client. Some of it may be missing. cached: the client stores a result that the server confirmed during a previous session. See Cached Results. complete: the server has returned the result during this session. error: the query failed. See Handling Errors. The complete value is currently only returned when Zero has received the server result. In the future, Zero will be able to return this result type when it knows that all possible data for this query is already available locally. Additionally, we plan to add a prefix result for when the data is known to be a prefix of the complete result. See Consistency for more information. Cached Results When Zero stores data on the client (in IndexedDB, or SQLite on React Native), it also remembers which query results the server confirmed. If the client still holds such a result when the app starts again, the query starts as cached instead of unknown, and becomes complete once the server returns the result in this session. If Zero evicts the stored result before then, the query goes back to unknown. A cached result was complete when the server sent it, but it may be out of date. Use it where a previous session's answer is good enough, for example to show stored data or an empty state without waiting for the server, which also works while offline. Wait for complete where the data must be current: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) const confirmed = issuesResult.type === 'cached' || issuesResult.type === 'complete' if (issues.length === 0 && confirmed) { return
Your inbox is empty
}const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) const confirmed = () => issuesResult().type === 'cached' || issuesResult().type === 'complete' return ( 0 || !confirmed()} fallback={
Your inbox is empty
} > {/* ... */}
)const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if ( issues.length === 0 && (resultType === 'cached' || resultType === 'complete') ) { console.log('Inbox is empty') } }) zero.run(query, {type: 'cached'}) and the cached promise from zero.preload() resolve on a cached or complete result. {type: 'complete'} and the complete promise still wait for the server. Handling Errors If the queries endpoint throws an application or parse error, zero-cache will report it to the client using the type and error fields on the query details object: const [posts, postsResult] = useQuery( queries.posts.byAuthorID('user123') ) if (postsResult.type === 'error') { return (
Error loading posts: {postsResult.error.message}
) }const [posts, postsResult] = useQuery(() => queries.posts.byAuthorID('user123') ) return (
Error loading posts: {postsResult().error.message}
)// Materialize a view of a query const postsView = queries.posts .byAuthorID('user123') .materialize() postsView.addListener((posts, resultType, error) => { if (resultType === 'error') { console.error('Error loading posts', error) } }) See Connection Status for how HTTP or network errors from the queries endpoint are handled. Granular Updates You can use the materialize() method to create a view that you can listen to for changes. However, this will only tell you when the view has changed and give you the complete new result. It won't tell you what changed. To know what changed, you can create your own custom View implementation: // Inside the View class // Instead of storing the change, we invoke some callback push(change: Change): void { switch (change.type) { case 'add': this.#onAdd?.(change) break case 'remove': this.#onRemove?.(change) break case 'edit': this.#onEdit?.(change) break case 'child': this.#onChild?.(change) break default: throw new Error(`Unknown change type: ${change['type']}`) } } For examples, see the View implementations in zero-vue or zero-solid. Query Caching Queries can be either active or cached. An active query is one that is currently being used by the application. Cached queries are not currently in use, but continue syncing in case they are needed again soon. Queries are deactivated according to how they were created: For useQuery(), the UI unmounts the component (which calls destroy() under the covers). For preload(), the UI calls cleanup() on the return value of preload(). For run(), queries are automatically deactivated immediately after the result is returned. For materialize() queries, the UI calls destroy() on the view. Additionally when a Zero instance closes, all active queries are automatically deactivated. This also happens when the containing page or script is unloaded. TTLs Each query has a ttl that controls how long it stays cached. If the user closes all tabs for your app, Zero stops running and the time that elapses doesn't count toward any TTLs. You do not need to account for such time when choosing a TTL – you only need to account for time your app is running without a query. TTL Defaults In most cases, the default TTL should work well: preload() queries default to ttl:'none', meaning they are not cached at all, and will stop syncing immediately when deactivated. But because preload() queries are typically registered at app startup and never shutdown, and because the ttl clock only ticks while Zero is running, this means that preload queries never get unregistered. Other queries have a default ttl of 5m (five minutes). Setting Different TTLs You can override the default TTL with the ttl parameter: const [user] = useQuery( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero.preload(queries.posts.byAuthorID('user123'), { ttl: '5m' })const [user] = useQuery( () => queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero().preload(queries.posts.byAuthorID('user123'), { ttl: '5m' })// run() const user = await zero.run( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // materialize() const view = zero.materialize( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero.preload(queries.posts.byAuthorID('user123'), { ttl: '5m' }) TTLs up to 10m (ten minutes) are currently supported. The following formats are allowed: Why Zero TTLs are Short Zero queries are not free. Just as in any database, queries consume resources on both the client and server. Memory is used to keep metadata about the query, and disk storage is used to keep the query's current state. We do drop this state after we haven't heard from a client for awhile, but this is only a partial improvement. If the client returns, we have to re-run the query to get the latest data. This means that we do not actually want to keep queries active unless there is a good chance they will be needed again soon. The default Zero TTL values might initially seem too short, but they are designed to work well with the way Zero's TTL clock works and strike a good balance between keeping queries alive long enough to be useful, while not keeping them alive so long that they consume resources unnecessarily. Local-Only Queries It can sometimes be useful to run queries only on the client. For example, to implement typeahead search, it really doesn't make sense to register a query with the server for every single keystroke. Zero doesn't yet have a way to run named queries local-only, but you can run ZQL expressions locally by passing them anywhere a query is supported. For example, to subscribe to a local-only query: // Queries the already synced data for issues, // without syncing more data. const [issues] = useQuery( zql.issue.orderBy('created', 'desc').limit(10) )// Queries the already synced data for issues, // without syncing more data. const [issues] = useQuery(() => zql.issue.orderBy('created', 'desc').limit(10) )// Queries the already synced data for issues, // without syncing more data. const view = z.materialize( zql.issue.orderBy('created', 'desc').limit(10) ) view.addListener(issues => { console.log('issues', issues) }) Custom Server Implementation It is possible to implement the ZERO_QUERY_URL endpoint without using Zero's TypeScript libraries, or even in a different language entirely. The endpoint receives a POST request with a JSON body of the form: type QueriesRequestBody = { id: string name: string args: readonly ReadonlyJSONValue[] }[] And responds with: type QueriesResponseBody = ( | { id: string name: string // See https://github.com/rocicorp/mono/blob/main/packages/zero-protocol/src/ast.ts ast: AST } | { error: 'app' id: string name: string details: ReadonlyJSONValue } | { error: 'zero' id: string name: string details: ReadonlyJSONValue } | { error: 'http' id: string name: string status: number details: ReadonlyJSONValue } )[] Consistency Zero always syncs a consistent partial replica of the backend database to the client. This avoids many common consistency issues that come up in classic web applications. But there are still some consistency issues to be aware of when using Zero. For example, imagine that you have a bug database w/ 10k issues. You preload the first 1k issues sorted by created. The user then does a query of issues assigned to themselves, sorted by created. Among the 1k issues that were preloaded imagine 100 are found that match the query. Since the data we preloaded is in the same order as this query, we are guaranteed that any local results found will be a prefix of the server results. The UX that result is nice: the user will see initial results to the query instantly. If more results are found server-side, those results are guaranteed to sort below the local results. There's no shuffling of results when the server response comes in. Now imagine that the user switches the sort to ‘sort by modified’. This new query will run locally, and will again find some local matches. But it is now unlikely that the local results found are a prefix of the server results. When the server result comes in, the user will probably see the results shuffle around. To avoid this annoying effect, what you should do in this example is also preload the first 1k issues sorted by modified desc. In general for any query shape you intend to do, you should preload the first n results for that query shape with no filters, in each sort you intend to use. Zero syncs the union of all active queries' results. You don't have to worry about syncing many sorts of the same query when it's likely the results will overlap heavily. In the future, we will be implementing a consistency model that fixes these issues automatically. We will prevent Zero from returning local data when that data is not known to be a prefix of the server result. Once the consistency model is implemented, preloading can be thought of as purely a performance thing, and not required to avoid unsightly flickering.", + "content": "Queries are how you read and sync data with Zero. Here's a simple example: // src/queries.ts import {defineQueries, defineQuery} from '@rocicorp/zero' import {z} from 'zod' import {zql} from 'schema.ts' export const queries = defineQueries({ postsByAuthor: defineQuery( z.object({authorID: z.string()}), ({args: {authorID}}) => zql.post.where('authorID', authorID) ) }) Architecture A copy of each query exists on both the client and on your server: Often the implementations will be the same, and you can just share their code. This is easy with full-stack frameworks like TanStack Start or Next.js. But the implementations don't have to be the same, or even compute the same result. For example, the server can add extra filters to enforce permissions that the client query does not. Life of a Query When a query is invoked, it initially runs on the client, against the client-side datastore. Any matching data is returned immediately and the user sees instant results. In the background, the name and arguments for the query are sent to zero-cache. Zero-cache calls the queries endpoint on your server to get the ZQL for the query. Your server looks up its implementation of the query, invokes it, and returns the resulting ZQL expression to zero-cache. Zero-cache then runs this ZQL against the server-side data. The initial server result is sent back to the client and the client query updates in response. zero-cache receives updates from Postgres via logical replication. It updates affected queries and sends row changes back to the client, which updates the client query, and the user sees the changes. Defining Queries Basics Create a query using defineQuery. The only required argument is a QueryFn, which must return a ZQL expression: import {zql} from 'schema.ts' const allPostsQueryDef = defineQuery(() => zql.post) Arguments The QueryFn can take a single args parameter. To enable this, pass a validator to defineQuery: import {zql} from 'schema.ts' const postsByAuthor = defineQuery( z.object({authorID: z.string().optional()}), ({args: {authorID}}) => { let q = zql.post if (authorID !== undefined) { q = q.where('authorID', authorID) } return q } ) We use Zod in these examples, but you can use any validation library that implements Standard Schema. Zero queries run on both the client and on your server. In the server case, the parameters come from the client and are untrusted. The validator ensures the data passed to your query is of the expected type. Query Registries The result of defineQuery is a QueryDefinition. By itself this isn't super useful. You need to register it using defineQueries: export const queries = defineQueries({ posts: { all: allPostsQueryDef } }) Typically these are done together in one step: export const queries = defineQueries({ posts: { all: defineQuery(() => zql.post) } }) The result of defineQueries is called a QueryRegistry. Each field in the registry is a callable Query that you can use to read data: import {zero} from 'zero.ts' import {queries} from 'queries.ts' const allPosts = await zero.run(queries.posts.all()) Query Names Each Query has a queryName which is computed by defineQueries. This name is later sent to your server to identify the query to run: console.log(queries.posts.all.queryName) // \"posts.all\" Context Query parameters are supplied by the client application and passed to the server automatically by Zero. This makes them unsuitable for credentials, since the user could modify them. For this reason, Zero queries also support the concept of a context object. Access your context with the ctx parameter to your query: const myPostsQuery = defineQuery(({ctx: {userID}}) => { // User cannot control context.userID, so this safely // restricts the query to the user's own posts. return zql.post.where('authorID', userID) }) If you don't want to register your Context and Schema types globally, you can use defineQueryWithType and defineQueriesWithType: import { defineQueriesWithType, defineQueryWithType } from '@rocicorp/zero' import type {Schema} from 'schema.ts' import type {ZeroContext} from 'context.ts' const defineQuery = defineQueryWithType< Schema, ZeroContext >() const defineQueries = defineQueriesWithType() queries.ts By convention, all queries for an application are listed in a central queries.ts file. This allows them to be easily used on both the client and server: import {defineQueries, defineQuery} from '@rocicorp/zero' import {z} from 'zod' import {zql} from './schema.ts' export const queries = defineQueries({ posts: { get: defineQuery(z.string(), id => zql.post.where('id', id) ), byAuthor: defineQuery( z.object({ authorID: z.string(), includeDrafts: z.boolean().optional() }), ({args: {authorID, includeDrafts}}) => { let q = zql.post.where('authorID', authorID) if (!includeDrafts) { q = q.where('isDraft', false) } return q } ) } }) You can use as many levels of nesting as you want to organize your queries. As your application grows, you can move queries to different files to keep them organized: // posts.ts export const postQueries = { get: defineQuery(z.string(), id => zql.post.where('id', id) ) // ... } // users.ts export const userQueries = { byRole: defineQuery(z.string(), role => zql.user.where('role', role) ) // ... } // queries.ts import {postQueries} from './posts.ts' import {userQueries} from './users.ts' export const queries = defineQueries({ posts: postQueries, users: userQueries }) Because defineQueries establishes the full name for each query (i.e., posts.get, users.byRole), it should only be used once at the top level of your queries.ts file. Server Setup In order for queries to sync, you must provide an implementation of the query endpoint on your server. zero-cache calls this endpoint to resolve each query to ZQL that it can run. Registering the Endpoint Use ZERO_QUERY_URL to tell zero-cache where to find your query implementation: export ZERO_QUERY_URL=\"http://localhost:3000/api/zero/query\" # run zero-cache, e.g. `npx zero-cache-dev` Implementing the Endpoint You can use the handleQueryRequest and mustGetQuery functions to implement the endpoint. // src/routes/api/zero/query.ts import {createFileRoute} from '@tanstack/react-router' import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export const Route = createFileRoute('/api/zero/query')({ server: { handlers: { POST: async ({request}) => { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request, userID: null }) return Response.json(result) } } } })// app/api/zero/query/route.ts import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export async function POST(request: Request) { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request, userID: null }) return Response.json(result) }// src/routes/api/zero/query.ts import type {APIEvent} from '@solidjs/start/server' import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' export async function POST(event: APIEvent) { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request: event.request, userID: null }) return Response.json(result) }// api/app.ts import {handleQueryRequest} from '@rocicorp/zero/server' import {mustGetQuery} from '@rocicorp/zero' import {queries} from 'queries.ts' import {schema} from 'schema.ts' app.post('/api/zero/query', async c => { const result = await handleQueryRequest({ handler: (name, args) => { const query = mustGetQuery(queries, name) return query.fn({args}) }, schema, request: c.req.raw, userID: null }) return c.json(result) }) handleQueryRequest accepts a standard Request and returns a JSON object which can be serialized and returned by your server framework of choice. mustGetQuery looks up the query in the registry and throws an error if not found. The query.fn function is your query implementation wrapped in the validator you provided. These examples have only public queries, so they do not pass a context. In authenticated apps, validate auth in the request, derive context from the session, and pass it to the query handler. See Authentication. Custom Query URL By default, Zero sends queries to the URL specified in the ZERO_QUERY_URL parameter in the zero-cache config. However you can customize this on a per-client basis. To do so, list multiple comma-separated URLs in ZERO_QUERY_URL: ZERO_QUERY_URL='https://api.example.com/query,https://api.staging.example.com/query' Then choose one of those URLs by passing it to queryURL on the Zero constructor: const zero = new Zero({ schema, queries, queryURL: 'https://api.staging.example.com/query' }) URL Patterns The strings listed in ZERO_QUERY_URL can also be URLPatterns: ZERO_QUERY_URL=\"https://mybranch-*.preview.myapp.com/query\" This queries URL will allow clients to choose URLs like: https://mybranch-aaa.preview.myapp.com/query ✅ https://mybranch-bbb.preview.myapp.com/query ✅ But rejects URLs like: https://preview.myapp.com/query ❌ (missing subdomain) https://malicious.com/query ❌ (different domain) https://mybranch-123.preview.myapp.com/query/extra ❌ (extra path) https://mybranch-123.preview.myapp.com/other ❌ (different path) Because URLPattern is a web standard, you can test them right in your browser: For more information, see the URLPattern docs. If you're configuring per-branch preview URLs (for example on Vercel), see Preview Deployments for the complete setup across both query and mutate endpoints. Running Queries Reactively The most common way to use queries is with the useQuery reactive hooks from the React or SolidJS bindings (or the equivalent low-level API): import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(queries.posts.get('user123')) return posts.map(post => (
{post.title}
)) }import {useQuery} from '@rocicorp/zero/solid' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(() => queries.posts.get('user123') ) return ( {post =>
{post.title}
}
) }import {queries} from 'zero/queries.ts' import {zero} from 'zero.ts' const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) for (let post of postsView.data) { console.log(post.title) } // updates as the underlying data changes postsView.addListener(posts => { console.log('posts', posts) }) These functions allow you to automatically re-render UI when a query changes. Conditionally Sometimes the inputs needed to construct a query are not available on the first render. For example, auth state or a route param might still be loading after a page refresh. Both React and Solid support conditional queries by passing undefined until the query can be constructed: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function Username({userID}: {userID: string | undefined}) { const [user] = useQuery( userID ? queries.users.getUser({ userID }) : undefined ) return user ?
{user.username}
: null }import {useQuery} from '@rocicorp/zero/solid' import {Show} from 'solid-js' import {queries} from 'zero/queries.ts' function Username(props: {userID: string | undefined}) { const [user] = useQuery(() => props.userID ? queries.users.getUser({ userID: props.userID }) : undefined ) return ( {user =>
{user().username}
}
) } Once You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} ) Pass {type: 'cached'} to also accept a result the server confirmed in an earlier session. It resolves as soon as the result is cached or complete, so it can return without waiting for the server: const results = await zero.run( queries.issues.byPriority('high'), {type: 'cached'} ) For Preloading Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) ) preload() returns three things. cleanup ends the preload, and Zero keeps the rows for the query's TTL. complete is a promise that resolves once the server returns the result, and cached resolves once the result is cached or complete. Missing Data Because Zero returns local results immediately and server results asynchronously, displaying \"not found\" / 404 UI can be slightly tricky. If you just use a simple existence check, you will often see the 404 UI flicker while the server result loads: const [issue] = useQuery(queries.issues.get('some-id')) // ❌ This causes flickering of the UI if (!issue) { return
404 Not Found
} else { return
{issue.title}
}const [issue] = useQuery(() => queries.issues.get('some-id') ) return ( {resolved => ( 404 Not Found} >
{resolved.title}
)}
)const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) postsView.addListener(posts => { // ❌ This is updated as data comes in console.log('posts', posts) }) To do this correctly, only display the \"not found\" UI when the result type is complete. This way the 404 page is slow but pages with data are still just as fast: const [issue, issueResult] = useQuery( queries.issues.get('some-id') ) if (!issue && issueResult.type === 'complete') { return
404 Not Found
} if (!issue) { return null } return
{issue.title}
const [issue, issueResult] = useQuery(() => queries.issues.get('some-id') ) return ( {resolved =>
{resolved.title}
}
404 Not Found
)const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) postsView.addListener((posts, resultType) => { if (resultType === 'complete') { console.log('posts', posts) } }) Partial Data Zero immediately returns the data for a query it has on the client, then falls back to the server for any missing data. Sometimes it's useful to know the difference between these two types of results. To do so, use the result from useQuery: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) if (issuesResult.type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) if (issuesResult().type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if (resultType === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') } }) The possible values of result.type are: unknown: whatever data the client has right now, which may be incomplete. cached: the client stores a result that the server confirmed during a previous session. See Cached Results. complete: the server has returned the result during this session. error: the query failed. See Handling Errors. The complete value is currently only returned when Zero has received the server result. In the future, Zero will be able to return this result type when it knows that all possible data for this query is already available locally. Additionally, we plan to add a prefix result for when the data is known to be a prefix of the complete result. See Consistency for more information. Cached Results Along with the data it stores on the client (IndexedDB in the browser, SQLite on React Native), Zero remembers which query results the server has confirmed. When the app starts again and the client still has one of those results, the query starts out cached instead of unknown. It becomes complete once the server sends the result again. If Zero evicts the stored result first, the query drops back to unknown. A cached result was complete when the server sent it, but it may be stale by now. It's good for showing stored data, or an empty state, without waiting for the server, and that works offline too. If the data has to be current, wait for complete. For example, to show an empty inbox without waiting for the server: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) const confirmed = issuesResult.type === 'cached' || issuesResult.type === 'complete' if (issues.length === 0 && confirmed) { return
Your inbox is empty
}const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) const confirmed = () => issuesResult().type === 'cached' || issuesResult().type === 'complete' return ( 0 || !confirmed()} fallback={
Your inbox is empty
} > {/* ... */}
)const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if ( issues.length === 0 && (resultType === 'cached' || resultType === 'complete') ) { console.log('Inbox is empty') } }) zero.run() and zero.preload() can also return as soon as a result is cached: pass {type: 'cached'} to run(), or wait on the cached promise that preload() returns. Handling Errors If the queries endpoint throws an application or parse error, zero-cache will report it to the client using the type and error fields on the query details object: const [posts, postsResult] = useQuery( queries.posts.byAuthorID('user123') ) if (postsResult.type === 'error') { return (
Error loading posts: {postsResult.error.message}
) }const [posts, postsResult] = useQuery(() => queries.posts.byAuthorID('user123') ) return (
Error loading posts: {postsResult().error.message}
)// Materialize a view of a query const postsView = queries.posts .byAuthorID('user123') .materialize() postsView.addListener((posts, resultType, error) => { if (resultType === 'error') { console.error('Error loading posts', error) } }) See Connection Status for how HTTP or network errors from the queries endpoint are handled. Granular Updates You can use the materialize() method to create a view that you can listen to for changes. However, this will only tell you when the view has changed and give you the complete new result. It won't tell you what changed. To know what changed, you can create your own custom View implementation: // Inside the View class // Instead of storing the change, we invoke some callback push(change: Change): void { switch (change.type) { case 'add': this.#onAdd?.(change) break case 'remove': this.#onRemove?.(change) break case 'edit': this.#onEdit?.(change) break case 'child': this.#onChild?.(change) break default: throw new Error(`Unknown change type: ${change['type']}`) } } For examples, see the View implementations in zero-vue or zero-solid. Query Caching Queries can be either active or cached. An active query is one that is currently being used by the application. Cached queries are not currently in use, but continue syncing in case they are needed again soon. Queries are deactivated according to how they were created: For useQuery(), the UI unmounts the component (which calls destroy() under the covers). For preload(), the UI calls cleanup() on the return value of preload(). For run(), queries are automatically deactivated immediately after the result is returned. For materialize() queries, the UI calls destroy() on the view. Additionally when a Zero instance closes, all active queries are automatically deactivated. This also happens when the containing page or script is unloaded. TTLs Each query has a ttl that controls how long it stays cached. If the user closes all tabs for your app, Zero stops running and the time that elapses doesn't count toward any TTLs. You do not need to account for such time when choosing a TTL – you only need to account for time your app is running without a query. TTL Defaults In most cases, the default TTL should work well: preload() queries default to ttl:'none', meaning they are not cached at all, and will stop syncing immediately when deactivated. But because preload() queries are typically registered at app startup and never shutdown, and because the ttl clock only ticks while Zero is running, this means that preload queries never get unregistered. Other queries have a default ttl of 5m (five minutes). Setting Different TTLs You can override the default TTL with the ttl parameter: const [user] = useQuery( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero.preload(queries.posts.byAuthorID('user123'), { ttl: '5m' })const [user] = useQuery( () => queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero().preload(queries.posts.byAuthorID('user123'), { ttl: '5m' })// run() const user = await zero.run( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // materialize() const view = zero.materialize( queries.posts.byAuthorID('user123'), {ttl: '5m'} ) // preload() zero.preload(queries.posts.byAuthorID('user123'), { ttl: '5m' }) TTLs up to 10m (ten minutes) are currently supported. The following formats are allowed: Why Zero TTLs are Short Zero queries are not free. Just as in any database, queries consume resources on both the client and server. Memory is used to keep metadata about the query, and disk storage is used to keep the query's current state. We do drop this state after we haven't heard from a client for awhile, but this is only a partial improvement. If the client returns, we have to re-run the query to get the latest data. This means that we do not actually want to keep queries active unless there is a good chance they will be needed again soon. The default Zero TTL values might initially seem too short, but they are designed to work well with the way Zero's TTL clock works and strike a good balance between keeping queries alive long enough to be useful, while not keeping them alive so long that they consume resources unnecessarily. Local-Only Queries It can sometimes be useful to run queries only on the client. For example, to implement typeahead search, it really doesn't make sense to register a query with the server for every single keystroke. Zero doesn't yet have a way to run named queries local-only, but you can run ZQL expressions locally by passing them anywhere a query is supported. For example, to subscribe to a local-only query: // Queries the already synced data for issues, // without syncing more data. const [issues] = useQuery( zql.issue.orderBy('created', 'desc').limit(10) )// Queries the already synced data for issues, // without syncing more data. const [issues] = useQuery(() => zql.issue.orderBy('created', 'desc').limit(10) )// Queries the already synced data for issues, // without syncing more data. const view = z.materialize( zql.issue.orderBy('created', 'desc').limit(10) ) view.addListener(issues => { console.log('issues', issues) }) Custom Server Implementation It is possible to implement the ZERO_QUERY_URL endpoint without using Zero's TypeScript libraries, or even in a different language entirely. The endpoint receives a POST request with a JSON body of the form: type QueriesRequestBody = { id: string name: string args: readonly ReadonlyJSONValue[] }[] And responds with: type QueriesResponseBody = ( | { id: string name: string // See https://github.com/rocicorp/mono/blob/main/packages/zero-protocol/src/ast.ts ast: AST } | { error: 'app' id: string name: string details: ReadonlyJSONValue } | { error: 'zero' id: string name: string details: ReadonlyJSONValue } | { error: 'http' id: string name: string status: number details: ReadonlyJSONValue } )[] Consistency Zero always syncs a consistent partial replica of the backend database to the client. This avoids many common consistency issues that come up in classic web applications. But there are still some consistency issues to be aware of when using Zero. For example, imagine that you have a bug database w/ 10k issues. You preload the first 1k issues sorted by created. The user then does a query of issues assigned to themselves, sorted by created. Among the 1k issues that were preloaded imagine 100 are found that match the query. Since the data we preloaded is in the same order as this query, we are guaranteed that any local results found will be a prefix of the server results. The UX that result is nice: the user will see initial results to the query instantly. If more results are found server-side, those results are guaranteed to sort below the local results. There's no shuffling of results when the server response comes in. Now imagine that the user switches the sort to ‘sort by modified’. This new query will run locally, and will again find some local matches. But it is now unlikely that the local results found are a prefix of the server results. When the server result comes in, the user will probably see the results shuffle around. To avoid this annoying effect, what you should do in this example is also preload the first 1k issues sorted by modified desc. In general for any query shape you intend to do, you should preload the first n results for that query shape with no filters, in each sort you intend to use. Zero syncs the union of all active queries' results. You don't have to worry about syncing many sorts of the same query when it's likely the results will overlap heavily. In the future, we will be implementing a consistency model that fixes these issues automatically. We will prevent Zero from returning local data when that data is not known to be a prefix of the server result. Once the consistency model is implemented, preloading can be thought of as purely a performance thing, and not required to avoid unsightly flickering.", "headings": [ { "text": "Architecture", @@ -3006,7 +3006,7 @@ "sectionTitle": "Running Queries", "sectionId": "running-queries", "url": "/docs/queries", - "content": "Reactively The most common way to use queries is with the useQuery reactive hooks from the React or SolidJS bindings (or the equivalent low-level API): import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(queries.posts.get('user123')) return posts.map(post => (
{post.title}
)) }import {useQuery} from '@rocicorp/zero/solid' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(() => queries.posts.get('user123') ) return ( {post =>
{post.title}
}
) }import {queries} from 'zero/queries.ts' import {zero} from 'zero.ts' const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) for (let post of postsView.data) { console.log(post.title) } // updates as the underlying data changes postsView.addListener(posts => { console.log('posts', posts) }) These functions allow you to automatically re-render UI when a query changes. Conditionally Sometimes the inputs needed to construct a query are not available on the first render. For example, auth state or a route param might still be loading after a page refresh. Both React and Solid support conditional queries by passing undefined until the query can be constructed: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function Username({userID}: {userID: string | undefined}) { const [user] = useQuery( userID ? queries.users.getUser({ userID }) : undefined ) return user ?
{user.username}
: null }import {useQuery} from '@rocicorp/zero/solid' import {Show} from 'solid-js' import {queries} from 'zero/queries.ts' function Username(props: {userID: string | undefined}) { const [user] = useQuery(() => props.userID ? queries.users.getUser({ userID: props.userID }) : undefined ) return ( {user =>
{user().username}
}
) } Once You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} ) To also accept a result the server confirmed during a previous session, pass {type: 'cached'}. It resolves as soon as the result is cached or complete, so it doesn't wait for the server when the client already stores a confirmed result: const results = await zero.run( queries.issues.byPriority('high'), {type: 'cached'} ) For Preloading Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) ) preload() returns cleanup, which ends the preload (Zero keeps its rows for the query's TTL), and two promises: complete resolves once the server has returned the result, and cached resolves once the result is cached or complete.", + "content": "Reactively The most common way to use queries is with the useQuery reactive hooks from the React or SolidJS bindings (or the equivalent low-level API): import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(queries.posts.get('user123')) return posts.map(post => (
{post.title}
)) }import {useQuery} from '@rocicorp/zero/solid' import {queries} from 'zero/queries.ts' function App() { const [posts] = useQuery(() => queries.posts.get('user123') ) return ( {post =>
{post.title}
}
) }import {queries} from 'zero/queries.ts' import {zero} from 'zero.ts' const postsView = zero.materialize( queries.posts.byAuthorID('user123') ) for (let post of postsView.data) { console.log(post.title) } // updates as the underlying data changes postsView.addListener(posts => { console.log('posts', posts) }) These functions allow you to automatically re-render UI when a query changes. Conditionally Sometimes the inputs needed to construct a query are not available on the first render. For example, auth state or a route param might still be loading after a page refresh. Both React and Solid support conditional queries by passing undefined until the query can be constructed: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'zero/queries.ts' function Username({userID}: {userID: string | undefined}) { const [user] = useQuery( userID ? queries.users.getUser({ userID }) : undefined ) return user ?
{user.username}
: null }import {useQuery} from '@rocicorp/zero/solid' import {Show} from 'solid-js' import {queries} from 'zero/queries.ts' function Username(props: {userID: string | undefined}) { const [user] = useQuery(() => props.userID ? queries.users.getUser({ userID: props.userID }) : undefined ) return ( {user =>
{user().username}
}
) } Once You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} ) Pass {type: 'cached'} to also accept a result the server confirmed in an earlier session. It resolves as soon as the result is cached or complete, so it can return without waiting for the server: const results = await zero.run( queries.issues.byPriority('high'), {type: 'cached'} ) For Preloading Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) ) preload() returns three things. cleanup ends the preload, and Zero keeps the rows for the query's TTL. complete is a promise that resolves once the server returns the result, and cached resolves once the result is cached or complete.", "kind": "section" }, { @@ -3036,7 +3036,7 @@ "sectionTitle": "Once", "sectionId": "once", "url": "/docs/queries", - "content": "You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} ) To also accept a result the server confirmed during a previous session, pass {type: 'cached'}. It resolves as soon as the result is cached or complete, so it doesn't wait for the server when the client already stores a confirmed result: const results = await zero.run( queries.issues.byPriority('high'), {type: 'cached'} )", + "content": "You usually want to subscribe to a query in a reactive UI, but every so often you'll need to run a query just once. To do this, use zero.run(): const results = await zero.run( queries.issues.byPriority('high') ) By default, run() only returns results that are currently available on the client. That is, it returns the data that would be given for result.type === 'unknown'. If you want to wait for the server to return results, pass {type: 'complete'} to run: const results = await zero.run( queries.issues.byPriority('high'), {type: 'complete'} ) Pass {type: 'cached'} to also accept a result the server confirmed in an earlier session. It resolves as soon as the result is cached or complete, so it can return without waiting for the server: const results = await zero.run( queries.issues.byPriority('high'), {type: 'cached'} )", "kind": "section" }, { @@ -3046,7 +3046,7 @@ "sectionTitle": "For Preloading", "sectionId": "for-preloading", "url": "/docs/queries", - "content": "Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) ) preload() returns cleanup, which ends the preload (Zero keeps its rows for the query's TTL), and two promises: complete resolves once the server has returned the result, and cached resolves once the result is cached or complete.", + "content": "Almost all Zero apps will want to preload some data in order to maximize the feel of instantaneous UI transitions. Because preload queries are often much larger than a screenful of UI, Zero provides a special zero.preload() method to avoid the overhead of materializing the result into JS objects: // Preload a large number of the inbox query results. zero.preload( queries.issues.inbox({ sort: 'created', sortDirection: 'desc', limit: 1000 }) ) preload() returns three things. cleanup ends the preload, and Zero keeps the rows for the query's TTL. complete is a promise that resolves once the server returns the result, and cached resolves once the result is cached or complete.", "kind": "section" }, { @@ -3066,7 +3066,7 @@ "sectionTitle": "Partial Data", "sectionId": "partial-data", "url": "/docs/queries", - "content": "Zero immediately returns the data for a query it has on the client, then falls back to the server for any missing data. Sometimes it's useful to know the difference between these two types of results. To do so, use the result from useQuery: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) if (issuesResult.type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) if (issuesResult().type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if (resultType === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') } }) The possible values of result.type are: unknown: the data Zero has on the client. Some of it may be missing. cached: the client stores a result that the server confirmed during a previous session. See Cached Results. complete: the server has returned the result during this session. error: the query failed. See Handling Errors. The complete value is currently only returned when Zero has received the server result. In the future, Zero will be able to return this result type when it knows that all possible data for this query is already available locally. Additionally, we plan to add a prefix result for when the data is known to be a prefix of the complete result. See Consistency for more information. Cached Results When Zero stores data on the client (in IndexedDB, or SQLite on React Native), it also remembers which query results the server confirmed. If the client still holds such a result when the app starts again, the query starts as cached instead of unknown, and becomes complete once the server returns the result in this session. If Zero evicts the stored result before then, the query goes back to unknown. A cached result was complete when the server sent it, but it may be out of date. Use it where a previous session's answer is good enough, for example to show stored data or an empty state without waiting for the server, which also works while offline. Wait for complete where the data must be current: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) const confirmed = issuesResult.type === 'cached' || issuesResult.type === 'complete' if (issues.length === 0 && confirmed) { return
Your inbox is empty
}const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) const confirmed = () => issuesResult().type === 'cached' || issuesResult().type === 'complete' return ( 0 || !confirmed()} fallback={
Your inbox is empty
} > {/* ... */}
)const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if ( issues.length === 0 && (resultType === 'cached' || resultType === 'complete') ) { console.log('Inbox is empty') } }) zero.run(query, {type: 'cached'}) and the cached promise from zero.preload() resolve on a cached or complete result. {type: 'complete'} and the complete promise still wait for the server.", + "content": "Zero immediately returns the data for a query it has on the client, then falls back to the server for any missing data. Sometimes it's useful to know the difference between these two types of results. To do so, use the result from useQuery: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) if (issuesResult.type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) if (issuesResult().type === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') }const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if (resultType === 'complete') { console.log('All data is present') } else { console.log('Some data is missing') } }) The possible values of result.type are: unknown: whatever data the client has right now, which may be incomplete. cached: the client stores a result that the server confirmed during a previous session. See Cached Results. complete: the server has returned the result during this session. error: the query failed. See Handling Errors. The complete value is currently only returned when Zero has received the server result. In the future, Zero will be able to return this result type when it knows that all possible data for this query is already available locally. Additionally, we plan to add a prefix result for when the data is known to be a prefix of the complete result. See Consistency for more information. Cached Results Along with the data it stores on the client (IndexedDB in the browser, SQLite on React Native), Zero remembers which query results the server has confirmed. When the app starts again and the client still has one of those results, the query starts out cached instead of unknown. It becomes complete once the server sends the result again. If Zero evicts the stored result first, the query drops back to unknown. A cached result was complete when the server sent it, but it may be stale by now. It's good for showing stored data, or an empty state, without waiting for the server, and that works offline too. If the data has to be current, wait for complete. For example, to show an empty inbox without waiting for the server: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) const confirmed = issuesResult.type === 'cached' || issuesResult.type === 'complete' if (issues.length === 0 && confirmed) { return
Your inbox is empty
}const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) const confirmed = () => issuesResult().type === 'cached' || issuesResult().type === 'complete' return ( 0 || !confirmed()} fallback={
Your inbox is empty
} > {/* ... */}
)const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if ( issues.length === 0 && (resultType === 'cached' || resultType === 'complete') ) { console.log('Inbox is empty') } }) zero.run() and zero.preload() can also return as soon as a result is cached: pass {type: 'cached'} to run(), or wait on the cached promise that preload() returns.", "kind": "section" }, { @@ -3076,7 +3076,7 @@ "sectionTitle": "Cached Results", "sectionId": "cached-results", "url": "/docs/queries", - "content": "When Zero stores data on the client (in IndexedDB, or SQLite on React Native), it also remembers which query results the server confirmed. If the client still holds such a result when the app starts again, the query starts as cached instead of unknown, and becomes complete once the server returns the result in this session. If Zero evicts the stored result before then, the query goes back to unknown. A cached result was complete when the server sent it, but it may be out of date. Use it where a previous session's answer is good enough, for example to show stored data or an empty state without waiting for the server, which also works while offline. Wait for complete where the data must be current: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) const confirmed = issuesResult.type === 'cached' || issuesResult.type === 'complete' if (issues.length === 0 && confirmed) { return
Your inbox is empty
}const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) const confirmed = () => issuesResult().type === 'cached' || issuesResult().type === 'complete' return ( 0 || !confirmed()} fallback={
Your inbox is empty
} > {/* ... */}
)const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if ( issues.length === 0 && (resultType === 'cached' || resultType === 'complete') ) { console.log('Inbox is empty') } }) zero.run(query, {type: 'cached'}) and the cached promise from zero.preload() resolve on a cached or complete result. {type: 'complete'} and the complete promise still wait for the server.", + "content": "Along with the data it stores on the client (IndexedDB in the browser, SQLite on React Native), Zero remembers which query results the server has confirmed. When the app starts again and the client still has one of those results, the query starts out cached instead of unknown. It becomes complete once the server sends the result again. If Zero evicts the stored result first, the query drops back to unknown. A cached result was complete when the server sent it, but it may be stale by now. It's good for showing stored data, or an empty state, without waiting for the server, and that works offline too. If the data has to be current, wait for complete. For example, to show an empty inbox without waiting for the server: const [issues, issuesResult] = useQuery( queries.issues.inbox() ) const confirmed = issuesResult.type === 'cached' || issuesResult.type === 'complete' if (issues.length === 0 && confirmed) { return
Your inbox is empty
}const [issues, issuesResult] = useQuery(() => queries.issues.inbox() ) const confirmed = () => issuesResult().type === 'cached' || issuesResult().type === 'complete' return ( 0 || !confirmed()} fallback={
Your inbox is empty
} > {/* ... */}
)const view = zero.materialize(queries.issues.inbox()) view.addListener((issues, resultType) => { if ( issues.length === 0 && (resultType === 'cached' || resultType === 'complete') ) { console.log('Inbox is empty') } }) zero.run() and zero.preload() can also return as soon as a result is cached: pass {type: 'cached'} to run(), or wait on the cached promise that preload() returns.", "kind": "section" }, { @@ -3245,7 +3245,7 @@ "title": "React", "searchTitle": "React", "url": "/docs/react", - "content": "Zero has built-in support for React. Here's what basic usage looks like. Setup Use the ZeroProvider component to setup Zero. It takes care of creating and destroying Zero instances reactively: import {createRoot} from 'react-dom/client' import {ZeroProvider} from '@rocicorp/zero/react' import {useSession} from 'my-session-provider' import App from './App.tsx' import {schema} from 'schema.ts' import {mutators} from 'mutators.ts' const cacheURL = import.meta.env.VITE_PUBLIC_ZERO_CACHE_URL! export default function Root() { const session = useSession() const userID = session?.userID const auth = session?.accessToken const context = userID ? {userID} : undefined return ( ) } If you use token auth, pass it with auth={session?.jwt}. If you use cookie auth, omit the parameter entirely. When auth changes from one string to another, ZeroProvider refreshes auth in place with zero.connection.connect({auth}). When auth is added or removed, or userID changes, it recreates the Zero instance. When the user is logged out, omit the userID entirely. You can also pass a Zero instance to the ZeroProvider if you want to control the lifecycle of the Zero instance yourself: // ZeroProvider just sets up the context, it doesn't manage // the lifecycle of the Zero instance. When you pass zero={zero}, ZeroProvider only provides React Context. It does not manage auth updates for that instance, so if you are using token auth, call zero.connection.connect({auth: newToken}) yourself or recreate the instance when the user changes. Usage Use useQuery to run queries: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'queries.ts' function Posts() { const [posts] = useQuery( queries.posts.byStatus({status: 'draft'}) ) return ( <> {posts.map(p => (
{p.title} ({p.comments.length} comments)
))} ) } For conditional queries, such as queries that depend on auth state or route params that may not be loaded yet, see Conditional Queries. Use useZero to get access to the Zero instance, for example to run mutators: import {useZero} from '@rocicorp/zero/react' import {mutators} from 'mutators.ts' function CompleteButton({issueID}: {issueID: string}) { const zero = useZero() const onClick = () => { zero.mutate(mutators.issues.complete({id: issueID})) } return } Suspense The useSuspenseQuery hook is exactly like useQuery, except it supports React Suspense. const [issues] = useSuspenseQuery(issueQuery, { suspendUntil: 'complete' // 'partial' or 'complete' }) Use the suspendUntil parameter to control how long to suspend for. The value complete suspends until authoritative results from the server are received. The partial value suspends until any non-empty data is received, or for a empty result that is complete. Query Identity You don't need to memoize queries. Build them inline, where you use them, and pass them straight to useQuery. Zero caches the queries you build with ZQL. Build the same query again and you usually get back the same Query object, even when it has related() or whereExists() callbacks written inline. So rebuilding a query on every render is cheap. You can also use a query as a dependency of useEffect or useMemo, or pass it as a prop to a component wrapped in React.memo: import {useEffect} from 'react' import {useQuery} from '@rocicorp/zero/react' import {zql} from 'schema.ts' function IssueSearch({text}: {text: string}) { // Rebuilt on every render. No useMemo needed. const query = zql.issue .where('title', 'ILIKE', `%${text}%`) .orderBy('created', 'desc') const [issues] = useQuery(query) // Scroll back to the top when the search changes. useEffect(() => { window.scrollTo(0, 0) }, [query]) return issues.map(issue => (
{issue.title}
)) } This example passes ZQL to useQuery directly, so it runs only on the client. Calls to registered queries work a little differently. Each call like queries.posts.byStatus({status}) returns a new object. You can still pass it inline: useQuery sees that it's the same query with the same arguments and reuses its previous work. But don't use the returned object as a dependency. Use its arguments instead. Keep in mind: Identity follows how you build a query. zql.issue.where('status', 'open').where('priority', 'high') and zql.issue.where('priority', 'high').where('status', 'open') return the same rows, but they can be different objects. A different value, such as a different ID in where(), gives a different object. Identity is an optimization, not a promise. Zero can hand back a new object for a query it has built before. When that happens, an effect or memo that depends on the query just runs again, so make sure that's safe. Examples See the sample directory for more complete React examples.", + "content": "Zero has built-in support for React. Here's what basic usage looks like. Setup Use the ZeroProvider component to setup Zero. It takes care of creating and destroying Zero instances reactively: import {createRoot} from 'react-dom/client' import {ZeroProvider} from '@rocicorp/zero/react' import {useSession} from 'my-session-provider' import App from './App.tsx' import {schema} from 'schema.ts' import {mutators} from 'mutators.ts' const cacheURL = import.meta.env.VITE_PUBLIC_ZERO_CACHE_URL! export default function Root() { const session = useSession() const userID = session?.userID const auth = session?.accessToken const context = userID ? {userID} : undefined return ( ) } If you use token auth, pass it with auth={session?.jwt}. If you use cookie auth, omit the parameter entirely. When auth changes from one string to another, ZeroProvider refreshes auth in place with zero.connection.connect({auth}). When auth is added or removed, or userID changes, it recreates the Zero instance. When the user is logged out, omit the userID entirely. You can also pass a Zero instance to the ZeroProvider if you want to control the lifecycle of the Zero instance yourself: // ZeroProvider just sets up the context, it doesn't manage // the lifecycle of the Zero instance. When you pass zero={zero}, ZeroProvider only provides React Context. It does not manage auth updates for that instance, so if you are using token auth, call zero.connection.connect({auth: newToken}) yourself or recreate the instance when the user changes. Usage Use useQuery to run queries: import {useQuery} from '@rocicorp/zero/react' import {queries} from 'queries.ts' function Posts() { const [posts] = useQuery( queries.posts.byStatus({status: 'draft'}) ) return ( <> {posts.map(p => (
{p.title} ({p.comments.length} comments)
))} ) } For conditional queries, such as queries that depend on auth state or route params that may not be loaded yet, see Conditional Queries. Use useZero to get access to the Zero instance, for example to run mutators: import {useZero} from '@rocicorp/zero/react' import {mutators} from 'mutators.ts' function CompleteButton({issueID}: {issueID: string}) { const zero = useZero() const onClick = () => { zero.mutate(mutators.issues.complete({id: issueID})) } return } Suspense The useSuspenseQuery hook is exactly like useQuery, except it supports React Suspense. const [issues] = useSuspenseQuery(issueQuery, { suspendUntil: 'complete' // 'partial' or 'complete' }) Use the suspendUntil parameter to control how long to suspend for. The value complete suspends until authoritative results from the server are received. The partial value suspends until any non-empty data is received, or for a empty result that is complete. Query Identity You don't need to memoize queries. Build them inline, where you use them, and pass them straight to useQuery. Zero caches the queries you build with ZQL. Build the same query again and you usually get back the same Query object, even when it has related() or whereExists() callbacks written inline. So rebuilding a query on every render is cheap. You can also use a query as a dependency of useEffect or useMemo, or pass it as a prop to a component wrapped in React.memo: import {useEffect} from 'react' import {useQuery} from '@rocicorp/zero/react' import {zql} from 'schema.ts' function IssueSearch({text}: {text: string}) { // Rebuilt on every render. No useMemo needed. const query = zql.issue .where('title', 'ILIKE', `%${text}%`) .orderBy('created', 'desc') const [issues] = useQuery(query) // Scroll back to the top when the search changes. useEffect(() => { window.scrollTo(0, 0) }, [query]) return issues.map(issue => (
{issue.title}
)) } This example passes ZQL to useQuery directly, so it runs only on the client. Calls to registered queries work a little differently. Each call like queries.posts.byStatus({status}) returns a new object. You can still pass it inline: useQuery sees that it's the same query with the same arguments and reuses its previous work. But don't use the returned object as a dependency. Use its arguments instead. Identity follows the order you build a query in. zql.issue.where('status', 'open').where('priority', 'high') and zql.issue.where('priority', 'high').where('status', 'open') return the same rows but can be different objects. A different value, such as another ID in where(), always gives a different object. Zero doesn't guarantee identity, though. It can hand back a new object for a query it has built before. When that happens, an effect or memo that depends on the query runs again, so make sure that's safe. Examples See the sample directory for more complete React examples.", "headings": [ { "text": "Setup", @@ -3307,7 +3307,7 @@ "sectionTitle": "Query Identity", "sectionId": "query-identity", "url": "/docs/react", - "content": "You don't need to memoize queries. Build them inline, where you use them, and pass them straight to useQuery. Zero caches the queries you build with ZQL. Build the same query again and you usually get back the same Query object, even when it has related() or whereExists() callbacks written inline. So rebuilding a query on every render is cheap. You can also use a query as a dependency of useEffect or useMemo, or pass it as a prop to a component wrapped in React.memo: import {useEffect} from 'react' import {useQuery} from '@rocicorp/zero/react' import {zql} from 'schema.ts' function IssueSearch({text}: {text: string}) { // Rebuilt on every render. No useMemo needed. const query = zql.issue .where('title', 'ILIKE', `%${text}%`) .orderBy('created', 'desc') const [issues] = useQuery(query) // Scroll back to the top when the search changes. useEffect(() => { window.scrollTo(0, 0) }, [query]) return issues.map(issue => (
{issue.title}
)) } This example passes ZQL to useQuery directly, so it runs only on the client. Calls to registered queries work a little differently. Each call like queries.posts.byStatus({status}) returns a new object. You can still pass it inline: useQuery sees that it's the same query with the same arguments and reuses its previous work. But don't use the returned object as a dependency. Use its arguments instead. Keep in mind: Identity follows how you build a query. zql.issue.where('status', 'open').where('priority', 'high') and zql.issue.where('priority', 'high').where('status', 'open') return the same rows, but they can be different objects. A different value, such as a different ID in where(), gives a different object. Identity is an optimization, not a promise. Zero can hand back a new object for a query it has built before. When that happens, an effect or memo that depends on the query just runs again, so make sure that's safe.", + "content": "You don't need to memoize queries. Build them inline, where you use them, and pass them straight to useQuery. Zero caches the queries you build with ZQL. Build the same query again and you usually get back the same Query object, even when it has related() or whereExists() callbacks written inline. So rebuilding a query on every render is cheap. You can also use a query as a dependency of useEffect or useMemo, or pass it as a prop to a component wrapped in React.memo: import {useEffect} from 'react' import {useQuery} from '@rocicorp/zero/react' import {zql} from 'schema.ts' function IssueSearch({text}: {text: string}) { // Rebuilt on every render. No useMemo needed. const query = zql.issue .where('title', 'ILIKE', `%${text}%`) .orderBy('created', 'desc') const [issues] = useQuery(query) // Scroll back to the top when the search changes. useEffect(() => { window.scrollTo(0, 0) }, [query]) return issues.map(issue => (
{issue.title}
)) } This example passes ZQL to useQuery directly, so it runs only on the client. Calls to registered queries work a little differently. Each call like queries.posts.byStatus({status}) returns a new object. You can still pass it inline: useQuery sees that it's the same query with the same arguments and reuses its previous work. But don't use the returned object as a dependency. Use its arguments instead. Identity follows the order you build a query in. zql.issue.where('status', 'open').where('priority', 'high') and zql.issue.where('priority', 'high').where('status', 'open') return the same rows but can be different objects. A different value, such as another ID in where(), always gives a different object. Zero doesn't guarantee identity, though. It can hand back a new object for a query it has built before. When that happens, an effect or memo that depends on the query runs again, so make sure that's safe.", "kind": "section" }, { @@ -8300,7 +8300,7 @@ "title": "zero-cache Config", "searchTitle": "zero-cache Config", "url": "/docs/zero-cache-config", - "content": "zero-cache is configured either via CLI flag or environment variable. There is no separate zero.config file. You can also see all available flags by running zero-cache --help. Required Flags Upstream DB The \"upstream\" authoritative postgres database. In the future we will support other types of upstream besides PG. flag: --upstream-db env: ZERO_UPSTREAM_DB required: true Admin Password A password used to administer zero-cache server, for example to access the /statz endpoint and the inspector. This is required in production (when NODE_ENV=production) because we want all Zero servers to be debuggable using admin tools by default, without needing a restart. But we also don't want to expose sensitive data using them. The admin password also grants access to everything that the operator password does. flag: --admin-password env: ZERO_ADMIN_PASSWORD required: in production (when NODE_ENV=production) Optional Flags Allow Legacy Queries Allows clients to send query ASTs directly to zero-cache. Ad-hoc queries (also called legacy queries) work this way. When this is false, zero-cache rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use queries. flag: --allow-legacy-queries env: ZERO_ALLOW_LEGACY_QUERIES default: false App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: 300 Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: 300 Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 4 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 168 (one week) CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. With Litestream v5 backups (the default), configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 or ZERO_LITESTREAM_BACKUP_USING_V5 is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build 0.5.18-zero.16. Restores fork the source backup with litestream restore -fork-to-url, so a custom executable must support that flag. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and ZERO_LITESTREAM_BACKUP_USING_V5 to false. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. Set this to false to write legacy WAL backups with Litestream v0.3.x instead. This requires ZERO_LITESTREAM_RESTORE_USING_V5, which is enabled by default. Zero requests an incremental backup every ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS, and acknowledges upstream changes only after a backup containing them has completed. Each replication-manager backs up to its own subfolder of the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: true Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. Leave this unset to use 48, lowered as needed so that the parts fit in a quarter of the memory available to the container. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Operator Password A password that grants access only to the diagnostic endpoints that cannot expose application data. People who operate zero-cache can use it to diagnose the server without being able to read the data it serves. These endpoints are: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Initial Readiness Threshold (ms) The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. When lag reporting is disabled, readiness does not wait for replication lag. flag: --replication-lag-initial-readiness-threshold-ms env: ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS default: 1000 Replication Lag Max Readiness Delay (ms) The maximum time that a server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. flag: --replication-lag-max-readiness-delay-ms env: ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS default: 600000 (10 minutes) Replication Lag Report Interval (ms) The approximate interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. Lag reports are also used at startup: a new server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready. This interval sets how long the server waits for the next report. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 10000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS View Syncer Hydration Budget (ms) A soft time limit for hydrating inactive (cached) queries when a view-syncer hydrates a client group's queries, for example after a client reconnects. Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your query endpoint does not count toward the budget. A value of 0 disables the budget. flag: --view-syncer-hydration-budget-ms env: ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS default: 0 View Syncer Query Hydration Timeout (ms) The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see Handling Errors). For the next five minutes, the query is rejected without being run again. After that, it can be retried. A value of 0 disables the limit. flag: --view-syncer-query-hydration-timeout-ms env: ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS default: 0 Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10 Deprecated Flags Change Max Connections ZERO_CHANGE_MAX_CONNS is deprecated and no longer controls change database connection usage. Size the database or pooler as described under Change DB. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS Change Streamer Mode ZERO_CHANGE_STREAMER_MODE is deprecated. Set ZERO_CHANGE_STREAMER_URI on view-syncers instead. The mode for running or connecting to the change-streamer: dedicated: runs the change-streamer and shuts down when another change-streamer takes over the replication slot. This is appropriate in a single-node configuration, or for the replication-manager in a multi-node configuration. discover: connects to the change-streamer as internally advertised in the change-db. This is appropriate for the view-syncers in a multi-node setup. This may not work in all networking configurations (e.g., some private networking or port forwarding setups). This option is ignored if ZERO_CHANGE_STREAMER_URI is set. flag: --change-streamer-mode env: ZERO_CHANGE_STREAMER_MODE default: dedicated Litestream Minimum Checkpoint Page Count ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT is deprecated. Use ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB instead. The WAL page count at which SQLite attempts a PASSIVE checkpoint. If unset, it is derived from the checkpoint threshold as checkpointThresholdMB * 256 (4 KiB pages). flag: --litestream-min-checkpoint-page-count env: ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT default: checkpointThresholdMB * 256 Auth JWK A public key in JWK format used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwk env: ZERO_AUTH_JWK Auth JWKS URL A URL that returns a JWK set used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwks-url env: ZERO_AUTH_JWKS_URL Auth Secret A symmetric key used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-secret env: ZERO_AUTH_SECRET", + "content": "zero-cache is configured either via CLI flag or environment variable. There is no separate zero.config file. You can also see all available flags by running zero-cache --help. Required Flags Upstream DB The \"upstream\" authoritative postgres database. In the future we will support other types of upstream besides PG. flag: --upstream-db env: ZERO_UPSTREAM_DB required: true Admin Password A password used to administer zero-cache server, for example to access the /statz endpoint and the inspector. This is required in production (when NODE_ENV=production) because we want all Zero servers to be debuggable using admin tools by default, without needing a restart. But we also don't want to expose sensitive data using them. The admin password also grants access to everything that the operator password does. flag: --admin-password env: ZERO_ADMIN_PASSWORD required: in production (when NODE_ENV=production) Optional Flags Allow Legacy Queries Allows clients to send query ASTs directly to zero-cache. Ad-hoc queries (also called legacy queries) work this way. When this is false, zero-cache rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use queries. flag: --allow-legacy-queries env: ZERO_ALLOW_LEGACY_QUERIES default: false App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: 300 Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: 300 Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 4 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 168 (one week) CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. With Litestream v5 backups (the default), configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 or ZERO_LITESTREAM_BACKUP_USING_V5 is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build 0.5.18-zero.16. Restores fork the source backup with litestream restore -fork-to-url, so a custom executable must support that flag. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and ZERO_LITESTREAM_BACKUP_USING_V5 to false. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. Set this to false to write legacy WAL backups with Litestream v0.3.x instead. This requires ZERO_LITESTREAM_RESTORE_USING_V5, which is enabled by default. Zero requests an incremental backup every ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS, and acknowledges upstream changes only after a backup containing them has completed. Each replication-manager backs up to its own subfolder of the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: true Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. Leave this unset to use 48, lowered as needed so that the parts fit in a quarter of the memory available to the container. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Operator Password A password for the diagnostic endpoints that can't expose application data, so the people who run zero-cache can debug it without being able to read the data it serves. It grants access to: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Initial Readiness Threshold (ms) The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. When lag reporting is disabled, readiness does not wait for replication lag. flag: --replication-lag-initial-readiness-threshold-ms env: ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS default: 1000 Replication Lag Max Readiness Delay (ms) The maximum time that a server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. flag: --replication-lag-max-readiness-delay-ms env: ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS default: 600000 (10 minutes) Replication Lag Report Interval (ms) The approximate interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. Lag reports are also used at startup: a new server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready. This interval sets how long the server waits for the next report. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 10000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS View Syncer Hydration Budget (ms) A soft time limit for hydrating inactive (cached) queries when a view-syncer hydrates a client group's queries, for example after a client reconnects. Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your query endpoint does not count toward the budget. A value of 0 disables the budget. flag: --view-syncer-hydration-budget-ms env: ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS default: 0 View Syncer Query Hydration Timeout (ms) The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see Handling Errors). For the next five minutes, the query is rejected without being run again. After that, it can be retried. A value of 0 disables the limit. flag: --view-syncer-query-hydration-timeout-ms env: ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS default: 0 Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10 Deprecated Flags Change Max Connections ZERO_CHANGE_MAX_CONNS is deprecated and no longer controls change database connection usage. Size the database or pooler as described under Change DB. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS Change Streamer Mode ZERO_CHANGE_STREAMER_MODE is deprecated. Set ZERO_CHANGE_STREAMER_URI on view-syncers instead. The mode for running or connecting to the change-streamer: dedicated: runs the change-streamer and shuts down when another change-streamer takes over the replication slot. This is appropriate in a single-node configuration, or for the replication-manager in a multi-node configuration. discover: connects to the change-streamer as internally advertised in the change-db. This is appropriate for the view-syncers in a multi-node setup. This may not work in all networking configurations (e.g., some private networking or port forwarding setups). This option is ignored if ZERO_CHANGE_STREAMER_URI is set. flag: --change-streamer-mode env: ZERO_CHANGE_STREAMER_MODE default: dedicated Litestream Minimum Checkpoint Page Count ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT is deprecated. Use ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB instead. The WAL page count at which SQLite attempts a PASSIVE checkpoint. If unset, it is derived from the checkpoint threshold as checkpointThresholdMB * 256 (4 KiB pages). flag: --litestream-min-checkpoint-page-count env: ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT default: checkpointThresholdMB * 256 Auth JWK A public key in JWK format used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwk env: ZERO_AUTH_JWK Auth JWKS URL A URL that returns a JWK set used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwks-url env: ZERO_AUTH_JWKS_URL Auth Secret A symmetric key used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-secret env: ZERO_AUTH_SECRET", "headings": [ { "text": "Required Flags", @@ -8726,7 +8726,7 @@ "sectionTitle": "Optional Flags", "sectionId": "optional-flags", "url": "/docs/zero-cache-config", - "content": "Allow Legacy Queries Allows clients to send query ASTs directly to zero-cache. Ad-hoc queries (also called legacy queries) work this way. When this is false, zero-cache rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use queries. flag: --allow-legacy-queries env: ZERO_ALLOW_LEGACY_QUERIES default: false App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: 300 Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: 300 Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 4 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 168 (one week) CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. With Litestream v5 backups (the default), configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 or ZERO_LITESTREAM_BACKUP_USING_V5 is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build 0.5.18-zero.16. Restores fork the source backup with litestream restore -fork-to-url, so a custom executable must support that flag. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and ZERO_LITESTREAM_BACKUP_USING_V5 to false. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. Set this to false to write legacy WAL backups with Litestream v0.3.x instead. This requires ZERO_LITESTREAM_RESTORE_USING_V5, which is enabled by default. Zero requests an incremental backup every ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS, and acknowledges upstream changes only after a backup containing them has completed. Each replication-manager backs up to its own subfolder of the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: true Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. Leave this unset to use 48, lowered as needed so that the parts fit in a quarter of the memory available to the container. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Operator Password A password that grants access only to the diagnostic endpoints that cannot expose application data. People who operate zero-cache can use it to diagnose the server without being able to read the data it serves. These endpoints are: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Initial Readiness Threshold (ms) The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. When lag reporting is disabled, readiness does not wait for replication lag. flag: --replication-lag-initial-readiness-threshold-ms env: ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS default: 1000 Replication Lag Max Readiness Delay (ms) The maximum time that a server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. flag: --replication-lag-max-readiness-delay-ms env: ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS default: 600000 (10 minutes) Replication Lag Report Interval (ms) The approximate interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. Lag reports are also used at startup: a new server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready. This interval sets how long the server waits for the next report. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 10000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS View Syncer Hydration Budget (ms) A soft time limit for hydrating inactive (cached) queries when a view-syncer hydrates a client group's queries, for example after a client reconnects. Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your query endpoint does not count toward the budget. A value of 0 disables the budget. flag: --view-syncer-hydration-budget-ms env: ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS default: 0 View Syncer Query Hydration Timeout (ms) The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see Handling Errors). For the next five minutes, the query is rejected without being run again. After that, it can be retried. A value of 0 disables the limit. flag: --view-syncer-query-hydration-timeout-ms env: ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS default: 0 Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10", + "content": "Allow Legacy Queries Allows clients to send query ASTs directly to zero-cache. Ad-hoc queries (also called legacy queries) work this way. When this is false, zero-cache rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use queries. flag: --allow-legacy-queries env: ZERO_ALLOW_LEGACY_QUERIES default: false App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: 300 Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: 300 Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 4 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 168 (one week) CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. With Litestream v5 backups (the default), configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 or ZERO_LITESTREAM_BACKUP_USING_V5 is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build 0.5.18-zero.16. Restores fork the source backup with litestream restore -fork-to-url, so a custom executable must support that flag. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and ZERO_LITESTREAM_BACKUP_USING_V5 to false. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. Set this to false to write legacy WAL backups with Litestream v0.3.x instead. This requires ZERO_LITESTREAM_RESTORE_USING_V5, which is enabled by default. Zero requests an incremental backup every ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS, and acknowledges upstream changes only after a backup containing them has completed. Each replication-manager backs up to its own subfolder of the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: true Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. Leave this unset to use 48, lowered as needed so that the parts fit in a quarter of the memory available to the container. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Operator Password A password for the diagnostic endpoints that can't expose application data, so the people who run zero-cache can debug it without being able to read the data it serves. It grants access to: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Initial Readiness Threshold (ms) The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. When lag reporting is disabled, readiness does not wait for replication lag. flag: --replication-lag-initial-readiness-threshold-ms env: ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS default: 1000 Replication Lag Max Readiness Delay (ms) The maximum time that a server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. flag: --replication-lag-max-readiness-delay-ms env: ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS default: 600000 (10 minutes) Replication Lag Report Interval (ms) The approximate interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. Lag reports are also used at startup: a new server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready. This interval sets how long the server waits for the next report. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 10000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS View Syncer Hydration Budget (ms) A soft time limit for hydrating inactive (cached) queries when a view-syncer hydrates a client group's queries, for example after a client reconnects. Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your query endpoint does not count toward the budget. A value of 0 disables the budget. flag: --view-syncer-hydration-budget-ms env: ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS default: 0 View Syncer Query Hydration Timeout (ms) The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see Handling Errors). For the next five minutes, the query is rejected without being run again. After that, it can be retried. A value of 0 disables the limit. flag: --view-syncer-query-hydration-timeout-ms env: ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS default: 0 Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10", "kind": "section" }, { @@ -9266,7 +9266,7 @@ "sectionTitle": "Operator Password", "sectionId": "operator-password", "url": "/docs/zero-cache-config", - "content": "A password that grants access only to the diagnostic endpoints that cannot expose application data. People who operate zero-cache can use it to diagnose the server without being able to read the data it serves. These endpoints are: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD", + "content": "A password for the diagnostic endpoints that can't expose application data, so the people who run zero-cache can debug it without being able to read the data it serves. It grants access to: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD", "kind": "section" }, { diff --git a/contents/docs/connection.mdx b/contents/docs/connection.mdx index 2be62c67..5b0d1861 100644 --- a/contents/docs/connection.mdx +++ b/contents/docs/connection.mdx @@ -12,7 +12,7 @@ Zero manages a persistent connection to `zero-cache` with the following lifecycl invert="light" /> -Before this lifecycle starts, Zero is in the [`initializing`](#initializing) state while it loads local data. It then moves to `connecting` and never returns to `initializing`. +Before any of this, Zero is [`initializing`](#initializing) while it loads local data. After that it moves to `connecting` and never goes back. ## Usage @@ -151,17 +151,15 @@ The easiest way to implement this is with a modal overlay that covers the entire ### Initializing -Zero starts in the `initializing` state. While `initializing`, Zero loads its local data. It has not contacted `zero-cache` yet. +Zero starts in the `initializing` state, where it loads its local data. It hasn't contacted `zero-cache` yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take tens of seconds. -How long this takes depends on the device and the amount of local data, not on the network. On a slow device with a lot of local data, it can take tens of seconds. +When the data is loaded, Zero moves to `connecting`. This only happens once, right after Zero is created. Later reconnects go straight to `connecting`. -Once the local data is loaded, Zero transitions to `connecting`. Zero is only `initializing` once, right after it is created. When Zero reconnects later, it goes straight to `connecting`. +While `initializing`, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters `connecting`. -Show a loading indicator while `initializing`, not an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters `connecting`, not when you create Zero. +There is no timeout while `initializing`, and the time doesn't count against `disconnectTimeoutMs`. If the local data never finishes loading, Zero stays in `initializing`, so add your own timeout if you need one. -There is no timeout while `initializing`, and the time spent here does not count against `disconnectTimeoutMs`. If the local data never finishes loading, Zero stays in `initializing`. To limit this, add your own timeout. - -Reads and writes are allowed while `initializing`. Queries return results once the local data is loaded. Writes are queued and are sent when Zero connects. +Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects. ### Connecting @@ -257,9 +255,9 @@ Reads and writes are both rejected while Zero is in the `closed` state. ## Storage Failures -Zero saves synced data on the device: in IndexedDB in the browser, or in SQLite on [React Native](/docs/react-native). This storage can fail. For example, the device can run out of space, or the database can fail to open. +Zero stores synced data on the device, in IndexedDB in the browser or SQLite on [React Native](/docs/react-native). Sometimes that fails: the device runs out of space, or the database won't open. -When this happens, Zero calls `onStorageFailure` once with a `StorageFailureError`. Its `kind` says what failed: +When it does, Zero calls `onStorageFailure` once with a `StorageFailureError`. Its `kind` says what went wrong: | `kind` | Meaning | | ------------- | --------------------------------------- | @@ -269,12 +267,9 @@ When this happens, Zero calls `onStorageFailure` once with a `StorageFailureErro In the browser, Zero reports a storage failure only when IndexedDB fails to open: `full` if the browser says the disk is full, otherwise `cannot-open`. -Zero keeps working after a storage failure: - -- **If storage fails while Zero is starting**, Zero runs in memory for the rest of the session, as if you had set `kvStore: 'mem'`. Nothing is saved on the device. -- **If storage fails later**, Zero stops saving data on the device. Queries and mutations keep working on the data in memory, and mutations are still sent to `zero-cache`. A read that needs data Zero hasn't loaded into memory can still fail. +Zero keeps working either way. If storage fails while Zero is starting, Zero runs in memory for the rest of the session, as if you had set `kvStore: 'mem'`, and nothing is saved on the device. If it fails later, Zero stops saving to the device. Queries and mutations keep working on the data in memory and mutations are still sent to `zero-cache`, but a read that needs data Zero hasn't loaded into memory can still fail. -Zero tries the device's storage again only when the app creates a new `Zero` instance, for example after a restart. +Zero only tries the device's storage again when the app creates a new `Zero` instance, for example after a restart. By default, Zero only logs the failure. Use `onStorageFailure` to tell the user what to do: diff --git a/contents/docs/mutators.mdx b/contents/docs/mutators.mdx index d4158264..e1866c79 100644 --- a/contents/docs/mutators.mdx +++ b/contents/docs/mutators.mdx @@ -866,9 +866,9 @@ If you want a different behavior, it is possible to [implement the mutate endpoi ### Retrying Mutators -Some errors go away if you try again. For example, at the `repeatable read` or `serializable` [isolation level](/docs/server-zql#isolation-level), Postgres can abort a transaction that conflicts with a concurrent one. The error is a serialization failure, code `40001`. +Some errors go away on a second try. At the `repeatable read` and `serializable` [isolation levels](/docs/server-zql#isolation-level), for example, Postgres can abort a transaction that conflicts with a concurrent one. It fails with a serialization failure, code `40001`. -Call `tx.retryOn()` in a server mutator to have Zero run the mutator again, in a fresh transaction, when it fails with an error you choose: +To have Zero run a server mutator again in a fresh transaction when it fails with an error like that, call `tx.retryOn()`: ```ts function isSerializationFailure(error: unknown) { @@ -911,14 +911,13 @@ tx.retryOn(async (error, attempt) => { }) ``` -A few things to know: +`retryOn()` only applies to the current run. The re-run starts the mutator from the top and calls `retryOn()` again, so call it before anything that can fail. You can call it more than once, and Zero runs the mutator again if any predicate returns `true`. -- `retryOn()` covers only the current run. The re-run calls it again because it runs the mutator again. Call it before anything that can fail. -- You can call `retryOn()` more than once. Zero runs the mutator again if any predicate returns `true`. -- Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run fails, Zero records the error. -- The predicate runs after the transaction has ended, so it can't use `tx`. Reading or writing through `tx` there throws. -- Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. -- `retryOn()` is only available on the server, when `tx.location === 'server'`. +Zero runs a mutator at most 5 times per mutation in each request to your mutate endpoint. If the last run also fails, Zero records the error. + +The predicate runs after the transaction has ended, so it can't use `tx`: reading or writing through `tx` there throws. + +Only retry mutators that are safe to run more than once. Work outside the database, such as sending an email, isn't rolled back. `retryOn()` only exists on the server, when `tx.location === 'server'`. { The possible values of `result.type` are: -- `unknown`: the data Zero has on the client. Some of it may be missing. +- `unknown`: whatever data the client has right now, which may be incomplete. - `cached`: the client stores a result that the server confirmed during a previous session. See [Cached Results](#cached-results). - `complete`: the server has returned the result during this session. - `error`: the query failed. See [Handling Errors](#handling-errors). @@ -787,9 +787,11 @@ The `complete` value is currently only returned when Zero has received the serve ### Cached Results -When Zero stores data on the client (in IndexedDB, or SQLite on React Native), it also remembers which query results the server confirmed. If the client still holds such a result when the app starts again, the query starts as `cached` instead of `unknown`, and becomes `complete` once the server returns the result in this session. If Zero evicts the stored result before then, the query goes back to `unknown`. +Along with the data it stores on the client (IndexedDB in the browser, SQLite on React Native), Zero remembers which query results the server has confirmed. When the app starts again and the client still has one of those results, the query starts out `cached` instead of `unknown`. It becomes `complete` once the server sends the result again. If Zero evicts the stored result first, the query drops back to `unknown`. -A `cached` result was complete when the server sent it, but it may be out of date. Use it where a previous session's answer is good enough, for example to show stored data or an empty state without waiting for the server, which also works while offline. Wait for `complete` where the data must be current: +A `cached` result was complete when the server sent it, but it may be stale by now. It's good for showing stored data, or an empty state, without waiting for the server, and that works offline too. If the data has to be current, wait for `complete`. + +For example, to show an empty inbox without waiting for the server: { -`zero.run(query, {type: 'cached'})` and the `cached` promise from `zero.preload()` resolve on a `cached` or `complete` result. `{type: 'complete'}` and the `complete` promise still wait for the server. +`zero.run()` and `zero.preload()` can also return as soon as a result is `cached`: pass `{type: 'cached'}` to `run()`, or wait on the `cached` promise that `preload()` returns. ## Handling Errors diff --git a/contents/docs/react.mdx b/contents/docs/react.mdx index 3b621852..cfc6342d 100644 --- a/contents/docs/react.mdx +++ b/contents/docs/react.mdx @@ -148,11 +148,9 @@ This example passes ZQL to `useQuery` directly, so it runs [only on the client]( Calls to [registered queries](/docs/queries#query-registries) work a little differently. Each call like `queries.posts.byStatus({status})` returns a new object. You can still pass it inline: `useQuery` sees that it's the same query with the same arguments and reuses its previous work. But don't use the returned object as a dependency. Use its arguments instead. -Keep in mind: +Identity follows the order you build a query in. `zql.issue.where('status', 'open').where('priority', 'high')` and `zql.issue.where('priority', 'high').where('status', 'open')` return the same rows but can be different objects. A different value, such as another ID in `where()`, always gives a different object. -- Identity follows how you build a query. `zql.issue.where('status', 'open').where('priority', 'high')` and `zql.issue.where('priority', 'high').where('status', 'open')` return the same rows, but they can be different objects. -- A different value, such as a different ID in `where()`, gives a different object. -- Identity is an optimization, not a promise. Zero can hand back a new object for a query it has built before. When that happens, an effect or memo that depends on the query just runs again, so make sure that's safe. +Zero doesn't guarantee identity, though. It can hand back a new object for a query it has built before. When that happens, an effect or memo that depends on the query runs again, so make sure that's safe. ## Examples diff --git a/contents/docs/zero-cache-config.mdx b/contents/docs/zero-cache-config.mdx index 89379f90..1c49ecb2 100644 --- a/contents/docs/zero-cache-config.mdx +++ b/contents/docs/zero-cache-config.mdx @@ -568,7 +568,7 @@ env: `ZERO_NUM_SYNC_WORKERS`
### Operator Password -A password that grants access only to the diagnostic endpoints that cannot expose application data. People who operate `zero-cache` can use it to diagnose the server without being able to read the data it serves. These endpoints are: +A password for the diagnostic endpoints that can't expose application data, so the people who run `zero-cache` can debug it without being able to read the data it serves. It grants access to: - `/profz` and `/profrmz`: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain `LIKE` patterns from client queries. - `/statz`: counts and sizes. From d518bfccfc37806ac49c98f740a9e55745ad2e10 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Thu, 15 Oct 2026 08:52:57 +0000 Subject: [PATCH 14/20] docs(connection): say initializing can take many seconds --- assets/search-index.json | 6 +++--- contents/docs/connection.mdx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/search-index.json b/assets/search-index.json index dc5144ab..898515e1 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -781,7 +781,7 @@ "title": "Connection Status", "searchTitle": "Connection Status", "url": "/docs/connection", - "content": "Overview Zero manages a persistent connection to zero-cache with the following lifecycle: Before any of this, Zero is initializing while it loads local data. After that it moves to connecting and never goes back. Usage The current connection state is available in the zero.connection.state property. This is subscribable and also has reactive hooks for React and SolidJS: import {useConnectionState} from '@rocicorp/zero/react' function ConnectionStatus() { const state = useConnectionState() switch (state.name) { case 'initializing': return
Loading...
case 'connecting': return
Connecting...
case 'connected': return
Connected
case 'disconnected': return
Offline
case 'error': return
Error
case 'needs-auth': return
Session expired
default: return null } }import {useConnectionState} from '@rocicorp/zero/solid' function ConnectionStatus() { const state = useConnectionState() return (
Loading...
Connecting...
Connected
Offline
Error
Session expired
) }zero.connection.state.subscribe(state => { switch (state.name) { case 'initializing': console.log('Loading local data...') break case 'connecting': console.log(`Connecting... ${state.reason}`) break case 'connected': console.log('Connected') break case 'disconnected': console.log(`Disconnected ${state.reason}`) break case 'error': console.log(`Error ${state.reason}`) break case 'needs-auth': console.log('Session expired') break default: return null } }) Offline Zero does not support offline writes. When the client is in the disconnected, error, or needs-auth states, reads from synced data continue to work, but writes are rejected. Offline UI While Zero is in the disconnected, error, or needs-auth states, you should prevent the user from inputting data to your application to avoid data loss. Zero automates this as best it can by rejecting writes in these states. But there can still be cases where the user can lose work – for example by typing into a textarea that is only written to Zero when the user presses a button. The easiest way to implement this is with a modal overlay that covers the entire screen and tells the user to reconnect. However, you could also continue to let the user use the app read-only, and only disable inputs. Details Initializing Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take tens of seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state. Storage Failures Zero stores synced data on the device, in IndexedDB in the browser or SQLite on React Native. Sometimes that fails: the device runs out of space, or the database won't open. When it does, Zero calls onStorageFailure once with a StorageFailureError. Its kind says what went wrong: In the browser, Zero reports a storage failure only when IndexedDB fails to open: full if the browser says the disk is full, otherwise cannot-open. Zero keeps working either way. If storage fails while Zero is starting, Zero runs in memory for the rest of the session, as if you had set kvStore: 'mem', and nothing is saved on the device. If it fails later, Zero stops saving to the device. Queries and mutations keep working on the data in memory and mutations are still sent to zero-cache, but a read that needs data Zero hasn't loaded into memory can still fail. Zero only tries the device's storage again when the app creates a new Zero instance, for example after a restart. By default, Zero only logs the failure. Use onStorageFailure to tell the user what to do: import type {ZeroOptions} from '@rocicorp/zero' const opts: ZeroOptions = { // ... onStorageFailure: failure => { // showBanner stands in for your app's own UI. if (failure.kind === 'full') { showBanner( 'Your device is out of space. Free up some space, then restart the app.' ) } else { showBanner( 'Data could not be saved on this device. Restart the app to try again.' ) } } } StorageFailureError is exported from @rocicorp/zero. Its cause is the original error from the browser or the SQLite driver. A custom kvStore can throw a StorageFailureError to get the same handling. Checking the Store zero.kvStore.kind tells you which storage Zero is using: 'idb', 'mem', 'expo-sqlite', or 'op-sqlite'. A custom store reports its own kind, if it sets one. if (zero.kvStore.kind === 'mem') { // Data is not saved on this device. } Without IndexedDB, kind is 'mem' from the start and onStorageFailure isn't called. When IndexedDB fails to open, kind changes to 'mem' only when Zero first reads or writes the store, so check kind when you need it, not once at startup. Why Zero Doesn't Support Offline Writes Supporting offline writes in collaborative applications is inherently difficult, and no sync engine or CRDT algorithm can automatically solve it for you. Despite what their marketing says 😉. Example Imagine two users are editing an article about cats. One goes offline and does a bunch of work on the article, while the other decides that the article should actually be about dogs and rewrites it. When the offline user reconnects, there is no way that any software algorithm can automatically resolve their conflict. One or the other of them is going to be upset. This is a trivial data model with a single field, and is already unsolvable. Real-world applications are much worse: Foreign keys and other constraints can pass while offline, but break when the user reconnects. Custom business logic and authorization rules can pass while offline, but break when the user reconnects. The application's schema can change while offline, and the user's data may not be processable by the new schema. Just take your own schema and ask yourself what should really happen if one user takes their device offline for a week and makes arbitrarily complex changes while other users are working online. Tradeoffs It is of course possible to create applications that support offline writes well (Git exists!). But it requires significant tradeoffs. For example, you could: Disallow destructive operations (i.e., users can create tasks while offline, but cannot edit or delete them). Support custom UX to allow users to fork and merge conflicts when they occur. Restrict offline writes to a single device. Accept potential user data loss. Zero's Position While we recognize that offline writes would be useful, the reality is that for most of the apps we want to support, the user is online the vast majority of the time and the cost to support offline is extremely high. There is simply more value in making the online experience great first, and that's where we're focused right now. We would like to revisit this in the future, but it's not a priority right now.", + "content": "Overview Zero manages a persistent connection to zero-cache with the following lifecycle: Before any of this, Zero is initializing while it loads local data. After that it moves to connecting and never goes back. Usage The current connection state is available in the zero.connection.state property. This is subscribable and also has reactive hooks for React and SolidJS: import {useConnectionState} from '@rocicorp/zero/react' function ConnectionStatus() { const state = useConnectionState() switch (state.name) { case 'initializing': return
Loading...
case 'connecting': return
Connecting...
case 'connected': return
Connected
case 'disconnected': return
Offline
case 'error': return
Error
case 'needs-auth': return
Session expired
default: return null } }import {useConnectionState} from '@rocicorp/zero/solid' function ConnectionStatus() { const state = useConnectionState() return (
Loading...
Connecting...
Connected
Offline
Error
Session expired
) }zero.connection.state.subscribe(state => { switch (state.name) { case 'initializing': console.log('Loading local data...') break case 'connecting': console.log(`Connecting... ${state.reason}`) break case 'connected': console.log('Connected') break case 'disconnected': console.log(`Disconnected ${state.reason}`) break case 'error': console.log(`Error ${state.reason}`) break case 'needs-auth': console.log('Session expired') break default: return null } }) Offline Zero does not support offline writes. When the client is in the disconnected, error, or needs-auth states, reads from synced data continue to work, but writes are rejected. Offline UI While Zero is in the disconnected, error, or needs-auth states, you should prevent the user from inputting data to your application to avoid data loss. Zero automates this as best it can by rejecting writes in these states. But there can still be cases where the user can lose work – for example by typing into a textarea that is only written to Zero when the user presses a button. The easiest way to implement this is with a modal overlay that covers the entire screen and tells the user to reconnect. However, you could also continue to let the user use the app read-only, and only disable inputs. Details Initializing Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take many seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state. Storage Failures Zero stores synced data on the device, in IndexedDB in the browser or SQLite on React Native. Sometimes that fails: the device runs out of space, or the database won't open. When it does, Zero calls onStorageFailure once with a StorageFailureError. Its kind says what went wrong: In the browser, Zero reports a storage failure only when IndexedDB fails to open: full if the browser says the disk is full, otherwise cannot-open. Zero keeps working either way. If storage fails while Zero is starting, Zero runs in memory for the rest of the session, as if you had set kvStore: 'mem', and nothing is saved on the device. If it fails later, Zero stops saving to the device. Queries and mutations keep working on the data in memory and mutations are still sent to zero-cache, but a read that needs data Zero hasn't loaded into memory can still fail. Zero only tries the device's storage again when the app creates a new Zero instance, for example after a restart. By default, Zero only logs the failure. Use onStorageFailure to tell the user what to do: import type {ZeroOptions} from '@rocicorp/zero' const opts: ZeroOptions = { // ... onStorageFailure: failure => { // showBanner stands in for your app's own UI. if (failure.kind === 'full') { showBanner( 'Your device is out of space. Free up some space, then restart the app.' ) } else { showBanner( 'Data could not be saved on this device. Restart the app to try again.' ) } } } StorageFailureError is exported from @rocicorp/zero. Its cause is the original error from the browser or the SQLite driver. A custom kvStore can throw a StorageFailureError to get the same handling. Checking the Store zero.kvStore.kind tells you which storage Zero is using: 'idb', 'mem', 'expo-sqlite', or 'op-sqlite'. A custom store reports its own kind, if it sets one. if (zero.kvStore.kind === 'mem') { // Data is not saved on this device. } Without IndexedDB, kind is 'mem' from the start and onStorageFailure isn't called. When IndexedDB fails to open, kind changes to 'mem' only when Zero first reads or writes the store, so check kind when you need it, not once at startup. Why Zero Doesn't Support Offline Writes Supporting offline writes in collaborative applications is inherently difficult, and no sync engine or CRDT algorithm can automatically solve it for you. Despite what their marketing says 😉. Example Imagine two users are editing an article about cats. One goes offline and does a bunch of work on the article, while the other decides that the article should actually be about dogs and rewrites it. When the offline user reconnects, there is no way that any software algorithm can automatically resolve their conflict. One or the other of them is going to be upset. This is a trivial data model with a single field, and is already unsolvable. Real-world applications are much worse: Foreign keys and other constraints can pass while offline, but break when the user reconnects. Custom business logic and authorization rules can pass while offline, but break when the user reconnects. The application's schema can change while offline, and the user's data may not be processable by the new schema. Just take your own schema and ask yourself what should really happen if one user takes their device offline for a week and makes arbitrarily complex changes while other users are working online. Tradeoffs It is of course possible to create applications that support offline writes well (Git exists!). But it requires significant tradeoffs. For example, you could: Disallow destructive operations (i.e., users can create tasks while offline, but cannot edit or delete them). Support custom UX to allow users to fork and merge conflicts when they occur. Restrict offline writes to a single device. Accept potential user data loss. Zero's Position While we recognize that offline writes would be useful, the reality is that for most of the apps we want to support, the user is online the vast majority of the time and the cost to support offline is extremely high. There is simply more value in making the online experience great first, and that's where we're focused right now. We would like to revisit this in the future, but it's not a priority right now.", "headings": [ { "text": "Overview", @@ -905,7 +905,7 @@ "sectionTitle": "Details", "sectionId": "details", "url": "/docs/connection", - "content": "Initializing Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take tens of seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state.", + "content": "Initializing Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take many seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state.", "kind": "section" }, { @@ -915,7 +915,7 @@ "sectionTitle": "Initializing", "sectionId": "initializing", "url": "/docs/connection", - "content": "Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take tens of seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects.", + "content": "Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take many seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects.", "kind": "section" }, { diff --git a/contents/docs/connection.mdx b/contents/docs/connection.mdx index 5b0d1861..32ab5f85 100644 --- a/contents/docs/connection.mdx +++ b/contents/docs/connection.mdx @@ -151,7 +151,7 @@ The easiest way to implement this is with a modal overlay that covers the entire ### Initializing -Zero starts in the `initializing` state, where it loads its local data. It hasn't contacted `zero-cache` yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take tens of seconds. +Zero starts in the `initializing` state, where it loads its local data. It hasn't contacted `zero-cache` yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take many seconds. When the data is loaded, Zero moves to `connecting`. This only happens once, right after Zero is created. Later reconnects go straight to `connecting`. From b8a4c4b9dad2eb4b8d3b5c9cb5312785de44a187 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Thu, 15 Oct 2026 09:21:33 +0000 Subject: [PATCH 15/20] docs(release-notes): correct the 1.11 Litestream and React Native details The pre-v5 backup format is Litestream v0.3 (legacy WAL), not v4. The React Native aggregate counted the three JSON.parse control benchmarks. Without them it is 53 benchmarks with a geometric mean of 1.68x, so say about 1.7x. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RRckJnCeGsYg9nCJSxBgTJ --- assets/search-index.json | 8 ++++---- contents/docs/release-notes/1.11.mdx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/search-index.json b/assets/search-index.json index 898515e1..3b5ee5af 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -5687,7 +5687,7 @@ "title": "Zero 1.11", "searchTitle": "Zero 1.11", "url": "/docs/release-notes/1.11", - "content": "Installation npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Overview Zero 1.11 can show cached query results right away, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication. Features Queries can now start with results that a previous session saved on the device. These come back with the new 'cached' result type, so you can show them right away and still tell them apart from data the server has confirmed. (#6578) (thanks @pawarren!) Building the same query again usually gives you the same Query object. useQuery no longer rebuilds and rehashes its query on every render, and queries built inline work as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero starts in a new 'initializing' connection state while it loads local data, and slow loading doesn't count against the connect timeout anymore. (#6561) If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore tells you which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Clients upgrading from 1.10 keep their local data and pending mutations. Future protocol changes won't reset clients to an empty local database either. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when tx.retryOn() accepts the error, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. A replica restored from another replica's backup continues that backup incrementally. Periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits for replication to catch up before it reports ready. (1c5ddcdb4) Inactive client groups are kept for 7 days instead of 48 hours, so users who come back within a week don't need a full resync. (#6484) zero-cache now logs slow queries with the reason and suggested indexes, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) A new hydration budget caps the time a view-syncer spends re-hydrating a reconnecting client's inactive queries, and a per-query hydration timeout aborts any query that takes too long to hydrate. Both are off by default. (#6477, #6499) New OpenTelemetry metrics track WAL usage. Replication status events now include index-build progress, and a new view-syncer status event reports restore progress. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image is down to 807 MB from about 1.09 GB. (#6695) Performance Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x less CPU. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.6x faster on React Native. That's the geometric mean of 56 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead. Fixes Query Results Queries using or with a flipped exists could return wrong rows. (#6636) Joins could leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery weren't synced, and changes to them didn't propagate. (913c0665f) Catching up a client after a large update could fail with Missing row, and a schema change during rehydration could fail with table has no column named. (#6662, #6616) A SQLite plan that nests one multi-index OR inside another could crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache called ZERO_QUERY_URL again for unchanged custom queries whenever it rebuilt its query pipelines. A permission change still re-transforms every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments could share one view, and a SolidJS view rebuilt after its query changed kept the old TTL. (#6461, #6497) In mutators, a row inserted without its optional columns didn't read those columns back as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) could return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients logged ChunkNotFoundError during persist. A write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database didn't delete the right file. (#6656) (thanks @pawarren!) Clients could go offline after the app resumed from the background or the computer woke from sleep, and React Native connect timeouts left the client stuck in an error state instead of retrying. (#6510, #6485) (thanks @pawarren!) Slow local startup could make a healthy server look unreachable. (#6475) A mutator that threw while being replayed disconnected the client in a loop. The client now stays connected, and the mutation is still sent to the server. (#6555) Client memory grew with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil didn't suspend until the first rows arrived, contrary to the docs (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing were logged as errors. They're now warnings or info, and fatal errors include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability zero-cache leaked memory and other resources around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) View-syncers could crash or return Internal error when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg crashed when Postgres closed a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector could run a view-syncer out of memory on large tables. (#6596) Error messages could include row values, query literals and mutation arguments. (#6413) Replication, Backfills, and Backups Replication could roll back or stop after an interrupted stream, and a replication-manager whose slot was invalidated crash-looped instead of resyncing. (#6562, #6590) Backfills could apply rows before replication had caught up to them, didn't resume when a view-syncer moved to another replication-manager, and leaked a Postgres connection that held back vacuum. (#6685, #6684, #6513) Replication slots are more robust. Slot creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag was measured wrong on Aurora. (e6a2590db) After initial sync, the replication-manager could serve before a backup covered its replica, so restoring view-syncers fell back to Postgres. It now waits for that backup. (#6465) Query pipelines were reset more often than needed, and dropping an upstream index didn't refresh table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers now fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager now keeps serving until the new one takes over. (21f349135) Change-database connections leaked after a failed catch-up, and a change-database reset could hang. (#6529, #6632) Litestream could run out of memory in small containers. Its upload concurrency now scales with available memory. (240c05730) Configuration values that start with a dash, such as some shard IDs, were parsed wrong. (81e6ac570) In the zero Docker image, initial sync and backfills could leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe) Breaking Changes Legacy queries: zero-cache now rejects queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: if your code switches over every result type or connection state, handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options; they no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours (was 48), replication lag is reported every 10 seconds (was 30), and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 (was 2). Startup can take longer while zero-cache waits for a backup and for replication to catch up, so give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates, or tx.retryOn() won't work. Using tx after its transaction has ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL don't apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they hadn't sent yet are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Unsent mutations are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after upgrading, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE. Use ZERO_CHANGE_STREAMER_URI instead. (698286b55) Events: there's a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", + "content": "Installation npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Overview Zero 1.11 can show cached query results right away, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication. Features Queries can now start with results that a previous session saved on the device. These come back with the new 'cached' result type, so you can show them right away and still tell them apart from data the server has confirmed. (#6578) (thanks @pawarren!) Building the same query again usually gives you the same Query object. useQuery no longer rebuilds and rehashes its query on every render, and queries built inline work as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero starts in a new 'initializing' connection state while it loads local data, and slow loading doesn't count against the connect timeout anymore. (#6561) If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore tells you which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Clients upgrading from 1.10 keep their local data and pending mutations. Future protocol changes won't reset clients to an empty local database either. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when tx.retryOn() accepts the error, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. A replica restored from another replica's backup continues that backup incrementally. Periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits for replication to catch up before it reports ready. (1c5ddcdb4) Inactive client groups are kept for 7 days instead of 48 hours, so users who come back within a week don't need a full resync. (#6484) zero-cache now logs slow queries with the reason and suggested indexes, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) A new hydration budget caps the time a view-syncer spends re-hydrating a reconnecting client's inactive queries, and a per-query hydration timeout aborts any query that takes too long to hydrate. Both are off by default. (#6477, #6499) New OpenTelemetry metrics track WAL usage. Replication status events now include index-build progress, and a new view-syncer status event reports restore progress. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image is down to 807 MB from about 1.09 GB. (#6695) Performance Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x less CPU. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.7x faster on React Native. That's the geometric mean of 53 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead. Fixes Query Results Queries using or with a flipped exists could return wrong rows. (#6636) Joins could leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery weren't synced, and changes to them didn't propagate. (913c0665f) Catching up a client after a large update could fail with Missing row, and a schema change during rehydration could fail with table has no column named. (#6662, #6616) A SQLite plan that nests one multi-index OR inside another could crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache called ZERO_QUERY_URL again for unchanged custom queries whenever it rebuilt its query pipelines. A permission change still re-transforms every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments could share one view, and a SolidJS view rebuilt after its query changed kept the old TTL. (#6461, #6497) In mutators, a row inserted without its optional columns didn't read those columns back as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) could return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients logged ChunkNotFoundError during persist. A write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database didn't delete the right file. (#6656) (thanks @pawarren!) Clients could go offline after the app resumed from the background or the computer woke from sleep, and React Native connect timeouts left the client stuck in an error state instead of retrying. (#6510, #6485) (thanks @pawarren!) Slow local startup could make a healthy server look unreachable. (#6475) A mutator that threw while being replayed disconnected the client in a loop. The client now stays connected, and the mutation is still sent to the server. (#6555) Client memory grew with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil didn't suspend until the first rows arrived, contrary to the docs (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing were logged as errors. They're now warnings or info, and fatal errors include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability zero-cache leaked memory and other resources around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) View-syncers could crash or return Internal error when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg crashed when Postgres closed a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector could run a view-syncer out of memory on large tables. (#6596) Error messages could include row values, query literals and mutation arguments. (#6413) Replication, Backfills, and Backups Replication could roll back or stop after an interrupted stream, and a replication-manager whose slot was invalidated crash-looped instead of resyncing. (#6562, #6590) Backfills could apply rows before replication had caught up to them, didn't resume when a view-syncer moved to another replication-manager, and leaked a Postgres connection that held back vacuum. (#6685, #6684, #6513) Replication slots are more robust. Slot creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag was measured wrong on Aurora. (e6a2590db) After initial sync, the replication-manager could serve before a backup covered its replica, so restoring view-syncers fell back to Postgres. It now waits for that backup. (#6465) Query pipelines were reset more often than needed, and dropping an upstream index didn't refresh table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers now fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager now keeps serving until the new one takes over. (21f349135) Change-database connections leaked after a failed catch-up, and a change-database reset could hang. (#6529, #6632) Litestream could run out of memory in small containers. Its upload concurrency now scales with available memory. (240c05730) Configuration values that start with a dash, such as some shard IDs, were parsed wrong. (81e6ac570) In the zero Docker image, initial sync and backfills could leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe) Breaking Changes Legacy queries: zero-cache now rejects queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: if your code switches over every result type or connection state, handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options; they no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep writing legacy WAL backups with Litestream v0.3, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours (was 48), replication lag is reported every 10 seconds (was 30), and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 (was 2). Startup can take longer while zero-cache waits for a backup and for replication to catch up, so give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates, or tx.retryOn() won't work. Using tx after its transaction has ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL don't apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they hadn't sent yet are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Unsent mutations are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after upgrading, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE. Use ZERO_CHANGE_STREAMER_URI instead. (698286b55) Events: there's a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", "headings": [ { "text": "Installation", @@ -5793,7 +5793,7 @@ "sectionTitle": "Performance", "sectionId": "performance", "url": "/docs/release-notes/1.11", - "content": "Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x less CPU. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.6x faster on React Native. That's the geometric mean of 56 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead.", + "content": "Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x less CPU. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.7x faster on React Native. That's the geometric mean of 53 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead.", "kind": "section" }, { @@ -5823,7 +5823,7 @@ "sectionTitle": "React Native", "sectionId": "react-native", "url": "/docs/release-notes/1.11", - "content": "Zero's query engine is about 1.6x faster on React Native. That's the geometric mean of 56 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster.", + "content": "Zero's query engine is about 1.7x faster on React Native. That's the geometric mean of 53 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster.", "kind": "section" }, { @@ -5913,7 +5913,7 @@ "sectionTitle": "Breaking Changes", "sectionId": "breaking-changes", "url": "/docs/release-notes/1.11", - "content": "Legacy queries: zero-cache now rejects queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: if your code switches over every result type or connection state, handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options; they no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep v4 backups, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours (was 48), replication lag is reported every 10 seconds (was 30), and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 (was 2). Startup can take longer while zero-cache waits for a backup and for replication to catch up, so give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates, or tx.retryOn() won't work. Using tx after its transaction has ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL don't apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they hadn't sent yet are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Unsent mutations are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after upgrading, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE. Use ZERO_CHANGE_STREAMER_URI instead. (698286b55) Events: there's a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", + "content": "Legacy queries: zero-cache now rejects queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: if your code switches over every result type or connection state, handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options; they no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep writing legacy WAL backups with Litestream v0.3, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours (was 48), replication lag is reported every 10 seconds (was 30), and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 (was 2). Startup can take longer while zero-cache waits for a backup and for replication to catch up, so give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates, or tx.retryOn() won't work. Using tx after its transaction has ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL don't apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they hadn't sent yet are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Unsent mutations are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after upgrading, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE. Use ZERO_CHANGE_STREAMER_URI instead. (698286b55) Events: there's a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", "kind": "section" }, { diff --git a/contents/docs/release-notes/1.11.mdx b/contents/docs/release-notes/1.11.mdx index b5b0593e..a367c201 100644 --- a/contents/docs/release-notes/1.11.mdx +++ b/contents/docs/release-notes/1.11.mdx @@ -99,7 +99,7 @@ When a transaction replicates, `zero-cache` has to bring every client group's qu ### React Native -Zero's query engine is about **1.6x faster** on React Native. That's the geometric mean of 56 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. +Zero's query engine is about **1.7x faster** on React Native. That's the geometric mean of 53 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Date: Thu, 15 Oct 2026 09:25:34 +0000 Subject: [PATCH 16/20] docs(connection): replace the lifecycle diagram with an SVG that starts in initializing Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RRckJnCeGsYg9nCJSxBgTJ --- public/images/connection-state/lifecycle.png | Bin 307412 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 public/images/connection-state/lifecycle.png diff --git a/public/images/connection-state/lifecycle.png b/public/images/connection-state/lifecycle.png deleted file mode 100644 index 1d91ed28a25dbb57c07ed01372d4792ef7a9a6ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 307412 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%0A#ZiU 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�Ve(*m8=nwIM0MG!wwIdfG-Q46;F6msT|S@J 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

Fl3UP-?+*C~<)kh~!?7FEeMI!D` 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 From 30e590b136f4298a896cecd47fad8eb840e1f160 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Thu, 15 Oct 2026 09:26:10 +0000 Subject: [PATCH 17/20] docs(connection): add the new lifecycle SVG The previous commit removed lifecycle.png without adding its replacement. Add lifecycle.svg and point the connection docs at it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RRckJnCeGsYg9nCJSxBgTJ --- contents/docs/connection.mdx | 2 +- public/images/connection-state/lifecycle.svg | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 public/images/connection-state/lifecycle.svg diff --git a/contents/docs/connection.mdx b/contents/docs/connection.mdx index 32ab5f85..c0a121ef 100644 --- a/contents/docs/connection.mdx +++ b/contents/docs/connection.mdx @@ -7,7 +7,7 @@ title: Connection Status Zero manages a persistent connection to `zero-cache` with the following lifecycle: diff --git a/public/images/connection-state/lifecycle.svg b/public/images/connection-state/lifecycle.svg new file mode 100644 index 00000000..e3dad9a1 --- /dev/null +++ b/public/images/connection-state/lifecycle.svg @@ -0,0 +1,5 @@ +

Connecting

Connected

disconnect

success

Disconnected

reconnect

timeout

Error

connect

error

Needs-Auth

connect

needs-auth

Closed

close()

called

Initializing

initialized

instantiation

\ No newline at end of file From badca05f6c7f4ecf93eeeb5b2eadf6a9883c6bed Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Thu, 15 Oct 2026 09:37:29 +0000 Subject: [PATCH 18/20] docs(connection): show hidden-tab transitions in the lifecycle diagram The diagram now has Connected to Disconnected when the tab is hidden and Disconnected to Connecting when it is visible again. The Disconnected section says Zero waits for the tab to be visible instead of retrying every 5 seconds after a hidden-tab disconnect. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RRckJnCeGsYg9nCJSxBgTJ --- assets/search-index.json | 6 +++--- contents/docs/connection.mdx | 2 +- public/images/connection-state/lifecycle.svg | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/search-index.json b/assets/search-index.json index 3b5ee5af..bbcdbfc8 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -781,7 +781,7 @@ "title": "Connection Status", "searchTitle": "Connection Status", "url": "/docs/connection", - "content": "Overview Zero manages a persistent connection to zero-cache with the following lifecycle: Before any of this, Zero is initializing while it loads local data. After that it moves to connecting and never goes back. Usage The current connection state is available in the zero.connection.state property. This is subscribable and also has reactive hooks for React and SolidJS: import {useConnectionState} from '@rocicorp/zero/react' function ConnectionStatus() { const state = useConnectionState() switch (state.name) { case 'initializing': return

Loading...
case 'connecting': return
Connecting...
case 'connected': return
Connected
case 'disconnected': return
Offline
case 'error': return
Error
case 'needs-auth': return
Session expired
default: return null } }import {useConnectionState} from '@rocicorp/zero/solid' function ConnectionStatus() { const state = useConnectionState() return (
Loading...
Connecting...
Connected
Offline
Error
Session expired
) }zero.connection.state.subscribe(state => { switch (state.name) { case 'initializing': console.log('Loading local data...') break case 'connecting': console.log(`Connecting... ${state.reason}`) break case 'connected': console.log('Connected') break case 'disconnected': console.log(`Disconnected ${state.reason}`) break case 'error': console.log(`Error ${state.reason}`) break case 'needs-auth': console.log('Session expired') break default: return null } }) Offline Zero does not support offline writes. When the client is in the disconnected, error, or needs-auth states, reads from synced data continue to work, but writes are rejected. Offline UI While Zero is in the disconnected, error, or needs-auth states, you should prevent the user from inputting data to your application to avoid data loss. Zero automates this as best it can by rejecting writes in these states. But there can still be cases where the user can lose work – for example by typing into a textarea that is only written to Zero when the user presses a button. The easiest way to implement this is with a modal overlay that covers the entire screen and tells the user to reconnect. However, you could also continue to let the user use the app read-only, and only disable inputs. Details Initializing Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take many seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state. Storage Failures Zero stores synced data on the device, in IndexedDB in the browser or SQLite on React Native. Sometimes that fails: the device runs out of space, or the database won't open. When it does, Zero calls onStorageFailure once with a StorageFailureError. Its kind says what went wrong: In the browser, Zero reports a storage failure only when IndexedDB fails to open: full if the browser says the disk is full, otherwise cannot-open. Zero keeps working either way. If storage fails while Zero is starting, Zero runs in memory for the rest of the session, as if you had set kvStore: 'mem', and nothing is saved on the device. If it fails later, Zero stops saving to the device. Queries and mutations keep working on the data in memory and mutations are still sent to zero-cache, but a read that needs data Zero hasn't loaded into memory can still fail. Zero only tries the device's storage again when the app creates a new Zero instance, for example after a restart. By default, Zero only logs the failure. Use onStorageFailure to tell the user what to do: import type {ZeroOptions} from '@rocicorp/zero' const opts: ZeroOptions = { // ... onStorageFailure: failure => { // showBanner stands in for your app's own UI. if (failure.kind === 'full') { showBanner( 'Your device is out of space. Free up some space, then restart the app.' ) } else { showBanner( 'Data could not be saved on this device. Restart the app to try again.' ) } } } StorageFailureError is exported from @rocicorp/zero. Its cause is the original error from the browser or the SQLite driver. A custom kvStore can throw a StorageFailureError to get the same handling. Checking the Store zero.kvStore.kind tells you which storage Zero is using: 'idb', 'mem', 'expo-sqlite', or 'op-sqlite'. A custom store reports its own kind, if it sets one. if (zero.kvStore.kind === 'mem') { // Data is not saved on this device. } Without IndexedDB, kind is 'mem' from the start and onStorageFailure isn't called. When IndexedDB fails to open, kind changes to 'mem' only when Zero first reads or writes the store, so check kind when you need it, not once at startup. Why Zero Doesn't Support Offline Writes Supporting offline writes in collaborative applications is inherently difficult, and no sync engine or CRDT algorithm can automatically solve it for you. Despite what their marketing says 😉. Example Imagine two users are editing an article about cats. One goes offline and does a bunch of work on the article, while the other decides that the article should actually be about dogs and rewrites it. When the offline user reconnects, there is no way that any software algorithm can automatically resolve their conflict. One or the other of them is going to be upset. This is a trivial data model with a single field, and is already unsolvable. Real-world applications are much worse: Foreign keys and other constraints can pass while offline, but break when the user reconnects. Custom business logic and authorization rules can pass while offline, but break when the user reconnects. The application's schema can change while offline, and the user's data may not be processable by the new schema. Just take your own schema and ask yourself what should really happen if one user takes their device offline for a week and makes arbitrarily complex changes while other users are working online. Tradeoffs It is of course possible to create applications that support offline writes well (Git exists!). But it requires significant tradeoffs. For example, you could: Disallow destructive operations (i.e., users can create tasks while offline, but cannot edit or delete them). Support custom UX to allow users to fork and merge conflicts when they occur. Restrict offline writes to a single device. Accept potential user data loss. Zero's Position While we recognize that offline writes would be useful, the reality is that for most of the apps we want to support, the user is online the vast majority of the time and the cost to support offline is extremely high. There is simply more value in making the online experience great first, and that's where we're focused right now. We would like to revisit this in the future, but it's not a priority right now.", + "content": "Overview Zero manages a persistent connection to zero-cache with the following lifecycle: Before any of this, Zero is initializing while it loads local data. After that it moves to connecting and never goes back. Usage The current connection state is available in the zero.connection.state property. This is subscribable and also has reactive hooks for React and SolidJS: import {useConnectionState} from '@rocicorp/zero/react' function ConnectionStatus() { const state = useConnectionState() switch (state.name) { case 'initializing': return
Loading...
case 'connecting': return
Connecting...
case 'connected': return
Connected
case 'disconnected': return
Offline
case 'error': return
Error
case 'needs-auth': return
Session expired
default: return null } }import {useConnectionState} from '@rocicorp/zero/solid' function ConnectionStatus() { const state = useConnectionState() return (
Loading...
Connecting...
Connected
Offline
Error
Session expired
) }zero.connection.state.subscribe(state => { switch (state.name) { case 'initializing': console.log('Loading local data...') break case 'connecting': console.log(`Connecting... ${state.reason}`) break case 'connected': console.log('Connected') break case 'disconnected': console.log(`Disconnected ${state.reason}`) break case 'error': console.log(`Error ${state.reason}`) break case 'needs-auth': console.log('Session expired') break default: return null } }) Offline Zero does not support offline writes. When the client is in the disconnected, error, or needs-auth states, reads from synced data continue to work, but writes are rejected. Offline UI While Zero is in the disconnected, error, or needs-auth states, you should prevent the user from inputting data to your application to avoid data loss. Zero automates this as best it can by rejecting writes in these states. But there can still be cases where the user can lose work – for example by typing into a textarea that is only written to Zero when the user presses a button. The easiest way to implement this is with a modal overlay that covers the entire screen and tells the user to reconnect. However, you could also continue to let the user use the app read-only, and only disable inputs. Details Initializing Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take many seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. After a hidden-tab disconnect, Zero instead waits until the tab is visible again, then goes back to connecting. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state. Storage Failures Zero stores synced data on the device, in IndexedDB in the browser or SQLite on React Native. Sometimes that fails: the device runs out of space, or the database won't open. When it does, Zero calls onStorageFailure once with a StorageFailureError. Its kind says what went wrong: In the browser, Zero reports a storage failure only when IndexedDB fails to open: full if the browser says the disk is full, otherwise cannot-open. Zero keeps working either way. If storage fails while Zero is starting, Zero runs in memory for the rest of the session, as if you had set kvStore: 'mem', and nothing is saved on the device. If it fails later, Zero stops saving to the device. Queries and mutations keep working on the data in memory and mutations are still sent to zero-cache, but a read that needs data Zero hasn't loaded into memory can still fail. Zero only tries the device's storage again when the app creates a new Zero instance, for example after a restart. By default, Zero only logs the failure. Use onStorageFailure to tell the user what to do: import type {ZeroOptions} from '@rocicorp/zero' const opts: ZeroOptions = { // ... onStorageFailure: failure => { // showBanner stands in for your app's own UI. if (failure.kind === 'full') { showBanner( 'Your device is out of space. Free up some space, then restart the app.' ) } else { showBanner( 'Data could not be saved on this device. Restart the app to try again.' ) } } } StorageFailureError is exported from @rocicorp/zero. Its cause is the original error from the browser or the SQLite driver. A custom kvStore can throw a StorageFailureError to get the same handling. Checking the Store zero.kvStore.kind tells you which storage Zero is using: 'idb', 'mem', 'expo-sqlite', or 'op-sqlite'. A custom store reports its own kind, if it sets one. if (zero.kvStore.kind === 'mem') { // Data is not saved on this device. } Without IndexedDB, kind is 'mem' from the start and onStorageFailure isn't called. When IndexedDB fails to open, kind changes to 'mem' only when Zero first reads or writes the store, so check kind when you need it, not once at startup. Why Zero Doesn't Support Offline Writes Supporting offline writes in collaborative applications is inherently difficult, and no sync engine or CRDT algorithm can automatically solve it for you. Despite what their marketing says 😉. Example Imagine two users are editing an article about cats. One goes offline and does a bunch of work on the article, while the other decides that the article should actually be about dogs and rewrites it. When the offline user reconnects, there is no way that any software algorithm can automatically resolve their conflict. One or the other of them is going to be upset. This is a trivial data model with a single field, and is already unsolvable. Real-world applications are much worse: Foreign keys and other constraints can pass while offline, but break when the user reconnects. Custom business logic and authorization rules can pass while offline, but break when the user reconnects. The application's schema can change while offline, and the user's data may not be processable by the new schema. Just take your own schema and ask yourself what should really happen if one user takes their device offline for a week and makes arbitrarily complex changes while other users are working online. Tradeoffs It is of course possible to create applications that support offline writes well (Git exists!). But it requires significant tradeoffs. For example, you could: Disallow destructive operations (i.e., users can create tasks while offline, but cannot edit or delete them). Support custom UX to allow users to fork and merge conflicts when they occur. Restrict offline writes to a single device. Accept potential user data loss. Zero's Position While we recognize that offline writes would be useful, the reality is that for most of the apps we want to support, the user is online the vast majority of the time and the cost to support offline is extremely high. There is simply more value in making the online experience great first, and that's where we're focused right now. We would like to revisit this in the future, but it's not a priority right now.", "headings": [ { "text": "Overview", @@ -905,7 +905,7 @@ "sectionTitle": "Details", "sectionId": "details", "url": "/docs/connection", - "content": "Initializing Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take many seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state.", + "content": "Initializing Zero starts in the initializing state, where it loads its local data. It hasn't contacted zero-cache yet, so the network plays no part. How long this takes depends on the device and on how much data is stored locally. On a slow device with a lot of data, it can take many seconds. When the data is loaded, Zero moves to connecting. This only happens once, right after Zero is created. Later reconnects go straight to connecting. While initializing, show a loading indicator rather than an offline or error message. If your app warns the user when Zero takes too long to connect, start that timer when Zero enters connecting. There is no timeout while initializing, and the time doesn't count against disconnectTimeoutMs. If the local data never finishes loading, Zero stays in initializing, so add your own timeout if you need one. Reads and writes are allowed. Queries return results once the local data is loaded, and writes are queued until Zero connects. Connecting Zero enters the connecting state after initializing. While connecting, Zero repeatedly tries to connect to zero-cache. After 1 minute of failed attempts, it transitions to disconnected. Time spent initializing does not count toward this minute. This timeout can be configured with the disconnectTimeoutMs constructor parameter: const opts: ZeroOptions = { // ... disconnectTimeoutMs: 1000 * 60 * 10 // 10 minutes } Reads and writes are allowed to Zero mutators while connecting. The writes are queued and are sent when the connection succeeds. If the connection fails, the writes remain queued and are sent the next time Zero connects. This is intended to paper over short connectivity glitches, such as server restarts, walking into an elevator, etc. While you can increase the disconnectTimeoutMs to allow for longer periods of offline operation, this has caveats and is not recommended. Please see offline for more information. Connected Once Zero connects to zero-cache and syncs the first time, it transitions to the connected state. Disconnected After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. After a hidden-tab disconnect, Zero instead waits until the tab is visible again, then goes back to connecting. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information. Error If zero-cache crashes, or mutate or query endpoints fail, Zero enters the error state. If the response code is 5xx, zero-cache will retry up to four times. Zero does not retry from the error state. Call zero.connection.connect() to retry manually. Reads are allowed while in the error state, but writes are rejected. You can forward connection errors to Sentry (or any error-monitoring tool) by subscribing to zero.connection.state. You can wrap reason in an Error and report it: import * as Sentry from '@sentry/browser' zero.connection.state.subscribe(state => { if (state.name !== 'error') return Sentry.withScope(scope => { scope.setTag('zero.connection.state', state.name) scope.setExtra('zero.connection.reason', state.reason) Sentry.captureException( new Error(`Zero connection error: ${state.reason}`) ) }) }) Needs-Auth If the mutate or query endpoints return a 401 or 403 status code, Zero transitions to the needs-auth state. For cookie auth, refresh the cookie and call zero.connection.connect(). For token auth, fetch a new token and call zero.connection.connect({auth: newToken}) to refresh the token in place without recreating the client. If you are using ZeroProvider, it will do this for you when the auth value changes from one token to another. Reads are allowed while in the needs-auth state, but writes are rejected. See Authentication for more information. Closed Zero transitions to the closed state when you call zero.close(). Most applications will never call close(), and even if they do, they should not still be using Zero at that time. So in practice, you should never see this state in a running application. Reads and writes are both rejected while Zero is in the closed state.", "kind": "section" }, { @@ -945,7 +945,7 @@ "sectionTitle": "Disconnected", "sectionId": "disconnected", "url": "/docs/connection", - "content": "After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information.", + "content": "After the disconnectTimeoutMs elapses while in the connecting state, Zero transitions to disconnected. Zero also transitions to disconnected when the tab is hidden for hiddenTabDisconnectDelay (default 5 minutes). While disconnected, Zero continues to try to reconnect to zero-cache every 5 seconds. After a hidden-tab disconnect, Zero instead waits until the tab is visible again, then goes back to connecting. Reads are allowed while disconnected, but writes are rejected and return an offline error. See Offline for more information.", "kind": "section" }, { diff --git a/contents/docs/connection.mdx b/contents/docs/connection.mdx index c0a121ef..dbb0f0c2 100644 --- a/contents/docs/connection.mdx +++ b/contents/docs/connection.mdx @@ -201,7 +201,7 @@ Once Zero connects to `zero-cache` and syncs the first time, it transitions to t After the `disconnectTimeoutMs` elapses while in the `connecting` state, Zero transitions to `disconnected`. Zero also transitions to `disconnected` when the tab is hidden for `hiddenTabDisconnectDelay` (default 5 minutes). -While `disconnected`, Zero continues to try to reconnect to `zero-cache` every 5 seconds. +While `disconnected`, Zero continues to try to reconnect to `zero-cache` every 5 seconds. After a hidden-tab disconnect, Zero instead waits until the tab is visible again, then goes back to `connecting`. Reads are allowed while `disconnected`, but writes are rejected and return an offline error. See [Offline](#offline) for more information. diff --git a/public/images/connection-state/lifecycle.svg b/public/images/connection-state/lifecycle.svg index e3dad9a1..8f91cbfe 100644 --- a/public/images/connection-state/lifecycle.svg +++ b/public/images/connection-state/lifecycle.svg @@ -2,4 +2,4 @@ font-family: "tldraw_draw"; font-weight: normal; src: url("data:font/woff2;base64,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") format(woff2); -}

Connecting

Connected

disconnect

success

Disconnected

reconnect

timeout

Error

connect

error

Needs-Auth

connect

needs-auth

Closed

close()

called

Initializing

initialized

instantiation

\ No newline at end of file +}

Connecting

Connected

disconnect

success

Disconnected

timeout

tab visible

tab hidden

reconnect

Error

connect

error

Needs-Auth

connect

needs-auth

Closed

close()

called

Initializing

initialized

instantiation

\ No newline at end of file From 8f8e387adbf854df26127b616e5e8bedd47325cb Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Thu, 15 Oct 2026 12:33:07 +0000 Subject: [PATCH 19/20] docs: point the API Reference link at 1.11.0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RRckJnCeGsYg9nCJSxBgTJ --- lib/routes-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routes-config.ts b/lib/routes-config.ts index 49dc1311..781acfa0 100644 --- a/lib/routes-config.ts +++ b/lib/routes-config.ts @@ -48,7 +48,7 @@ export const ROUTES = [ {title: 'REST APIs', href: '/rest'}, { title: 'API Reference', - href: 'https://npmx.dev/package-docs/@rocicorp/zero/v/1.10.0#class-Zero', + href: 'https://npmx.dev/package-docs/@rocicorp/zero/v/1.11.0#class-Zero', }, ], }, From f79d2fc6ba4ee2bd1aa49161515fb6bbae9badd1 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Fri, 16 Oct 2026 10:02:55 +0000 Subject: [PATCH 20/20] docs(release-notes): describe 'cached' as knowing a result is server-confirmed Local rows were already shown right away as 'unknown'. What 1.11 adds is knowing that they are a result the server confirmed in an earlier session. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RRckJnCeGsYg9nCJSxBgTJ --- assets/search-index.json | 6 +++--- contents/docs/release-notes/1.11.mdx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/search-index.json b/assets/search-index.json index bbcdbfc8..d8d20699 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -5687,7 +5687,7 @@ "title": "Zero 1.11", "searchTitle": "Zero 1.11", "url": "/docs/release-notes/1.11", - "content": "Installation npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Overview Zero 1.11 can show cached query results right away, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication. Features Queries can now start with results that a previous session saved on the device. These come back with the new 'cached' result type, so you can show them right away and still tell them apart from data the server has confirmed. (#6578) (thanks @pawarren!) Building the same query again usually gives you the same Query object. useQuery no longer rebuilds and rehashes its query on every render, and queries built inline work as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero starts in a new 'initializing' connection state while it loads local data, and slow loading doesn't count against the connect timeout anymore. (#6561) If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore tells you which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Clients upgrading from 1.10 keep their local data and pending mutations. Future protocol changes won't reset clients to an empty local database either. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when tx.retryOn() accepts the error, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. A replica restored from another replica's backup continues that backup incrementally. Periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits for replication to catch up before it reports ready. (1c5ddcdb4) Inactive client groups are kept for 7 days instead of 48 hours, so users who come back within a week don't need a full resync. (#6484) zero-cache now logs slow queries with the reason and suggested indexes, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) A new hydration budget caps the time a view-syncer spends re-hydrating a reconnecting client's inactive queries, and a per-query hydration timeout aborts any query that takes too long to hydrate. Both are off by default. (#6477, #6499) New OpenTelemetry metrics track WAL usage. Replication status events now include index-build progress, and a new view-syncer status event reports restore progress. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image is down to 807 MB from about 1.09 GB. (#6695) Performance Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x less CPU. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.7x faster on React Native. That's the geometric mean of 53 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead. Fixes Query Results Queries using or with a flipped exists could return wrong rows. (#6636) Joins could leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery weren't synced, and changes to them didn't propagate. (913c0665f) Catching up a client after a large update could fail with Missing row, and a schema change during rehydration could fail with table has no column named. (#6662, #6616) A SQLite plan that nests one multi-index OR inside another could crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache called ZERO_QUERY_URL again for unchanged custom queries whenever it rebuilt its query pipelines. A permission change still re-transforms every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments could share one view, and a SolidJS view rebuilt after its query changed kept the old TTL. (#6461, #6497) In mutators, a row inserted without its optional columns didn't read those columns back as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) could return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients logged ChunkNotFoundError during persist. A write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database didn't delete the right file. (#6656) (thanks @pawarren!) Clients could go offline after the app resumed from the background or the computer woke from sleep, and React Native connect timeouts left the client stuck in an error state instead of retrying. (#6510, #6485) (thanks @pawarren!) Slow local startup could make a healthy server look unreachable. (#6475) A mutator that threw while being replayed disconnected the client in a loop. The client now stays connected, and the mutation is still sent to the server. (#6555) Client memory grew with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil didn't suspend until the first rows arrived, contrary to the docs (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing were logged as errors. They're now warnings or info, and fatal errors include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability zero-cache leaked memory and other resources around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) View-syncers could crash or return Internal error when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg crashed when Postgres closed a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector could run a view-syncer out of memory on large tables. (#6596) Error messages could include row values, query literals and mutation arguments. (#6413) Replication, Backfills, and Backups Replication could roll back or stop after an interrupted stream, and a replication-manager whose slot was invalidated crash-looped instead of resyncing. (#6562, #6590) Backfills could apply rows before replication had caught up to them, didn't resume when a view-syncer moved to another replication-manager, and leaked a Postgres connection that held back vacuum. (#6685, #6684, #6513) Replication slots are more robust. Slot creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag was measured wrong on Aurora. (e6a2590db) After initial sync, the replication-manager could serve before a backup covered its replica, so restoring view-syncers fell back to Postgres. It now waits for that backup. (#6465) Query pipelines were reset more often than needed, and dropping an upstream index didn't refresh table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers now fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager now keeps serving until the new one takes over. (21f349135) Change-database connections leaked after a failed catch-up, and a change-database reset could hang. (#6529, #6632) Litestream could run out of memory in small containers. Its upload concurrency now scales with available memory. (240c05730) Configuration values that start with a dash, such as some shard IDs, were parsed wrong. (81e6ac570) In the zero Docker image, initial sync and backfills could leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe) Breaking Changes Legacy queries: zero-cache now rejects queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: if your code switches over every result type or connection state, handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options; they no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep writing legacy WAL backups with Litestream v0.3, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours (was 48), replication lag is reported every 10 seconds (was 30), and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 (was 2). Startup can take longer while zero-cache waits for a backup and for replication to catch up, so give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates, or tx.retryOn() won't work. Using tx after its transaction has ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL don't apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they hadn't sent yet are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Unsent mutations are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after upgrading, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE. Use ZERO_CHANGE_STREAMER_URI instead. (698286b55) Events: there's a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", + "content": "Installation npm install @rocicorp/zero@1.11 Deploy zero-cache before updating clients. Zero 1.11 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Overview Zero 1.11 tells you when local query results were already confirmed by the server, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication. Features Queries now tell you when their local data is a result the server confirmed in an earlier session: they start with the new 'cached' result type instead of 'unknown'. That's enough to show stored data or an empty state without waiting for the server, even offline. (#6578) (thanks @pawarren!) Building the same query again usually gives you the same Query object. useQuery no longer rebuilds and rehashes its query on every render, and queries built inline work as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero starts in a new 'initializing' connection state while it loads local data, and slow loading doesn't count against the connect timeout anymore. (#6561) If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore tells you which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Clients upgrading from 1.10 keep their local data and pending mutations. Future protocol changes won't reset clients to an empty local database either. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when tx.retryOn() accepts the error, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. A replica restored from another replica's backup continues that backup incrementally. Periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits for replication to catch up before it reports ready. (1c5ddcdb4) Inactive client groups are kept for 7 days instead of 48 hours, so users who come back within a week don't need a full resync. (#6484) zero-cache now logs slow queries with the reason and suggested indexes, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) A new hydration budget caps the time a view-syncer spends re-hydrating a reconnecting client's inactive queries, and a per-query hydration timeout aborts any query that takes too long to hydrate. Both are off by default. (#6477, #6499) New OpenTelemetry metrics track WAL usage. Replication status events now include index-build progress, and a new view-syncer status event reports restore progress. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image is down to 807 MB from about 1.09 GB. (#6695) Performance Client Cold Start Apps usually register queries before Zero has finished loading local data, for example when useQuery runs at startup. Zero 1.10 then pushed every loaded row through every registered query. Zero 1.11 waits for the data to load and hydrates each query once (#6472). With 13 queries and about 30,000 local rows, startup is about 7x faster, and about 8x faster on React Native. Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.11 does this with about 2.5x less CPU. We measured 100 client groups on one sync worker, each syncing a query with a related subquery. React Native Zero's query engine is about 1.7x faster on React Native. That's the geometric mean of 53 query engine benchmarks on Hermes, and 48 of them got more than 5% faster. Hydrating queries is 1.6x to 4.8x faster, and applying changes 1.2x to 2.2x faster. Building and Hashing Queries Components rebuild their queries on every render, and Zero hashes each query to identify it. Building the same query again now usually returns the same object, with its hash already computed (#6469). That makes building and hashing a query about 7x to 17x faster, depending on its size. Client Writes With Many Joined Queries Got Slower This is the one benchmark that got slower. With 300 open queries, each joining the same four tables, applying one write in the client's query engine takes about 1.2x longer. Joins now keep an index of their parent rows so they can skip changes no parent can see (#6645, #6708). In this benchmark every change reaches some parent, so keeping the index up to date is pure overhead. Fixes Query Results Queries using or with a flipped exists could return wrong rows. (#6636) Joins could leave a deleted row on clients, keep a stale row in a limit() query, or fail with node does not exist. (#6660) Rows behind an exists inside a scalar subquery weren't synced, and changes to them didn't propagate. (913c0665f) Catching up a client after a large update could fail with Missing row, and a schema change during rehydration could fail with table has no column named. (#6662, #6616) A SQLite plan that nests one multi-index OR inside another could crash zero-cache in a loop. (6ea17bfcd) (thanks @robertoborgone!) zero-cache called ZERO_QUERY_URL again for unchanged custom queries whenever it rebuilt its query pipelines. A permission change still re-transforms every query. (#6601) In React and SolidJS, two custom queries with the same query but different names or arguments could share one view, and a SolidJS view rebuilt after its query changed kept the old TTL. (#6461, #6497) In mutators, a row inserted without its optional columns didn't read those columns back as null. (d4816752c) Clients and React Native SQLite-backed stores (expo-sqlite, op-sqlite) could return corrupted data under concurrent reads, hang after a failed transaction, or keep part of a failed commit. (#6495, #6655, #6683) (thanks @pawarren!) Healthy clients logged ChunkNotFoundError during persist. A write that would corrupt the local store now fails instead. (#6670, #6559) (thanks @pawarren!) With op-sqlite and a custom location or encryptionKey, dropping the database didn't delete the right file. (#6656) (thanks @pawarren!) Clients could go offline after the app resumed from the background or the computer woke from sleep, and React Native connect timeouts left the client stuck in an error state instead of retrying. (#6510, #6485) (thanks @pawarren!) Slow local startup could make a healthy server look unreachable. (#6475) A mutator that threw while being replayed disconnected the client in a loop. The client now stays connected, and the mutation is still sent to the server. (#6555) Client memory grew with every query used in a session. (#6474) useSuspenseQuery with an options object and no suspendUntil didn't suspend until the first rows arrived, contrary to the docs (see Breaking Changes). (ffe67b23a) (thanks @charpeni!) Routine reconnects, auth errors and server rebalancing were logged as errors. They're now warnings or info, and fatal errors include their cause. (#6581, #6692, #6498, #6682, #6691, 42316d521, 075b4215e, 79adc09fb) (thanks @pawarren!) zero-cache Reliability zero-cache leaked memory and other resources around closed connections, stopped view-syncers, abandoned hydrations, and inspector and profiling requests. (#6500, #6514, #6515, #6516, #6525, #6526, #6527, #6528, #6540, #6541, #6542, #6698) View-syncers could crash or return Internal error when another view-syncer advanced the same client group, when a client connection was missing, or when a push arrived on a closing connection. (#6481, #6530, #6600, #6614, #6631, d545bc7e2) (thanks @jdnvn!) Servers using zeroNodePg crashed when Postgres closed a pooled connection. (1b220fa07) (thanks @pawarren!) Analyzing a query in the Inspector could run a view-syncer out of memory on large tables. (#6596) Error messages could include row values, query literals and mutation arguments. (#6413) Replication, Backfills, and Backups Replication could roll back or stop after an interrupted stream, and a replication-manager whose slot was invalidated crash-looped instead of resyncing. (#6562, #6590) Backfills could apply rows before replication had caught up to them, didn't resume when a view-syncer moved to another replication-manager, and leaked a Postgres connection that held back vacuum. (#6685, #6684, #6513) Replication slots are more robust. Slot creation retries while long transactions block it, slots stay active during long initial syncs, slots dropped outside Zero are handled, and inactive slots are cleaned up. (25a9ea1ac, f3bba7d73, 7a998b566, caf79b161, #6568) Replication lag was measured wrong on Aurora. (e6a2590db) After initial sync, the replication-manager could serve before a backup covered its replica, so restoring view-syncers fell back to Postgres. It now waits for that backup. (#6465) Query pipelines were reset more often than needed, and dropping an upstream index didn't refresh table schemas. (4341cdcbd, 89787f827, bb71cc931) View-syncers now fail fast when the replication-manager is unreachable, and a slow handshake is no longer mistaken for a missed heartbeat. (0af1a7109, #6598) During a resync, the old replication-manager now keeps serving until the new one takes over. (21f349135) Change-database connections leaked after a failed catch-up, and a change-database reset could hang. (#6529, #6632) Litestream could run out of memory in small containers. Its upload concurrency now scales with available memory. (240c05730) Configuration values that start with a dash, such as some shard IDs, were parsed wrong. (81e6ac570) In the zero Docker image, initial sync and backfills could leave a stalled Postgres connection open after copying a table. Other installs keep the 1.10 behavior. (3f139f52a, 22fa3c27e) Type Checking A rejected whereExists(..., {scalar: true}) now explains why, and a misspelled relationship name lists the valid ones. (cc020b368, 12a36dcfe) Breaking Changes Legacy queries: zero-cache now rejects queries sent directly by clients. If you still use legacy queries, set ZERO_ALLOW_LEGACY_QUERIES=true or move to custom queries. (#6399) New states: if your code switches over every result type or connection state, handle 'cached' and 'initializing'. Treat 'cached' like 'unknown': the data may be out of date. (#6578, #6561) Litestream v5 by default: remove the ZERO_LITESTREAM_VFS_* options; they no longer exist. Each replication-manager backs up to its own folder under the backup URL, so end the URL with / and add a lifecycle policy to your bucket. To keep writing legacy WAL backups with Litestream v0.3, set ZERO_LITESTREAM_BACKUP_USING_V5=false. If you set ZERO_LITESTREAM_EXECUTABLE_V5, use a Litestream build with -fork-to-url. (fb536f686, c1a586810) Changed defaults: inactive client groups are kept for 168 hours (was 48), replication lag is reported every 10 seconds (was 30), and ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION is 4 (was 2). Startup can take longer while zero-cache waits for a backup and for replication to catch up, so give readiness probes more time. (#6484, 1c5ddcdb4, #6521, #6465) Server mutators: return values must be JSON. A custom Database must pass addRetryPredicate on to the transaction it creates, or tx.retryOn() won't work. Using tx after its transaction has ended now throws. (7756eb9bb, 4fe91c0d8) ZQLDatabase.run() no longer wraps reads in a transaction, so per-transaction settings such as SET LOCAL don't apply to it. (#6577) useSuspenseQuery with an options object and no suspendUntil now suspends until the first rows arrive, as documented. Pass suspendUntil: 'complete' for the old behavior. (ffe67b23a) Upgrading from 1.9 or earlier: clients start with a new local database, and mutations they hadn't sent yet are lost. Clients upgrading from 1.10 keep theirs. (d597c69e8, 28e581483) Client storage: a client whose local store is corrupt now deletes it and calls onClientStateNotFound. Unsent mutations are lost. (#6558) One-time query reload: queries are identified differently in 1.11, so after upgrading, and again after a rollback, zero-cache transforms and hydrates each client's queries once more. Expect a burst of requests to ZERO_QUERY_URL. (#6450, #6462) Deprecated: ZERO_CHANGE_STREAMER_MODE. Use ZERO_CHANGE_STREAMER_URI instead. (698286b55) Events: there's a new zero/events/status/view-syncer/v1 event type. Event consumers that reject unknown types must accept it. (#6621)", "headings": [ { "text": "Installation", @@ -5773,7 +5773,7 @@ "sectionTitle": "Overview", "sectionId": "overview", "url": "/docs/release-notes/1.11", - "content": "Zero 1.11 can show cached query results right away, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication.", + "content": "Zero 1.11 tells you when local query results were already confirmed by the server, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication.", "kind": "section" }, { @@ -5783,7 +5783,7 @@ "sectionTitle": "Features", "sectionId": "features", "url": "/docs/release-notes/1.11", - "content": "Queries can now start with results that a previous session saved on the device. These come back with the new 'cached' result type, so you can show them right away and still tell them apart from data the server has confirmed. (#6578) (thanks @pawarren!) Building the same query again usually gives you the same Query object. useQuery no longer rebuilds and rehashes its query on every render, and queries built inline work as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero starts in a new 'initializing' connection state while it loads local data, and slow loading doesn't count against the connect timeout anymore. (#6561) If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore tells you which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Clients upgrading from 1.10 keep their local data and pending mutations. Future protocol changes won't reset clients to an empty local database either. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when tx.retryOn() accepts the error, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. A replica restored from another replica's backup continues that backup incrementally. Periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits for replication to catch up before it reports ready. (1c5ddcdb4) Inactive client groups are kept for 7 days instead of 48 hours, so users who come back within a week don't need a full resync. (#6484) zero-cache now logs slow queries with the reason and suggested indexes, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) A new hydration budget caps the time a view-syncer spends re-hydrating a reconnecting client's inactive queries, and a per-query hydration timeout aborts any query that takes too long to hydrate. Both are off by default. (#6477, #6499) New OpenTelemetry metrics track WAL usage. Replication status events now include index-build progress, and a new view-syncer status event reports restore progress. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image is down to 807 MB from about 1.09 GB. (#6695)", + "content": "Queries now tell you when their local data is a result the server confirmed in an earlier session: they start with the new 'cached' result type instead of 'unknown'. That's enough to show stored data or an empty state without waiting for the server, even offline. (#6578) (thanks @pawarren!) Building the same query again usually gives you the same Query object. useQuery no longer rebuilds and rehashes its query on every render, and queries built inline work as React dependencies. (#6469, #6496) (thanks @charpeni!) Zero starts in a new 'initializing' connection state while it loads local data, and slow loading doesn't count against the connect timeout anymore. (#6561) If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new onStorageFailure option and keeps running in memory. zero.kvStore tells you which store is in use. (#6585, #6657, #6686) (thanks @pawarren and @Karavil!) Clients upgrading from 1.10 keep their local data and pending mutations. Future protocol changes won't reset clients to an empty local database either. (d597c69e8, 28e581483) Server mutators can return a JSON value to the client, run again in a new transaction when tx.retryOn() accepts the error, and run at a chosen transaction isolation level. (7756eb9bb, 4fe91c0d8, cb2821f3a) (thanks @pawarren!) Litestream v5 backups are now the default. A replica restored from another replica's backup continues that backup incrementally. Periodic snapshots keep backups compact, and the replica's WAL stays bounded. (fb536f686, c1a586810, #6476, #6539, #6717) A zero-cache that comes online now waits for replication to catch up before it reports ready. (1c5ddcdb4) Inactive client groups are kept for 7 days instead of 48 hours, so users who come back within a week don't need a full resync. (#6484) zero-cache now logs slow queries with the reason and suggested indexes, and periodically logs per-query statistics. A new operator password gives access to CPU profiles (/profz) and replica planner statistics (/plannerz) without the admin password. (#6446, #6639, #6673, #6675, #6699) A new hydration budget caps the time a view-syncer spends re-hydrating a reconnecting client's inactive queries, and a per-query hydration timeout aborts any query that takes too long to hydrate. Both are off by default. (#6477, #6499) New OpenTelemetry metrics track WAL usage. Replication status events now include index-build progress, and a new view-syncer status event reports restore progress. (#6591, #6621, #6714) Server mutations can send their logs to your own LogSink. (#6677) (thanks @pawarren!) The zero Docker image is down to 807 MB from about 1.09 GB. (#6695)", "kind": "section" }, { diff --git a/contents/docs/release-notes/1.11.mdx b/contents/docs/release-notes/1.11.mdx index a367c201..4b6d7988 100644 --- a/contents/docs/release-notes/1.11.mdx +++ b/contents/docs/release-notes/1.11.mdx @@ -20,11 +20,11 @@ npm install @rocicorp/zero@1.11 ## Overview -Zero 1.11 can show cached query results right away, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication. +Zero 1.11 tells you when local query results were already confirmed by the server, keeps working when local storage fails, and makes Litestream v5 the default for backups. Client cold start is about 7x faster in our benchmarks. There are also a lot of fixes, especially in queries and replication. ## Features -- Queries can now start with results that a previous session saved on the device. These come back with the new [`'cached'` result type](/docs/queries#cached-results), so you can show them right away and still tell them apart from data the server has confirmed. ([#6578](https://github.com/rocicorp/mono/pull/6578)) (thanks [@pawarren](https://github.com/pawarren)!) +- Queries now tell you when their local data is a result the server confirmed in an earlier session: they start with the new [`'cached'` result type](/docs/queries#cached-results) instead of `'unknown'`. That's enough to show stored data or an empty state without waiting for the server, even offline. ([#6578](https://github.com/rocicorp/mono/pull/6578)) (thanks [@pawarren](https://github.com/pawarren)!) - Building the same query again usually gives you the same `Query` object. `useQuery` no longer rebuilds and rehashes its query on every render, and queries built inline work as [React dependencies](/docs/react#query-identity). ([#6469](https://github.com/rocicorp/mono/pull/6469), [#6496](https://github.com/rocicorp/mono/pull/6496)) (thanks [@charpeni](https://github.com/charpeni)!) - Zero starts in a new [`'initializing'` connection state](/docs/connection#initializing) while it loads local data, and slow loading doesn't count against the connect timeout anymore. ([#6561](https://github.com/rocicorp/mono/pull/6561)) - If local storage can't be opened, fills up, or hits I/O errors, Zero reports it once through the new [`onStorageFailure`](/docs/connection#storage-failures) option and keeps running in memory. `zero.kvStore` tells you which store is in use. ([#6585](https://github.com/rocicorp/mono/pull/6585), [#6657](https://github.com/rocicorp/mono/pull/6657), [#6686](https://github.com/rocicorp/mono/pull/6686)) (thanks [@pawarren](https://github.com/pawarren) and [@Karavil](https://github.com/Karavil)!)