diff --git a/assets/search-index.json b/assets/search-index.json index 0d4d7415..0a72d4a4 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -14,7 +14,7 @@ "kind": "page" }, { - "id": "76-agents#options", + "id": "77-agents#options", "title": "Agent Support", "searchTitle": "Options", "sectionTitle": "Options", @@ -126,7 +126,7 @@ "kind": "page" }, { - "id": "77-auth#set-userid-on-client", + "id": "78-auth#set-userid-on-client", "title": "Authentication", "searchTitle": "Set userID on Client", "sectionTitle": "Set userID on Client", @@ -136,7 +136,7 @@ "kind": "section" }, { - "id": "78-auth#define-the-context-type", + "id": "79-auth#define-the-context-type", "title": "Authentication", "searchTitle": "Define the Context Type", "sectionTitle": "Define the Context Type", @@ -146,7 +146,7 @@ "kind": "section" }, { - "id": "79-auth#send-credentials", + "id": "80-auth#send-credentials", "title": "Authentication", "searchTitle": "Send Credentials", "sectionTitle": "Send Credentials", @@ -156,7 +156,7 @@ "kind": "section" }, { - "id": "80-auth#cookies", + "id": "81-auth#cookies", "title": "Authentication", "searchTitle": "Cookies", "sectionTitle": "Cookies", @@ -166,7 +166,7 @@ "kind": "section" }, { - "id": "81-auth#cookie-deployment", + "id": "82-auth#cookie-deployment", "title": "Authentication", "searchTitle": "Cookie Deployment", "sectionTitle": "Cookie Deployment", @@ -176,7 +176,7 @@ "kind": "section" }, { - "id": "82-auth#tokens", + "id": "83-auth#tokens", "title": "Authentication", "searchTitle": "Tokens", "sectionTitle": "Tokens", @@ -186,7 +186,7 @@ "kind": "section" }, { - "id": "83-auth#implement-api-endpoints", + "id": "84-auth#implement-api-endpoints", "title": "Authentication", "searchTitle": "Implement API Endpoints", "sectionTitle": "Implement API Endpoints", @@ -196,7 +196,7 @@ "kind": "section" }, { - "id": "84-auth#query", + "id": "85-auth#query", "title": "Authentication", "searchTitle": "Query", "sectionTitle": "Query", @@ -206,7 +206,7 @@ "kind": "section" }, { - "id": "85-auth#mutate", + "id": "86-auth#mutate", "title": "Authentication", "searchTitle": "Mutate", "sectionTitle": "Mutate", @@ -216,7 +216,7 @@ "kind": "section" }, { - "id": "86-auth#updating-tokens", + "id": "87-auth#updating-tokens", "title": "Authentication", "searchTitle": "Updating Tokens", "sectionTitle": "Updating Tokens", @@ -226,7 +226,7 @@ "kind": "section" }, { - "id": "87-auth#auth-failure-and-refresh", + "id": "88-auth#auth-failure-and-refresh", "title": "Authentication", "searchTitle": "Auth Failure and Refresh", "sectionTitle": "Auth Failure and Refresh", @@ -236,7 +236,7 @@ "kind": "section" }, { - "id": "88-auth#permission-patterns", + "id": "89-auth#permission-patterns", "title": "Authentication", "searchTitle": "Permission Patterns", "sectionTitle": "Permission Patterns", @@ -246,7 +246,7 @@ "kind": "section" }, { - "id": "89-auth#read-permissions", + "id": "90-auth#read-permissions", "title": "Authentication", "searchTitle": "Read Permissions", "sectionTitle": "Read Permissions", @@ -256,7 +256,7 @@ "kind": "section" }, { - "id": "90-auth#only-owned-rows", + "id": "91-auth#only-owned-rows", "title": "Authentication", "searchTitle": "Only Owned Rows", "sectionTitle": "Only Owned Rows", @@ -266,7 +266,7 @@ "kind": "section" }, { - "id": "91-auth#owned-or-shared-rows", + "id": "92-auth#owned-or-shared-rows", "title": "Authentication", "searchTitle": "Owned or Shared Rows", "sectionTitle": "Owned or Shared Rows", @@ -276,7 +276,7 @@ "kind": "section" }, { - "id": "92-auth#owned-rows-or-all-if-admin", + "id": "93-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": "93-auth#deny-by-returning-no-rows", + "id": "94-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": "94-auth#write-permissions", + "id": "95-auth#write-permissions", "title": "Authentication", "searchTitle": "Write Permissions", "sectionTitle": "Write Permissions", @@ -306,7 +306,7 @@ "kind": "section" }, { - "id": "95-auth#enforce-ownership", + "id": "96-auth#enforce-ownership", "title": "Authentication", "searchTitle": "Enforce Ownership", "sectionTitle": "Enforce Ownership", @@ -316,7 +316,7 @@ "kind": "section" }, { - "id": "96-auth#edit-owned-rows", + "id": "97-auth#edit-owned-rows", "title": "Authentication", "searchTitle": "Edit Owned Rows", "sectionTitle": "Edit Owned Rows", @@ -326,7 +326,7 @@ "kind": "section" }, { - "id": "97-auth#edit-owned-or-shared-rows", + "id": "98-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": "98-auth#edit-owned-or-all-if-admin", + "id": "99-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": "99-auth#logging-out", + "id": "100-auth#logging-out", "title": "Authentication", "searchTitle": "Logging Out", "sectionTitle": "Logging Out", @@ -383,7 +383,7 @@ "kind": "page" }, { - "id": "100-community#ui-frameworks", + "id": "101-community#ui-frameworks", "title": "From the Community", "searchTitle": "UI Frameworks", "sectionTitle": "UI Frameworks", @@ -393,7 +393,7 @@ "kind": "section" }, { - "id": "101-community#miscellaneous", + "id": "102-community#miscellaneous", "title": "From the Community", "searchTitle": "Miscellaneous", "sectionTitle": "Miscellaneous", @@ -407,7 +407,7 @@ "title": "Connecting to Postgres", "searchTitle": "Connecting to Postgres", "url": "/docs/connecting-to-postgres", - "content": "In the future, Zero will work with many different backend databases. Today only Postgres is supported. Specifically, Zero requires Postgres v15.0 or higher, and support for logical replication. Here are some common Postgres options and what we know about their support level: Event Triggers Zero uses Postgres “Event Triggers” when possible to implement high-quality, efficient schema migration. Some hosted Postgres providers don't provide access to Event Triggers. Zero still works out of the box with these providers, but for correctness, any schema change triggers a full reset of all server-side and client-side state. For small databases (< 10GB) this can be OK, but for bigger databases you should either manually tell Zero about the schema change or choose a provider with event trigger support. Configuration WAL Level The Postgres wal_level config parameter has to be set to logical. You can check what level your pg has with this command: psql -c 'SHOW wal_level' If it doesn’t output logical then you need to change the wal level. To do this, run: psql -c \"ALTER SYSTEM SET wal_level = 'logical';\" Then restart Postgres. On most pg systems you can do this like so: data_dir=$(psql -t -A -c 'SHOW data_directory') pg_ctl -D \"$data_dir\" restart After your server restarts, show the wal_level again to ensure it has changed: psql -c 'SHOW wal_level' Bounding WAL Size For development databases, you can set a max_slot_wal_keep_size value in Postgres. This will help limit the amount of WAL kept around. This is a configuration parameter that bounds the amount of WAL kept around for replication slots, and invalidates the slots that are too far behind. Zero-cache will automatically detect if the replication slot has been invalidated and re-sync replicas from scratch. This configuration can cause problems like slot has been invalidated because it exceeded the maximum reserved size and is not recommended for production databases. Provider-Specific Notes PlanetScale for Postgres Roles zero-cache should connect using the default role that PlanetScale provides, because PlanetScale user-defined roles cannot create replication slots. Connection Limits Change max_connections to at least 100. The default is 25, which is too low for Zero in most configurations. Pooling Make sure to only use a direct connection for the ZERO_UPSTREAM_DB, and use pooled URLs for ZERO_CVR_DB, ZERO_CHANGE_DB, and your API (see Deployment). High Availability PlanetScale Postgres can fail over to a standby during maintenance or an outage. By default a logical replication slot does not survive promotion of a standby, so after a failover zero-cache would find its slot missing and re-sync every replica from scratch. To avoid this, first, run zero-cache with ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER=true so it creates failover-enabled slots. Then, run the script below to register Zero's replication slots with PlanetScale and enable the two cluster parameters failover needs: APP=\"\" # your ZERO_APP_ID — on Zero Cloud this is your instance ID ORG=\"\" # PlanetScale organization DB=\"\" # PlanetScale database BRANCH=\"main\" SHARD=\"0\" if [ -z \"$APP\" ] || [ -z \"$ORG\" ] || [ -z \"$DB\" ]; then echo \"Set APP, ORG, and DB first — nothing was sent.\" elif pscale api -X PATCH \"organizations/${ORG}/databases/${DB}/branches/${BRANCH}/changes\" --input=- >/dev/null </dev/null <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 itself crashes, or if the mutate or query endpoints return a network or HTTP error, Zero transitions to the error state. This type of error is unlikely to resolve just by retrying, so Zero doesn't try. The app can retry the connection manually by calling zero.connection.connect(). 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: 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.", "headings": [ { "text": "Overview", @@ -819,7 +847,7 @@ "kind": "page" }, { - "id": "126-connection#overview", + "id": "129-connection#overview", "title": "Connection Status", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -829,7 +857,7 @@ "kind": "section" }, { - "id": "127-connection#usage", + "id": "130-connection#usage", "title": "Connection Status", "searchTitle": "Usage", "sectionTitle": "Usage", @@ -839,7 +867,7 @@ "kind": "section" }, { - "id": "128-connection#offline", + "id": "131-connection#offline", "title": "Connection Status", "searchTitle": "Offline", "sectionTitle": "Offline", @@ -849,7 +877,7 @@ "kind": "section" }, { - "id": "129-connection#offline-ui", + "id": "132-connection#offline-ui", "title": "Connection Status", "searchTitle": "Offline UI", "sectionTitle": "Offline UI", @@ -859,17 +887,17 @@ "kind": "section" }, { - "id": "130-connection#details", + "id": "133-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 itself crashes, or if the mutate or query endpoints return a network or HTTP error, Zero transitions to the error state. This type of error is unlikely to resolve just by retrying, so Zero doesn't try. The app can retry the connection manually by calling zero.connection.connect(). 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": "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.", "kind": "section" }, { - "id": "131-connection#connecting", + "id": "134-connection#connecting", "title": "Connection Status", "searchTitle": "Connecting", "sectionTitle": "Connecting", @@ -879,7 +907,7 @@ "kind": "section" }, { - "id": "132-connection#connected", + "id": "135-connection#connected", "title": "Connection Status", "searchTitle": "Connected", "sectionTitle": "Connected", @@ -889,7 +917,7 @@ "kind": "section" }, { - "id": "133-connection#disconnected", + "id": "136-connection#disconnected", "title": "Connection Status", "searchTitle": "Disconnected", "sectionTitle": "Disconnected", @@ -899,17 +927,17 @@ "kind": "section" }, { - "id": "134-connection#error", + "id": "137-connection#error", "title": "Connection Status", "searchTitle": "Error", "sectionTitle": "Error", "sectionId": "error", "url": "/docs/connection", - "content": "If zero-cache itself crashes, or if the mutate or query endpoints return a network or HTTP error, Zero transitions to the error state. This type of error is unlikely to resolve just by retrying, so Zero doesn't try. The app can retry the connection manually by calling zero.connection.connect(). 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}`) ) }) })", + "content": "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}`) ) }) })", "kind": "section" }, { - "id": "135-connection#needs-auth", + "id": "138-connection#needs-auth", "title": "Connection Status", "searchTitle": "Needs-Auth", "sectionTitle": "Needs-Auth", @@ -919,7 +947,7 @@ "kind": "section" }, { - "id": "136-connection#closed", + "id": "139-connection#closed", "title": "Connection Status", "searchTitle": "Closed", "sectionTitle": "Closed", @@ -929,7 +957,7 @@ "kind": "section" }, { - "id": "137-connection#why-zero-doesnt-support-offline-writes", + "id": "140-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", @@ -939,7 +967,7 @@ "kind": "section" }, { - "id": "138-connection#example", + "id": "141-connection#example", "title": "Connection Status", "searchTitle": "Example", "sectionTitle": "Example", @@ -949,7 +977,7 @@ "kind": "section" }, { - "id": "139-connection#tradeoffs", + "id": "142-connection#tradeoffs", "title": "Connection Status", "searchTitle": "Tradeoffs", "sectionTitle": "Tradeoffs", @@ -959,7 +987,7 @@ "kind": "section" }, { - "id": "140-connection#zeros-position", + "id": "143-connection#zeros-position", "title": "Connection Status", "searchTitle": "Zero's Position", "sectionTitle": "Zero's Position", @@ -1007,7 +1035,7 @@ "kind": "page" }, { - "id": "141-debug/analyze-query-cli#set-up", + "id": "144-debug/analyze-query-cli#set-up", "title": "Analyze Query CLI", "searchTitle": "Set Up", "sectionTitle": "Set Up", @@ -1017,7 +1045,7 @@ "kind": "section" }, { - "id": "142-debug/analyze-query-cli#run-zql-queries", + "id": "145-debug/analyze-query-cli#run-zql-queries", "title": "Analyze Query CLI", "searchTitle": "Run ZQL Queries", "sectionTitle": "Run ZQL Queries", @@ -1027,7 +1055,7 @@ "kind": "section" }, { - "id": "143-debug/analyze-query-cli#production-use", + "id": "146-debug/analyze-query-cli#production-use", "title": "Analyze Query CLI", "searchTitle": "Production Use", "sectionTitle": "Production Use", @@ -1037,7 +1065,7 @@ "kind": "section" }, { - "id": "144-debug/analyze-query-cli#env-var-shorthand", + "id": "147-debug/analyze-query-cli#env-var-shorthand", "title": "Analyze Query CLI", "searchTitle": "Env Var Shorthand", "sectionTitle": "Env Var Shorthand", @@ -1047,7 +1075,7 @@ "kind": "section" }, { - "id": "145-debug/analyze-query-cli#other-input-modes", + "id": "148-debug/analyze-query-cli#other-input-modes", "title": "Analyze Query CLI", "searchTitle": "Other Input Modes", "sectionTitle": "Other Input Modes", @@ -1057,7 +1085,7 @@ "kind": "section" }, { - "id": "146-debug/analyze-query-cli#output", + "id": "149-debug/analyze-query-cli#output", "title": "Analyze Query CLI", "searchTitle": "Output", "sectionTitle": "Output", @@ -1067,7 +1095,7 @@ "kind": "section" }, { - "id": "147-debug/analyze-query-cli#optional-output", + "id": "150-debug/analyze-query-cli#optional-output", "title": "Analyze Query CLI", "searchTitle": "Optional Output", "sectionTitle": "Optional Output", @@ -1127,7 +1155,7 @@ "kind": "page" }, { - "id": "148-debug/inspector#accessing-the-inspector", + "id": "151-debug/inspector#accessing-the-inspector", "title": "Inspector", "searchTitle": "Accessing the Inspector", "sectionTitle": "Accessing the Inspector", @@ -1137,7 +1165,7 @@ "kind": "section" }, { - "id": "149-debug/inspector#clients-and-groups", + "id": "152-debug/inspector#clients-and-groups", "title": "Inspector", "searchTitle": "Clients and Groups", "sectionTitle": "Clients and Groups", @@ -1147,7 +1175,7 @@ "kind": "section" }, { - "id": "150-debug/inspector#queries", + "id": "153-debug/inspector#queries", "title": "Inspector", "searchTitle": "Queries", "sectionTitle": "Queries", @@ -1157,7 +1185,7 @@ "kind": "section" }, { - "id": "151-debug/inspector#analyzing-queries", + "id": "154-debug/inspector#analyzing-queries", "title": "Inspector", "searchTitle": "Analyzing Queries", "sectionTitle": "Analyzing Queries", @@ -1167,7 +1195,7 @@ "kind": "section" }, { - "id": "152-debug/inspector#interpreting-query-analysis", + "id": "155-debug/inspector#interpreting-query-analysis", "title": "Inspector", "searchTitle": "Interpreting Query Analysis", "sectionTitle": "Interpreting Query Analysis", @@ -1177,7 +1205,7 @@ "kind": "section" }, { - "id": "153-debug/inspector#viewing-sqlite-plans", + "id": "156-debug/inspector#viewing-sqlite-plans", "title": "Inspector", "searchTitle": "Viewing SQLite Plans", "sectionTitle": "Viewing SQLite Plans", @@ -1187,7 +1215,7 @@ "kind": "section" }, { - "id": "154-debug/inspector#viewing-zero-plans", + "id": "157-debug/inspector#viewing-zero-plans", "title": "Inspector", "searchTitle": "Viewing Zero Plans", "sectionTitle": "Viewing Zero Plans", @@ -1197,7 +1225,7 @@ "kind": "section" }, { - "id": "155-debug/inspector#analyzing-arbitrary-zql", + "id": "158-debug/inspector#analyzing-arbitrary-zql", "title": "Inspector", "searchTitle": "Analyzing Arbitrary ZQL", "sectionTitle": "Analyzing Arbitrary ZQL", @@ -1207,7 +1235,7 @@ "kind": "section" }, { - "id": "156-debug/inspector#table-data", + "id": "159-debug/inspector#table-data", "title": "Inspector", "searchTitle": "Table Data", "sectionTitle": "Table Data", @@ -1217,7 +1245,7 @@ "kind": "section" }, { - "id": "157-debug/inspector#server-version", + "id": "160-debug/inspector#server-version", "title": "Inspector", "searchTitle": "Server Version", "sectionTitle": "Server Version", @@ -1258,7 +1286,7 @@ "kind": "page" }, { - "id": "158-debug/replication#resetting", + "id": "161-debug/replication#resetting", "title": "Replication", "searchTitle": "Resetting", "sectionTitle": "Resetting", @@ -1268,7 +1296,7 @@ "kind": "section" }, { - "id": "159-debug/replication#inspecting", + "id": "162-debug/replication#inspecting", "title": "Replication", "searchTitle": "Inspecting", "sectionTitle": "Inspecting", @@ -1278,7 +1306,7 @@ "kind": "section" }, { - "id": "160-debug/replication#miscellaneous", + "id": "163-debug/replication#miscellaneous", "title": "Replication", "searchTitle": "Miscellaneous", "sectionTitle": "Miscellaneous", @@ -1318,7 +1346,7 @@ "kind": "page" }, { - "id": "161-debug/slow-queries#analyze-queries", + "id": "164-debug/slow-queries#analyze-queries", "title": "Slow Queries", "searchTitle": "Analyze Queries", "sectionTitle": "Analyze Queries", @@ -1328,7 +1356,7 @@ "kind": "section" }, { - "id": "162-debug/slow-queries#check-ttl", + "id": "165-debug/slow-queries#check-ttl", "title": "Slow Queries", "searchTitle": "Check ttl", "sectionTitle": "Check ttl", @@ -1338,7 +1366,7 @@ "kind": "section" }, { - "id": "163-debug/slow-queries#locality", + "id": "166-debug/slow-queries#locality", "title": "Slow Queries", "searchTitle": "Locality", "sectionTitle": "Locality", @@ -1348,7 +1376,7 @@ "kind": "section" }, { - "id": "164-debug/slow-queries#check-storage", + "id": "167-debug/slow-queries#check-storage", "title": "Slow Queries", "searchTitle": "Check Storage", "sectionTitle": "Check Storage", @@ -1358,7 +1386,7 @@ "kind": "section" }, { - "id": "165-debug/slow-queries#statz", + "id": "168-debug/slow-queries#statz", "title": "Slow Queries", "searchTitle": "/statz", "sectionTitle": "/statz", @@ -1391,7 +1419,7 @@ "kind": "page" }, { - "id": "166-deprecated/ad-hoc-queries#overview", + "id": "169-deprecated/ad-hoc-queries#overview", "title": "Ad-Hoc Queries (Deprecated)", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -1415,7 +1443,7 @@ "kind": "page" }, { - "id": "167-deprecated/crud-mutators#overview", + "id": "170-deprecated/crud-mutators#overview", "title": "CRUD Mutators (Deprecated)", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -1487,7 +1515,7 @@ "kind": "page" }, { - "id": "168-deprecated/rls-permissions#define-permissions", + "id": "171-deprecated/rls-permissions#define-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Define Permissions", "sectionTitle": "Define Permissions", @@ -1497,7 +1525,7 @@ "kind": "section" }, { - "id": "169-deprecated/rls-permissions#access-is-denied-by-default", + "id": "172-deprecated/rls-permissions#access-is-denied-by-default", "title": "RLS Permissions (Deprecated)", "searchTitle": "Access is Denied by Default", "sectionTitle": "Access is Denied by Default", @@ -1507,7 +1535,7 @@ "kind": "section" }, { - "id": "170-deprecated/rls-permissions#permission-evaluation", + "id": "173-deprecated/rls-permissions#permission-evaluation", "title": "RLS Permissions (Deprecated)", "searchTitle": "Permission Evaluation", "sectionTitle": "Permission Evaluation", @@ -1517,7 +1545,7 @@ "kind": "section" }, { - "id": "171-deprecated/rls-permissions#permission-deployment", + "id": "174-deprecated/rls-permissions#permission-deployment", "title": "RLS Permissions (Deprecated)", "searchTitle": "Permission Deployment", "sectionTitle": "Permission Deployment", @@ -1527,7 +1555,7 @@ "kind": "section" }, { - "id": "172-deprecated/rls-permissions#rules", + "id": "175-deprecated/rls-permissions#rules", "title": "RLS Permissions (Deprecated)", "searchTitle": "Rules", "sectionTitle": "Rules", @@ -1537,7 +1565,7 @@ "kind": "section" }, { - "id": "173-deprecated/rls-permissions#select-permissions", + "id": "176-deprecated/rls-permissions#select-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Select Permissions", "sectionTitle": "Select Permissions", @@ -1547,7 +1575,7 @@ "kind": "section" }, { - "id": "174-deprecated/rls-permissions#insert-permissions", + "id": "177-deprecated/rls-permissions#insert-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Insert Permissions", "sectionTitle": "Insert Permissions", @@ -1557,7 +1585,7 @@ "kind": "section" }, { - "id": "175-deprecated/rls-permissions#update-permissions", + "id": "178-deprecated/rls-permissions#update-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Update Permissions", "sectionTitle": "Update Permissions", @@ -1567,7 +1595,7 @@ "kind": "section" }, { - "id": "176-deprecated/rls-permissions#delete-permissions", + "id": "179-deprecated/rls-permissions#delete-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Delete Permissions", "sectionTitle": "Delete Permissions", @@ -1577,7 +1605,7 @@ "kind": "section" }, { - "id": "177-deprecated/rls-permissions#permissions-based-on-auth-data", + "id": "180-deprecated/rls-permissions#permissions-based-on-auth-data", "title": "RLS Permissions (Deprecated)", "searchTitle": "Permissions Based on Auth Data", "sectionTitle": "Permissions Based on Auth Data", @@ -1587,7 +1615,7 @@ "kind": "section" }, { - "id": "178-deprecated/rls-permissions#debugging", + "id": "181-deprecated/rls-permissions#debugging", "title": "RLS Permissions (Deprecated)", "searchTitle": "Debugging", "sectionTitle": "Debugging", @@ -1597,7 +1625,7 @@ "kind": "section" }, { - "id": "179-deprecated/rls-permissions#read-permissions", + "id": "182-deprecated/rls-permissions#read-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Read Permissions", "sectionTitle": "Read Permissions", @@ -1607,7 +1635,7 @@ "kind": "section" }, { - "id": "180-deprecated/rls-permissions#write-permissions", + "id": "183-deprecated/rls-permissions#write-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Write Permissions", "sectionTitle": "Write Permissions", @@ -1687,7 +1715,7 @@ "kind": "page" }, { - "id": "181-install#integrate-zero", + "id": "184-install#integrate-zero", "title": "Install Zero", "searchTitle": "Integrate Zero", "sectionTitle": "Integrate Zero", @@ -1697,7 +1725,7 @@ "kind": "section" }, { - "id": "182-install#set-up-your-database", + "id": "185-install#set-up-your-database", "title": "Install Zero", "searchTitle": "Set Up Your Database", "sectionTitle": "Set Up Your Database", @@ -1707,7 +1735,7 @@ "kind": "section" }, { - "id": "183-install#install-zero", + "id": "186-install#install-zero", "title": "Install Zero", "searchTitle": "Install Zero", "sectionTitle": "Install Zero", @@ -1717,7 +1745,7 @@ "kind": "section" }, { - "id": "184-install#set-up-your-zero-schema", + "id": "187-install#set-up-your-zero-schema", "title": "Install Zero", "searchTitle": "Set Up Your Zero Schema", "sectionTitle": "Set Up Your Zero Schema", @@ -1727,7 +1755,7 @@ "kind": "section" }, { - "id": "185-install#set-up-the-zero-client", + "id": "188-install#set-up-the-zero-client", "title": "Install Zero", "searchTitle": "Set Up the Zero Client", "sectionTitle": "Set Up the Zero Client", @@ -1737,7 +1765,7 @@ "kind": "section" }, { - "id": "186-install#sync-data", + "id": "189-install#sync-data", "title": "Install Zero", "searchTitle": "Sync Data", "sectionTitle": "Sync Data", @@ -1747,7 +1775,7 @@ "kind": "section" }, { - "id": "187-install#define-query", + "id": "190-install#define-query", "title": "Install Zero", "searchTitle": "Define Query", "sectionTitle": "Define Query", @@ -1757,7 +1785,7 @@ "kind": "section" }, { - "id": "188-install#add-query-endpoint", + "id": "191-install#add-query-endpoint", "title": "Install Zero", "searchTitle": "Add Query Endpoint", "sectionTitle": "Add Query Endpoint", @@ -1767,7 +1795,7 @@ "kind": "section" }, { - "id": "189-install#invoke-query", + "id": "192-install#invoke-query", "title": "Install Zero", "searchTitle": "Invoke Query", "sectionTitle": "Invoke Query", @@ -1777,7 +1805,7 @@ "kind": "section" }, { - "id": "190-install#more-about-queries", + "id": "193-install#more-about-queries", "title": "Install Zero", "searchTitle": "More about Queries", "sectionTitle": "More about Queries", @@ -1787,7 +1815,7 @@ "kind": "section" }, { - "id": "191-install#mutate-data", + "id": "194-install#mutate-data", "title": "Install Zero", "searchTitle": "Mutate Data", "sectionTitle": "Mutate Data", @@ -1797,7 +1825,7 @@ "kind": "section" }, { - "id": "192-install#define-mutators", + "id": "195-install#define-mutators", "title": "Install Zero", "searchTitle": "Define Mutators", "sectionTitle": "Define Mutators", @@ -1807,7 +1835,7 @@ "kind": "section" }, { - "id": "193-install#add-mutate-endpoint", + "id": "196-install#add-mutate-endpoint", "title": "Install Zero", "searchTitle": "Add Mutate Endpoint", "sectionTitle": "Add Mutate Endpoint", @@ -1817,7 +1845,7 @@ "kind": "section" }, { - "id": "194-install#invoke-mutators", + "id": "197-install#invoke-mutators", "title": "Install Zero", "searchTitle": "Invoke Mutators", "sectionTitle": "Invoke Mutators", @@ -1827,7 +1855,7 @@ "kind": "section" }, { - "id": "195-install#more-about-mutators", + "id": "198-install#more-about-mutators", "title": "Install Zero", "searchTitle": "More about Mutators", "sectionTitle": "More about Mutators", @@ -1850,7 +1878,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' }) 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') }) If Zero receives any response from the mutate endpoint other than HTTP 200, 401, or 403, it will disconnect and enter the error state. 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. 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.", "headings": [ { "text": "Architecture", @@ -1972,7 +2000,7 @@ "kind": "page" }, { - "id": "196-mutators#architecture", + "id": "199-mutators#architecture", "title": "Mutators", "searchTitle": "Architecture", "sectionTitle": "Architecture", @@ -1982,7 +2010,7 @@ "kind": "section" }, { - "id": "197-mutators#life-of-a-mutation", + "id": "200-mutators#life-of-a-mutation", "title": "Mutators", "searchTitle": "Life of a Mutation", "sectionTitle": "Life of a Mutation", @@ -1992,17 +2020,17 @@ "kind": "section" }, { - "id": "198-mutators#defining-mutators", + "id": "201-mutators#defining-mutators", "title": "Mutators", "searchTitle": "Defining Mutators", "sectionTitle": "Defining Mutators", "sectionId": "defining-mutators", "url": "/docs/mutators", - "content": "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' }) 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.", + "content": "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.", "kind": "section" }, { - "id": "199-mutators#basics", + "id": "202-mutators#basics", "title": "Mutators", "searchTitle": "Basics", "sectionTitle": "Basics", @@ -2012,27 +2040,27 @@ "kind": "section" }, { - "id": "200-mutators#writing-data", + "id": "203-mutators#writing-data", "title": "Mutators", "searchTitle": "Writing Data", "sectionTitle": "Writing Data", "sectionId": "writing-data", "url": "/docs/mutators", - "content": "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' }) 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 })", + "content": "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 })", "kind": "section" }, { - "id": "201-mutators#insert", + "id": "204-mutators#insert", "title": "Mutators", "searchTitle": "Insert", "sectionTitle": "Insert", "sectionId": "insert", "url": "/docs/mutators", - "content": "Create new records with insert: tx.mutate.user.insert({ id: 'user-123', username: 'sam', language: 'js' }) 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 })", + "content": "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 })", "kind": "section" }, { - "id": "202-mutators#upsert", + "id": "205-mutators#upsert", "title": "Mutators", "searchTitle": "Upsert", "sectionTitle": "Upsert", @@ -2042,7 +2070,7 @@ "kind": "section" }, { - "id": "203-mutators#update", + "id": "206-mutators#update", "title": "Mutators", "searchTitle": "Update", "sectionTitle": "Update", @@ -2052,7 +2080,7 @@ "kind": "section" }, { - "id": "204-mutators#delete", + "id": "207-mutators#delete", "title": "Mutators", "searchTitle": "Delete", "sectionTitle": "Delete", @@ -2062,7 +2090,7 @@ "kind": "section" }, { - "id": "205-mutators#arguments", + "id": "208-mutators#arguments", "title": "Mutators", "searchTitle": "Arguments", "sectionTitle": "Arguments", @@ -2072,7 +2100,7 @@ "kind": "section" }, { - "id": "206-mutators#reading-data", + "id": "209-mutators#reading-data", "title": "Mutators", "searchTitle": "Reading Data", "sectionTitle": "Reading Data", @@ -2082,7 +2110,7 @@ "kind": "section" }, { - "id": "207-mutators#context", + "id": "210-mutators#context", "title": "Mutators", "searchTitle": "Context", "sectionTitle": "Context", @@ -2092,7 +2120,7 @@ "kind": "section" }, { - "id": "208-mutators#mutator-registries", + "id": "211-mutators#mutator-registries", "title": "Mutators", "searchTitle": "Mutator Registries", "sectionTitle": "Mutator Registries", @@ -2102,7 +2130,7 @@ "kind": "section" }, { - "id": "209-mutators#mutator-names", + "id": "212-mutators#mutator-names", "title": "Mutators", "searchTitle": "Mutator Names", "sectionTitle": "Mutator Names", @@ -2112,7 +2140,7 @@ "kind": "section" }, { - "id": "210-mutators#mutatorsts", + "id": "213-mutators#mutatorsts", "title": "Mutators", "searchTitle": "mutators.ts", "sectionTitle": "mutators.ts", @@ -2122,7 +2150,7 @@ "kind": "section" }, { - "id": "211-mutators#registration", + "id": "214-mutators#registration", "title": "Mutators", "searchTitle": "Registration", "sectionTitle": "Registration", @@ -2132,17 +2160,17 @@ "kind": "section" }, { - "id": "212-mutators#server-setup", + "id": "215-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') }) If Zero receives any response from the mutate endpoint other than HTTP 200, 401, or 403, it will disconnect and enter the error state. 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. 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": "213-mutators#registering-the-endpoint", + "id": "216-mutators#registering-the-endpoint", "title": "Mutators", "searchTitle": "Registering the Endpoint", "sectionTitle": "Registering the Endpoint", @@ -2152,7 +2180,7 @@ "kind": "section" }, { - "id": "214-mutators#implementing-the-endpoint", + "id": "217-mutators#implementing-the-endpoint", "title": "Mutators", "searchTitle": "Implementing the Endpoint", "sectionTitle": "Implementing the Endpoint", @@ -2162,17 +2190,17 @@ "kind": "section" }, { - "id": "215-mutators#handling-errors", + "id": "218-mutators#handling-errors", "title": "Mutators", "searchTitle": "Handling Errors", "sectionTitle": "Handling Errors", "sectionId": "handling-errors", "url": "/docs/mutators", - "content": "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') }) If Zero receives any response from the mutate endpoint other than HTTP 200, 401, or 403, it will disconnect and enter the error state. 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.", + "content": "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.", "kind": "section" }, { - "id": "216-mutators#custom-mutate-url", + "id": "219-mutators#custom-mutate-url", "title": "Mutators", "searchTitle": "Custom Mutate URL", "sectionTitle": "Custom Mutate URL", @@ -2182,7 +2210,7 @@ "kind": "section" }, { - "id": "217-mutators#url-patterns", + "id": "220-mutators#url-patterns", "title": "Mutators", "searchTitle": "URL Patterns", "sectionTitle": "URL Patterns", @@ -2192,7 +2220,7 @@ "kind": "section" }, { - "id": "218-mutators#server-specific-code", + "id": "221-mutators#server-specific-code", "title": "Mutators", "searchTitle": "Server-Specific Code", "sectionTitle": "Server-Specific Code", @@ -2202,7 +2230,7 @@ "kind": "section" }, { - "id": "219-mutators#running-mutators", + "id": "222-mutators#running-mutators", "title": "Mutators", "searchTitle": "Running Mutators", "sectionTitle": "Running Mutators", @@ -2212,7 +2240,7 @@ "kind": "section" }, { - "id": "220-mutators#waiting-for-results", + "id": "223-mutators#waiting-for-results", "title": "Mutators", "searchTitle": "Waiting for Results", "sectionTitle": "Waiting for Results", @@ -2222,7 +2250,7 @@ "kind": "section" }, { - "id": "221-mutators#permissions", + "id": "224-mutators#permissions", "title": "Mutators", "searchTitle": "Permissions", "sectionTitle": "Permissions", @@ -2232,7 +2260,7 @@ "kind": "section" }, { - "id": "222-mutators#dropping-down-to-raw-sql", + "id": "225-mutators#dropping-down-to-raw-sql", "title": "Mutators", "searchTitle": "Dropping Down to Raw SQL", "sectionTitle": "Dropping Down to Raw SQL", @@ -2242,7 +2270,7 @@ "kind": "section" }, { - "id": "223-mutators#notifications-and-async-work", + "id": "226-mutators#notifications-and-async-work", "title": "Mutators", "searchTitle": "Notifications and Async Work", "sectionTitle": "Notifications and Async Work", @@ -2252,7 +2280,7 @@ "kind": "section" }, { - "id": "224-mutators#custom-mutate-implementation", + "id": "227-mutators#custom-mutate-implementation", "title": "Mutators", "searchTitle": "Custom Mutate Implementation", "sectionTitle": "Custom Mutate Implementation", @@ -2276,7 +2304,7 @@ "kind": "page" }, { - "id": "225-open-source#business-model", + "id": "228-open-source#business-model", "title": "Zero is Open Source Software", "searchTitle": "Business Model", "sectionTitle": "Business Model", @@ -2290,7 +2318,7 @@ "title": "OpenTelemetry", "searchTitle": "OpenTelemetry", "url": "/docs/otel", - "content": "The zero-cache service embeds the JavaScript OTLP Exporter and can send logs, traces, and metrics to any standard otel collector. To enable otel, set the following environment variables then run zero-cache as normal: OTEL_EXPORTER_OTLP_ENDPOINT=\"\" OTEL_EXPORTER_OTLP_HEADERS=\"\" OTEL_RESOURCE_ATTRIBUTES=\"\" OTEL_NODE_RESOURCE_DETECTORS=\"env,host,os\" Grafana Cloud Walkthrough Here are instructions to setup Grafana Cloud, but the setup for other otel collectors should be similar. Sign up for Grafana Cloud (Free Tier) Click Connections > Add Connection in the left sidebar add-connection Search for \"OpenTelemetry\" and select it Click \"Quickstart\" quickstart Select \"JavaScript\" javascript Create a new token Copy the environment variables into your .env file or similar copy-env Start zero-cache Look for logs under \"Drilldown\" > \"Logs\" in left sidebar Distributed Tracing You can enable end-to-end trace correlation from your frontend through zero-cache to your API server. This allows you to see the full request flow in your tracing UI. To enable this, provide a getTraceparent callback when creating your Zero client: import {ZeroProvider} from '@rocicorp/zero/react' import {propagation, context} from '@opentelemetry/api' function getTraceparent() { const carrier: Record = {} propagation.inject(context.active(), carrier) return carrier.traceparent } return ( )import {ZeroProvider} from '@rocicorp/zero/solid' import {propagation, context} from '@opentelemetry/api' function getTraceparent() { const carrier: Record = {} propagation.inject(context.active(), carrier) return carrier.traceparent } return ( )import {Zero} from '@rocicorp/zero' import {propagation, context} from '@opentelemetry/api' const zero = new Zero({ // ... other options getTraceparent: () => { const carrier: Record = {} propagation.inject(context.active(), carrier) return carrier.traceparent } }) This callback is called before sending WebSocket messages that trigger API server calls (push, changeDesiredQueries, initConnection). The returned W3C traceparent header is forwarded through zero-cache to your API server, where it can be used to continue the trace. Metrics Reference view_syncer_lag and view_syncer_hydration require OpenTelemetry exponential histogram support. Prometheus users must enable native histograms. Use the existing serving_lag gauges if your backend does not support them. zero.server zero.replica zero.replication zero.sync zero.mutation", + "content": "The zero-cache service embeds the JavaScript OTLP Exporter and can send logs, traces, and metrics to any standard otel collector. To enable otel, set the following environment variables then run zero-cache as normal: OTEL_EXPORTER_OTLP_ENDPOINT=\"\" OTEL_EXPORTER_OTLP_HEADERS=\"\" OTEL_RESOURCE_ATTRIBUTES=\"\" OTEL_NODE_RESOURCE_DETECTORS=\"env,host,os\" Grafana Cloud Walkthrough Here are instructions to setup Grafana Cloud, but the setup for other otel collectors should be similar. Sign up for Grafana Cloud (Free Tier) Click Connections > Add Connection in the left sidebar add-connection Search for \"OpenTelemetry\" and select it Click \"Quickstart\" quickstart Select \"JavaScript\" javascript Create a new token Copy the environment variables into your .env file or similar copy-env Start zero-cache Look for logs under \"Drilldown\" > \"Logs\" in left sidebar Distributed Tracing You can enable end-to-end trace correlation from your frontend through zero-cache to your API server. This allows you to see the full request flow in your tracing UI. To enable this, provide a getTraceparent callback when creating your Zero client: import {ZeroProvider} from '@rocicorp/zero/react' import {propagation, context} from '@opentelemetry/api' function getTraceparent() { const carrier: Record = {} propagation.inject(context.active(), carrier) return carrier.traceparent } return ( )import {ZeroProvider} from '@rocicorp/zero/solid' import {propagation, context} from '@opentelemetry/api' function getTraceparent() { const carrier: Record = {} propagation.inject(context.active(), carrier) return carrier.traceparent } return ( )import {Zero} from '@rocicorp/zero' import {propagation, context} from '@opentelemetry/api' const zero = new Zero({ // ... other options getTraceparent: () => { const carrier: Record = {} propagation.inject(context.active(), carrier) return carrier.traceparent } }) This callback is called before sending WebSocket messages that trigger API server calls (push, changeDesiredQueries, initConnection). The returned W3C traceparent header is forwarded through zero-cache to your API server, where it can be used to continue the trace. Metrics Reference zero_sync_view_syncer_lag, zero_sync_view_syncer_hydration, and zero_sync_e2e_serving_lag require OpenTelemetry exponential histogram support. Prometheus users must enable native histograms. Use the existing zero_sync_serving_lag gauges if your backend does not support them. zero.server zero.replica zero.replication zero_replication_total_lag and zero_replication_last_total_lag now report the same latest measured round trip and do not grow when reports stop arriving. Use zero_replication_lag_report_retries to detect a stalled or missing report stream. zero.sync Serving-lag metrics include only client groups with at least one connected client and a validated background connection context. Retained groups without an eligible connection do not contribute lag. zero.mutation", "headings": [ { "text": "Grafana Cloud Walkthrough", @@ -2328,7 +2356,7 @@ "kind": "page" }, { - "id": "226-otel#grafana-cloud-walkthrough", + "id": "229-otel#grafana-cloud-walkthrough", "title": "OpenTelemetry", "searchTitle": "Grafana Cloud Walkthrough", "sectionTitle": "Grafana Cloud Walkthrough", @@ -2338,7 +2366,7 @@ "kind": "section" }, { - "id": "227-otel#distributed-tracing", + "id": "230-otel#distributed-tracing", "title": "OpenTelemetry", "searchTitle": "Distributed Tracing", "sectionTitle": "Distributed Tracing", @@ -2348,17 +2376,17 @@ "kind": "section" }, { - "id": "228-otel#metrics-reference", + "id": "231-otel#metrics-reference", "title": "OpenTelemetry", "searchTitle": "Metrics Reference", "sectionTitle": "Metrics Reference", "sectionId": "metrics-reference", "url": "/docs/otel", - "content": "view_syncer_lag and view_syncer_hydration require OpenTelemetry exponential histogram support. Prometheus users must enable native histograms. Use the existing serving_lag gauges if your backend does not support them. zero.server zero.replica zero.replication zero.sync zero.mutation", + "content": "zero_sync_view_syncer_lag, zero_sync_view_syncer_hydration, and zero_sync_e2e_serving_lag require OpenTelemetry exponential histogram support. Prometheus users must enable native histograms. Use the existing zero_sync_serving_lag gauges if your backend does not support them. zero.server zero.replica zero.replication zero_replication_total_lag and zero_replication_last_total_lag now report the same latest measured round trip and do not grow when reports stop arriving. Use zero_replication_lag_report_retries to detect a stalled or missing report stream. zero.sync Serving-lag metrics include only client groups with at least one connected client and a validated background connection context. Retained groups without an eligible connection do not contribute lag. zero.mutation", "kind": "section" }, { - "id": "229-otel#zeroserver", + "id": "232-otel#zeroserver", "title": "OpenTelemetry", "searchTitle": "zero.server", "sectionTitle": "zero.server", @@ -2368,7 +2396,7 @@ "kind": "section" }, { - "id": "230-otel#zeroreplica", + "id": "233-otel#zeroreplica", "title": "OpenTelemetry", "searchTitle": "zero.replica", "sectionTitle": "zero.replica", @@ -2378,27 +2406,27 @@ "kind": "section" }, { - "id": "231-otel#zeroreplication", + "id": "234-otel#zeroreplication", "title": "OpenTelemetry", "searchTitle": "zero.replication", "sectionTitle": "zero.replication", "sectionId": "zeroreplication", "url": "/docs/otel", - "content": "", + "content": "zero_replication_total_lag and zero_replication_last_total_lag now report the same latest measured round trip and do not grow when reports stop arriving. Use zero_replication_lag_report_retries to detect a stalled or missing report stream.", "kind": "section" }, { - "id": "232-otel#zerosync", + "id": "235-otel#zerosync", "title": "OpenTelemetry", "searchTitle": "zero.sync", "sectionTitle": "zero.sync", "sectionId": "zerosync", "url": "/docs/otel", - "content": "", + "content": "Serving-lag metrics include only client groups with at least one connected client and a validated background connection context. Retained groups without an eligible connection do not contribute lag.", "kind": "section" }, { - "id": "233-otel#zeromutation", + "id": "236-otel#zeromutation", "title": "OpenTelemetry", "searchTitle": "zero.mutation", "sectionTitle": "zero.mutation", @@ -2458,7 +2486,7 @@ "kind": "page" }, { - "id": "234-postgres-support#object-names", + "id": "237-postgres-support#object-names", "title": "Supported Postgres Features", "searchTitle": "Object Names", "sectionTitle": "Object Names", @@ -2468,7 +2496,7 @@ "kind": "section" }, { - "id": "235-postgres-support#object-types", + "id": "238-postgres-support#object-types", "title": "Supported Postgres Features", "searchTitle": "Object Types", "sectionTitle": "Object Types", @@ -2478,7 +2506,7 @@ "kind": "section" }, { - "id": "236-postgres-support#column-types", + "id": "239-postgres-support#column-types", "title": "Supported Postgres Features", "searchTitle": "Column Types", "sectionTitle": "Column Types", @@ -2488,7 +2516,7 @@ "kind": "section" }, { - "id": "237-postgres-support#column-defaults", + "id": "240-postgres-support#column-defaults", "title": "Supported Postgres Features", "searchTitle": "Column Defaults", "sectionTitle": "Column Defaults", @@ -2498,7 +2526,7 @@ "kind": "section" }, { - "id": "238-postgres-support#ids", + "id": "241-postgres-support#ids", "title": "Supported Postgres Features", "searchTitle": "IDs", "sectionTitle": "IDs", @@ -2508,7 +2536,7 @@ "kind": "section" }, { - "id": "239-postgres-support#primary-keys", + "id": "242-postgres-support#primary-keys", "title": "Supported Postgres Features", "searchTitle": "Primary Keys", "sectionTitle": "Primary Keys", @@ -2518,7 +2546,7 @@ "kind": "section" }, { - "id": "240-postgres-support#limiting-replication", + "id": "243-postgres-support#limiting-replication", "title": "Supported Postgres Features", "searchTitle": "Limiting Replication", "sectionTitle": "Limiting Replication", @@ -2528,7 +2556,7 @@ "kind": "section" }, { - "id": "241-postgres-support#zero-cache-replication", + "id": "244-postgres-support#zero-cache-replication", "title": "Supported Postgres Features", "searchTitle": "zero-cache replication", "sectionTitle": "zero-cache replication", @@ -2538,7 +2566,7 @@ "kind": "section" }, { - "id": "242-postgres-support#browser-client-replication", + "id": "245-postgres-support#browser-client-replication", "title": "Supported Postgres Features", "searchTitle": "Browser client replication", "sectionTitle": "Browser client replication", @@ -2548,7 +2576,7 @@ "kind": "section" }, { - "id": "243-postgres-support#schema-changes", + "id": "246-postgres-support#schema-changes", "title": "Supported Postgres Features", "searchTitle": "Schema changes", "sectionTitle": "Schema changes", @@ -2584,7 +2612,7 @@ "kind": "page" }, { - "id": "244-previews#overview", + "id": "247-previews#overview", "title": "Previews", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -2594,7 +2622,7 @@ "kind": "section" }, { - "id": "245-previews#configure-allowed-endpoint-patterns", + "id": "248-previews#configure-allowed-endpoint-patterns", "title": "Previews", "searchTitle": "Configure Allowed Endpoint Patterns", "sectionTitle": "Configure Allowed Endpoint Patterns", @@ -2604,7 +2632,7 @@ "kind": "section" }, { - "id": "246-previews#choose-endpoint-urls-in-the-client", + "id": "249-previews#choose-endpoint-urls-in-the-client", "title": "Previews", "searchTitle": "Choose Endpoint URLs in the Client", "sectionTitle": "Choose Endpoint URLs in the Client", @@ -2614,7 +2642,7 @@ "kind": "section" }, { - "id": "247-previews#schema-changes-in-previews", + "id": "250-previews#schema-changes-in-previews", "title": "Previews", "searchTitle": "Schema Changes in Previews", "sectionTitle": "Schema Changes in Previews", @@ -2758,7 +2786,7 @@ "kind": "page" }, { - "id": "248-queries#architecture", + "id": "251-queries#architecture", "title": "Queries", "searchTitle": "Architecture", "sectionTitle": "Architecture", @@ -2768,7 +2796,7 @@ "kind": "section" }, { - "id": "249-queries#life-of-a-query", + "id": "252-queries#life-of-a-query", "title": "Queries", "searchTitle": "Life of a Query", "sectionTitle": "Life of a Query", @@ -2778,7 +2806,7 @@ "kind": "section" }, { - "id": "250-queries#defining-queries", + "id": "253-queries#defining-queries", "title": "Queries", "searchTitle": "Defining Queries", "sectionTitle": "Defining Queries", @@ -2788,7 +2816,7 @@ "kind": "section" }, { - "id": "251-queries#basics", + "id": "254-queries#basics", "title": "Queries", "searchTitle": "Basics", "sectionTitle": "Basics", @@ -2798,7 +2826,7 @@ "kind": "section" }, { - "id": "252-queries#arguments", + "id": "255-queries#arguments", "title": "Queries", "searchTitle": "Arguments", "sectionTitle": "Arguments", @@ -2808,7 +2836,7 @@ "kind": "section" }, { - "id": "253-queries#query-registries", + "id": "256-queries#query-registries", "title": "Queries", "searchTitle": "Query Registries", "sectionTitle": "Query Registries", @@ -2818,7 +2846,7 @@ "kind": "section" }, { - "id": "254-queries#query-names", + "id": "257-queries#query-names", "title": "Queries", "searchTitle": "Query Names", "sectionTitle": "Query Names", @@ -2828,7 +2856,7 @@ "kind": "section" }, { - "id": "255-queries#context", + "id": "258-queries#context", "title": "Queries", "searchTitle": "Context", "sectionTitle": "Context", @@ -2838,7 +2866,7 @@ "kind": "section" }, { - "id": "256-queries#queriests", + "id": "259-queries#queriests", "title": "Queries", "searchTitle": "queries.ts", "sectionTitle": "queries.ts", @@ -2848,7 +2876,7 @@ "kind": "section" }, { - "id": "257-queries#server-setup", + "id": "260-queries#server-setup", "title": "Queries", "searchTitle": "Server Setup", "sectionTitle": "Server Setup", @@ -2858,7 +2886,7 @@ "kind": "section" }, { - "id": "258-queries#registering-the-endpoint", + "id": "261-queries#registering-the-endpoint", "title": "Queries", "searchTitle": "Registering the Endpoint", "sectionTitle": "Registering the Endpoint", @@ -2868,7 +2896,7 @@ "kind": "section" }, { - "id": "259-queries#implementing-the-endpoint", + "id": "262-queries#implementing-the-endpoint", "title": "Queries", "searchTitle": "Implementing the Endpoint", "sectionTitle": "Implementing the Endpoint", @@ -2878,7 +2906,7 @@ "kind": "section" }, { - "id": "260-queries#custom-query-url", + "id": "263-queries#custom-query-url", "title": "Queries", "searchTitle": "Custom Query URL", "sectionTitle": "Custom Query URL", @@ -2888,7 +2916,7 @@ "kind": "section" }, { - "id": "261-queries#url-patterns", + "id": "264-queries#url-patterns", "title": "Queries", "searchTitle": "URL Patterns", "sectionTitle": "URL Patterns", @@ -2898,7 +2926,7 @@ "kind": "section" }, { - "id": "262-queries#running-queries", + "id": "265-queries#running-queries", "title": "Queries", "searchTitle": "Running Queries", "sectionTitle": "Running Queries", @@ -2908,7 +2936,7 @@ "kind": "section" }, { - "id": "263-queries#reactively", + "id": "266-queries#reactively", "title": "Queries", "searchTitle": "Reactively", "sectionTitle": "Reactively", @@ -2918,7 +2946,7 @@ "kind": "section" }, { - "id": "264-queries#conditionally", + "id": "267-queries#conditionally", "title": "Queries", "searchTitle": "Conditionally", "sectionTitle": "Conditionally", @@ -2928,7 +2956,7 @@ "kind": "section" }, { - "id": "265-queries#once", + "id": "268-queries#once", "title": "Queries", "searchTitle": "Once", "sectionTitle": "Once", @@ -2938,7 +2966,7 @@ "kind": "section" }, { - "id": "266-queries#for-preloading", + "id": "269-queries#for-preloading", "title": "Queries", "searchTitle": "For Preloading", "sectionTitle": "For Preloading", @@ -2948,7 +2976,7 @@ "kind": "section" }, { - "id": "267-queries#missing-data", + "id": "270-queries#missing-data", "title": "Queries", "searchTitle": "Missing Data", "sectionTitle": "Missing Data", @@ -2958,7 +2986,7 @@ "kind": "section" }, { - "id": "268-queries#partial-data", + "id": "271-queries#partial-data", "title": "Queries", "searchTitle": "Partial Data", "sectionTitle": "Partial Data", @@ -2968,7 +2996,7 @@ "kind": "section" }, { - "id": "269-queries#handling-errors", + "id": "272-queries#handling-errors", "title": "Queries", "searchTitle": "Handling Errors", "sectionTitle": "Handling Errors", @@ -2978,7 +3006,7 @@ "kind": "section" }, { - "id": "270-queries#granular-updates", + "id": "273-queries#granular-updates", "title": "Queries", "searchTitle": "Granular Updates", "sectionTitle": "Granular Updates", @@ -2988,7 +3016,7 @@ "kind": "section" }, { - "id": "271-queries#query-caching", + "id": "274-queries#query-caching", "title": "Queries", "searchTitle": "Query Caching", "sectionTitle": "Query Caching", @@ -2998,7 +3026,7 @@ "kind": "section" }, { - "id": "272-queries#ttls", + "id": "275-queries#ttls", "title": "Queries", "searchTitle": "TTLs", "sectionTitle": "TTLs", @@ -3008,7 +3036,7 @@ "kind": "section" }, { - "id": "273-queries#ttl-defaults", + "id": "276-queries#ttl-defaults", "title": "Queries", "searchTitle": "TTL Defaults", "sectionTitle": "TTL Defaults", @@ -3018,7 +3046,7 @@ "kind": "section" }, { - "id": "274-queries#setting-different-ttls", + "id": "277-queries#setting-different-ttls", "title": "Queries", "searchTitle": "Setting Different TTLs", "sectionTitle": "Setting Different TTLs", @@ -3028,7 +3056,7 @@ "kind": "section" }, { - "id": "275-queries#why-zero-ttls-are-short", + "id": "278-queries#why-zero-ttls-are-short", "title": "Queries", "searchTitle": "Why Zero TTLs are Short", "sectionTitle": "Why Zero TTLs are Short", @@ -3038,7 +3066,7 @@ "kind": "section" }, { - "id": "276-queries#local-only-queries", + "id": "279-queries#local-only-queries", "title": "Queries", "searchTitle": "Local-Only Queries", "sectionTitle": "Local-Only Queries", @@ -3048,7 +3076,7 @@ "kind": "section" }, { - "id": "277-queries#custom-server-implementation", + "id": "280-queries#custom-server-implementation", "title": "Queries", "searchTitle": "Custom Server Implementation", "sectionTitle": "Custom Server Implementation", @@ -3058,7 +3086,7 @@ "kind": "section" }, { - "id": "278-queries#consistency", + "id": "281-queries#consistency", "title": "Queries", "searchTitle": "Consistency", "sectionTitle": "Consistency", @@ -3090,7 +3118,7 @@ "kind": "page" }, { - "id": "279-quickstart#hello-zero-solid", + "id": "282-quickstart#hello-zero-solid", "title": "Quickstart", "searchTitle": "hello-zero-solid", "sectionTitle": "hello-zero-solid", @@ -3100,7 +3128,7 @@ "kind": "section" }, { - "id": "280-quickstart#hello-zero-cf", + "id": "283-quickstart#hello-zero-cf", "title": "Quickstart", "searchTitle": "hello-zero-cf", "sectionTitle": "hello-zero-cf", @@ -3110,7 +3138,7 @@ "kind": "section" }, { - "id": "281-quickstart#hello-zero", + "id": "284-quickstart#hello-zero", "title": "Quickstart", "searchTitle": "hello-zero", "sectionTitle": "hello-zero", @@ -3155,7 +3183,7 @@ "kind": "page" }, { - "id": "282-react#setup", + "id": "285-react#setup", "title": "React", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -3165,7 +3193,7 @@ "kind": "section" }, { - "id": "283-react#usage", + "id": "286-react#usage", "title": "React", "searchTitle": "Usage", "sectionTitle": "Usage", @@ -3175,7 +3203,7 @@ "kind": "section" }, { - "id": "284-react#suspense", + "id": "287-react#suspense", "title": "React", "searchTitle": "Suspense", "sectionTitle": "Suspense", @@ -3185,7 +3213,7 @@ "kind": "section" }, { - "id": "285-react#examples", + "id": "288-react#examples", "title": "React", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -3217,7 +3245,7 @@ "kind": "page" }, { - "id": "286-release-notes/0.1#breaking-changes", + "id": "289-release-notes/0.1#breaking-changes", "title": "Zero 0.1", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -3227,7 +3255,7 @@ "kind": "section" }, { - "id": "287-release-notes/0.1#features", + "id": "290-release-notes/0.1#features", "title": "Zero 0.1", "searchTitle": "Features", "sectionTitle": "Features", @@ -3237,7 +3265,7 @@ "kind": "section" }, { - "id": "288-release-notes/0.1#source-tree-fixes", + "id": "291-release-notes/0.1#source-tree-fixes", "title": "Zero 0.1", "searchTitle": "Source tree fixes", "sectionTitle": "Source tree fixes", @@ -3273,7 +3301,7 @@ "kind": "page" }, { - "id": "289-release-notes/0.10#install", + "id": "292-release-notes/0.10#install", "title": "Zero 0.10", "searchTitle": "Install", "sectionTitle": "Install", @@ -3283,7 +3311,7 @@ "kind": "section" }, { - "id": "290-release-notes/0.10#features", + "id": "293-release-notes/0.10#features", "title": "Zero 0.10", "searchTitle": "Features", "sectionTitle": "Features", @@ -3293,7 +3321,7 @@ "kind": "section" }, { - "id": "291-release-notes/0.10#fixes", + "id": "294-release-notes/0.10#fixes", "title": "Zero 0.10", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3303,7 +3331,7 @@ "kind": "section" }, { - "id": "292-release-notes/0.10#breaking-changes", + "id": "295-release-notes/0.10#breaking-changes", "title": "Zero 0.10", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3339,7 +3367,7 @@ "kind": "page" }, { - "id": "293-release-notes/0.11#install", + "id": "296-release-notes/0.11#install", "title": "Zero 0.11", "searchTitle": "Install", "sectionTitle": "Install", @@ -3349,7 +3377,7 @@ "kind": "section" }, { - "id": "294-release-notes/0.11#features", + "id": "297-release-notes/0.11#features", "title": "Zero 0.11", "searchTitle": "Features", "sectionTitle": "Features", @@ -3359,7 +3387,7 @@ "kind": "section" }, { - "id": "295-release-notes/0.11#fixes", + "id": "298-release-notes/0.11#fixes", "title": "Zero 0.11", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3369,7 +3397,7 @@ "kind": "section" }, { - "id": "296-release-notes/0.11#breaking-changes", + "id": "299-release-notes/0.11#breaking-changes", "title": "Zero 0.11", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3405,7 +3433,7 @@ "kind": "page" }, { - "id": "297-release-notes/0.12#install", + "id": "300-release-notes/0.12#install", "title": "Zero 0.12", "searchTitle": "Install", "sectionTitle": "Install", @@ -3415,7 +3443,7 @@ "kind": "section" }, { - "id": "298-release-notes/0.12#features", + "id": "301-release-notes/0.12#features", "title": "Zero 0.12", "searchTitle": "Features", "sectionTitle": "Features", @@ -3425,7 +3453,7 @@ "kind": "section" }, { - "id": "299-release-notes/0.12#fixes", + "id": "302-release-notes/0.12#fixes", "title": "Zero 0.12", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3435,7 +3463,7 @@ "kind": "section" }, { - "id": "300-release-notes/0.12#breaking-changes", + "id": "303-release-notes/0.12#breaking-changes", "title": "Zero 0.12", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3471,7 +3499,7 @@ "kind": "page" }, { - "id": "301-release-notes/0.13#install", + "id": "304-release-notes/0.13#install", "title": "Zero 0.13", "searchTitle": "Install", "sectionTitle": "Install", @@ -3481,7 +3509,7 @@ "kind": "section" }, { - "id": "302-release-notes/0.13#features", + "id": "305-release-notes/0.13#features", "title": "Zero 0.13", "searchTitle": "Features", "sectionTitle": "Features", @@ -3491,7 +3519,7 @@ "kind": "section" }, { - "id": "303-release-notes/0.13#fixes", + "id": "306-release-notes/0.13#fixes", "title": "Zero 0.13", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3501,7 +3529,7 @@ "kind": "section" }, { - "id": "304-release-notes/0.13#breaking-changes", + "id": "307-release-notes/0.13#breaking-changes", "title": "Zero 0.13", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3537,7 +3565,7 @@ "kind": "page" }, { - "id": "305-release-notes/0.14#install", + "id": "308-release-notes/0.14#install", "title": "Zero 0.14", "searchTitle": "Install", "sectionTitle": "Install", @@ -3547,7 +3575,7 @@ "kind": "section" }, { - "id": "306-release-notes/0.14#features", + "id": "309-release-notes/0.14#features", "title": "Zero 0.14", "searchTitle": "Features", "sectionTitle": "Features", @@ -3557,7 +3585,7 @@ "kind": "section" }, { - "id": "307-release-notes/0.14#fixes", + "id": "310-release-notes/0.14#fixes", "title": "Zero 0.14", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3567,7 +3595,7 @@ "kind": "section" }, { - "id": "308-release-notes/0.14#breaking-changes", + "id": "311-release-notes/0.14#breaking-changes", "title": "Zero 0.14", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3607,7 +3635,7 @@ "kind": "page" }, { - "id": "309-release-notes/0.15#install", + "id": "312-release-notes/0.15#install", "title": "Zero 0.15", "searchTitle": "Install", "sectionTitle": "Install", @@ -3617,7 +3645,7 @@ "kind": "section" }, { - "id": "310-release-notes/0.15#upgrade-guide", + "id": "313-release-notes/0.15#upgrade-guide", "title": "Zero 0.15", "searchTitle": "Upgrade Guide", "sectionTitle": "Upgrade Guide", @@ -3627,7 +3655,7 @@ "kind": "section" }, { - "id": "311-release-notes/0.15#features", + "id": "314-release-notes/0.15#features", "title": "Zero 0.15", "searchTitle": "Features", "sectionTitle": "Features", @@ -3637,7 +3665,7 @@ "kind": "section" }, { - "id": "312-release-notes/0.15#fixes", + "id": "315-release-notes/0.15#fixes", "title": "Zero 0.15", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3647,7 +3675,7 @@ "kind": "section" }, { - "id": "313-release-notes/0.15#breaking-changes", + "id": "316-release-notes/0.15#breaking-changes", "title": "Zero 0.15", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3687,7 +3715,7 @@ "kind": "page" }, { - "id": "314-release-notes/0.16#install", + "id": "317-release-notes/0.16#install", "title": "Zero 0.16", "searchTitle": "Install", "sectionTitle": "Install", @@ -3697,7 +3725,7 @@ "kind": "section" }, { - "id": "315-release-notes/0.16#upgrading", + "id": "318-release-notes/0.16#upgrading", "title": "Zero 0.16", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -3707,7 +3735,7 @@ "kind": "section" }, { - "id": "316-release-notes/0.16#features", + "id": "319-release-notes/0.16#features", "title": "Zero 0.16", "searchTitle": "Features", "sectionTitle": "Features", @@ -3717,7 +3745,7 @@ "kind": "section" }, { - "id": "317-release-notes/0.16#fixes", + "id": "320-release-notes/0.16#fixes", "title": "Zero 0.16", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3727,7 +3755,7 @@ "kind": "section" }, { - "id": "318-release-notes/0.16#breaking-changes", + "id": "321-release-notes/0.16#breaking-changes", "title": "Zero 0.16", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3767,7 +3795,7 @@ "kind": "page" }, { - "id": "319-release-notes/0.17#install", + "id": "322-release-notes/0.17#install", "title": "Zero 0.17", "searchTitle": "Install", "sectionTitle": "Install", @@ -3777,7 +3805,7 @@ "kind": "section" }, { - "id": "320-release-notes/0.17#upgrading", + "id": "323-release-notes/0.17#upgrading", "title": "Zero 0.17", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -3787,7 +3815,7 @@ "kind": "section" }, { - "id": "321-release-notes/0.17#features", + "id": "324-release-notes/0.17#features", "title": "Zero 0.17", "searchTitle": "Features", "sectionTitle": "Features", @@ -3797,7 +3825,7 @@ "kind": "section" }, { - "id": "322-release-notes/0.17#fixes", + "id": "325-release-notes/0.17#fixes", "title": "Zero 0.17", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3807,7 +3835,7 @@ "kind": "section" }, { - "id": "323-release-notes/0.17#breaking-changes", + "id": "326-release-notes/0.17#breaking-changes", "title": "Zero 0.17", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3847,7 +3875,7 @@ "kind": "page" }, { - "id": "324-release-notes/0.18#install", + "id": "327-release-notes/0.18#install", "title": "Zero 0.18", "searchTitle": "Install", "sectionTitle": "Install", @@ -3857,7 +3885,7 @@ "kind": "section" }, { - "id": "325-release-notes/0.18#upgrading", + "id": "328-release-notes/0.18#upgrading", "title": "Zero 0.18", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -3867,7 +3895,7 @@ "kind": "section" }, { - "id": "326-release-notes/0.18#features", + "id": "329-release-notes/0.18#features", "title": "Zero 0.18", "searchTitle": "Features", "sectionTitle": "Features", @@ -3877,7 +3905,7 @@ "kind": "section" }, { - "id": "327-release-notes/0.18#fixes", + "id": "330-release-notes/0.18#fixes", "title": "Zero 0.18", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3887,7 +3915,7 @@ "kind": "section" }, { - "id": "328-release-notes/0.18#breaking-changes", + "id": "331-release-notes/0.18#breaking-changes", "title": "Zero 0.18", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3927,7 +3955,7 @@ "kind": "page" }, { - "id": "329-release-notes/0.19#install", + "id": "332-release-notes/0.19#install", "title": "Zero 0.19", "searchTitle": "Install", "sectionTitle": "Install", @@ -3937,7 +3965,7 @@ "kind": "section" }, { - "id": "330-release-notes/0.19#upgrading", + "id": "333-release-notes/0.19#upgrading", "title": "Zero 0.19", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -3947,7 +3975,7 @@ "kind": "section" }, { - "id": "331-release-notes/0.19#features", + "id": "334-release-notes/0.19#features", "title": "Zero 0.19", "searchTitle": "Features", "sectionTitle": "Features", @@ -3957,7 +3985,7 @@ "kind": "section" }, { - "id": "332-release-notes/0.19#fixes", + "id": "335-release-notes/0.19#fixes", "title": "Zero 0.19", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3967,7 +3995,7 @@ "kind": "section" }, { - "id": "333-release-notes/0.19#breaking-changes", + "id": "336-release-notes/0.19#breaking-changes", "title": "Zero 0.19", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4011,7 +4039,7 @@ "kind": "page" }, { - "id": "334-release-notes/0.2#breaking-changes", + "id": "337-release-notes/0.2#breaking-changes", "title": "Zero 0.2", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -4021,7 +4049,7 @@ "kind": "section" }, { - "id": "335-release-notes/0.2#features", + "id": "338-release-notes/0.2#features", "title": "Zero 0.2", "searchTitle": "Features", "sectionTitle": "Features", @@ -4031,7 +4059,7 @@ "kind": "section" }, { - "id": "336-release-notes/0.2#fixes", + "id": "339-release-notes/0.2#fixes", "title": "Zero 0.2", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4041,7 +4069,7 @@ "kind": "section" }, { - "id": "337-release-notes/0.2#docs", + "id": "340-release-notes/0.2#docs", "title": "Zero 0.2", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -4051,7 +4079,7 @@ "kind": "section" }, { - "id": "338-release-notes/0.2#source-tree-fixes", + "id": "341-release-notes/0.2#source-tree-fixes", "title": "Zero 0.2", "searchTitle": "Source tree fixes", "sectionTitle": "Source tree fixes", @@ -4061,7 +4089,7 @@ "kind": "section" }, { - "id": "339-release-notes/0.2#zbugs", + "id": "342-release-notes/0.2#zbugs", "title": "Zero 0.2", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -4101,7 +4129,7 @@ "kind": "page" }, { - "id": "340-release-notes/0.20#install", + "id": "343-release-notes/0.20#install", "title": "Zero 0.20", "searchTitle": "Install", "sectionTitle": "Install", @@ -4111,7 +4139,7 @@ "kind": "section" }, { - "id": "341-release-notes/0.20#upgrading", + "id": "344-release-notes/0.20#upgrading", "title": "Zero 0.20", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4121,7 +4149,7 @@ "kind": "section" }, { - "id": "342-release-notes/0.20#features", + "id": "345-release-notes/0.20#features", "title": "Zero 0.20", "searchTitle": "Features", "sectionTitle": "Features", @@ -4131,7 +4159,7 @@ "kind": "section" }, { - "id": "343-release-notes/0.20#fixes", + "id": "346-release-notes/0.20#fixes", "title": "Zero 0.20", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4141,7 +4169,7 @@ "kind": "section" }, { - "id": "344-release-notes/0.20#breaking-changes", + "id": "347-release-notes/0.20#breaking-changes", "title": "Zero 0.20", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4181,7 +4209,7 @@ "kind": "page" }, { - "id": "345-release-notes/0.21#install", + "id": "348-release-notes/0.21#install", "title": "Zero 0.21", "searchTitle": "Install", "sectionTitle": "Install", @@ -4191,7 +4219,7 @@ "kind": "section" }, { - "id": "346-release-notes/0.21#upgrading", + "id": "349-release-notes/0.21#upgrading", "title": "Zero 0.21", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4201,7 +4229,7 @@ "kind": "section" }, { - "id": "347-release-notes/0.21#features", + "id": "350-release-notes/0.21#features", "title": "Zero 0.21", "searchTitle": "Features", "sectionTitle": "Features", @@ -4211,7 +4239,7 @@ "kind": "section" }, { - "id": "348-release-notes/0.21#fixes", + "id": "351-release-notes/0.21#fixes", "title": "Zero 0.21", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4221,7 +4249,7 @@ "kind": "section" }, { - "id": "349-release-notes/0.21#breaking-changes", + "id": "352-release-notes/0.21#breaking-changes", "title": "Zero 0.21", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4273,7 +4301,7 @@ "kind": "page" }, { - "id": "350-release-notes/0.22#install", + "id": "353-release-notes/0.22#install", "title": "Zero 0.22", "searchTitle": "Install", "sectionTitle": "Install", @@ -4283,7 +4311,7 @@ "kind": "section" }, { - "id": "351-release-notes/0.22#upgrading", + "id": "354-release-notes/0.22#upgrading", "title": "Zero 0.22", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4293,7 +4321,7 @@ "kind": "section" }, { - "id": "352-release-notes/0.22#how-ttls-used-to-work", + "id": "355-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", @@ -4303,7 +4331,7 @@ "kind": "section" }, { - "id": "353-release-notes/0.22#how-ttls-work-now", + "id": "356-release-notes/0.22#how-ttls-work-now", "title": "Zero 0.22", "searchTitle": "How TTLs Work Now", "sectionTitle": "How TTLs Work Now", @@ -4313,7 +4341,7 @@ "kind": "section" }, { - "id": "354-release-notes/0.22#using-new-ttls", + "id": "357-release-notes/0.22#using-new-ttls", "title": "Zero 0.22", "searchTitle": "Using New TTLs", "sectionTitle": "Using New TTLs", @@ -4323,7 +4351,7 @@ "kind": "section" }, { - "id": "355-release-notes/0.22#features", + "id": "358-release-notes/0.22#features", "title": "Zero 0.22", "searchTitle": "Features", "sectionTitle": "Features", @@ -4333,7 +4361,7 @@ "kind": "section" }, { - "id": "356-release-notes/0.22#fixes", + "id": "359-release-notes/0.22#fixes", "title": "Zero 0.22", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4343,7 +4371,7 @@ "kind": "section" }, { - "id": "357-release-notes/0.22#breaking-changes", + "id": "360-release-notes/0.22#breaking-changes", "title": "Zero 0.22", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4387,7 +4415,7 @@ "kind": "page" }, { - "id": "358-release-notes/0.23#install", + "id": "361-release-notes/0.23#install", "title": "Zero 0.23", "searchTitle": "Install", "sectionTitle": "Install", @@ -4397,7 +4425,7 @@ "kind": "section" }, { - "id": "359-release-notes/0.23#upgrading", + "id": "362-release-notes/0.23#upgrading", "title": "Zero 0.23", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4407,7 +4435,7 @@ "kind": "section" }, { - "id": "360-release-notes/0.23#features", + "id": "363-release-notes/0.23#features", "title": "Zero 0.23", "searchTitle": "Features", "sectionTitle": "Features", @@ -4417,7 +4445,7 @@ "kind": "section" }, { - "id": "361-release-notes/0.23#fixes", + "id": "364-release-notes/0.23#fixes", "title": "Zero 0.23", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4427,7 +4455,7 @@ "kind": "section" }, { - "id": "362-release-notes/0.23#zbugs", + "id": "365-release-notes/0.23#zbugs", "title": "Zero 0.23", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -4437,7 +4465,7 @@ "kind": "section" }, { - "id": "363-release-notes/0.23#breaking-changes", + "id": "366-release-notes/0.23#breaking-changes", "title": "Zero 0.23", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4477,7 +4505,7 @@ "kind": "page" }, { - "id": "364-release-notes/0.24#installation", + "id": "367-release-notes/0.24#installation", "title": "Zero 0.24", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -4487,7 +4515,7 @@ "kind": "section" }, { - "id": "365-release-notes/0.24#features", + "id": "368-release-notes/0.24#features", "title": "Zero 0.24", "searchTitle": "Features", "sectionTitle": "Features", @@ -4497,7 +4525,7 @@ "kind": "section" }, { - "id": "366-release-notes/0.24#fixes", + "id": "369-release-notes/0.24#fixes", "title": "Zero 0.24", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4507,7 +4535,7 @@ "kind": "section" }, { - "id": "367-release-notes/0.24#breaking-changes", + "id": "370-release-notes/0.24#breaking-changes", "title": "Zero 0.24", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4517,7 +4545,7 @@ "kind": "section" }, { - "id": "368-release-notes/0.24#example-upgrades", + "id": "371-release-notes/0.24#example-upgrades", "title": "Zero 0.24", "searchTitle": "Example Upgrades", "sectionTitle": "Example Upgrades", @@ -4565,7 +4593,7 @@ "kind": "page" }, { - "id": "369-release-notes/0.25#installation", + "id": "372-release-notes/0.25#installation", "title": "Zero 0.25", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -4575,7 +4603,7 @@ "kind": "section" }, { - "id": "370-release-notes/0.25#overview", + "id": "373-release-notes/0.25#overview", "title": "Zero 0.25", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -4585,7 +4613,7 @@ "kind": "section" }, { - "id": "371-release-notes/0.25#upgrading", + "id": "374-release-notes/0.25#upgrading", "title": "Zero 0.25", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4595,7 +4623,7 @@ "kind": "section" }, { - "id": "372-release-notes/0.25#features", + "id": "375-release-notes/0.25#features", "title": "Zero 0.25", "searchTitle": "Features", "sectionTitle": "Features", @@ -4605,7 +4633,7 @@ "kind": "section" }, { - "id": "373-release-notes/0.25#performance", + "id": "376-release-notes/0.25#performance", "title": "Zero 0.25", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -4615,7 +4643,7 @@ "kind": "section" }, { - "id": "374-release-notes/0.25#fixes", + "id": "377-release-notes/0.25#fixes", "title": "Zero 0.25", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4625,7 +4653,7 @@ "kind": "section" }, { - "id": "375-release-notes/0.25#breaking-changes", + "id": "378-release-notes/0.25#breaking-changes", "title": "Zero 0.25", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4661,7 +4689,7 @@ "kind": "page" }, { - "id": "376-release-notes/0.26#installation", + "id": "379-release-notes/0.26#installation", "title": "Zero 0.26", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -4671,7 +4699,7 @@ "kind": "section" }, { - "id": "377-release-notes/0.26#features", + "id": "380-release-notes/0.26#features", "title": "Zero 0.26", "searchTitle": "Features", "sectionTitle": "Features", @@ -4681,7 +4709,7 @@ "kind": "section" }, { - "id": "378-release-notes/0.26#fixes", + "id": "381-release-notes/0.26#fixes", "title": "Zero 0.26", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4691,7 +4719,7 @@ "kind": "section" }, { - "id": "379-release-notes/0.26#breaking-changes", + "id": "382-release-notes/0.26#breaking-changes", "title": "Zero 0.26", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4735,7 +4763,7 @@ "kind": "page" }, { - "id": "380-release-notes/0.3#install", + "id": "383-release-notes/0.3#install", "title": "Zero 0.3", "searchTitle": "Install", "sectionTitle": "Install", @@ -4745,7 +4773,7 @@ "kind": "section" }, { - "id": "381-release-notes/0.3#breaking-changes", + "id": "384-release-notes/0.3#breaking-changes", "title": "Zero 0.3", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -4755,7 +4783,7 @@ "kind": "section" }, { - "id": "382-release-notes/0.3#features", + "id": "385-release-notes/0.3#features", "title": "Zero 0.3", "searchTitle": "Features", "sectionTitle": "Features", @@ -4765,7 +4793,7 @@ "kind": "section" }, { - "id": "383-release-notes/0.3#fixes", + "id": "386-release-notes/0.3#fixes", "title": "Zero 0.3", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4775,7 +4803,7 @@ "kind": "section" }, { - "id": "384-release-notes/0.3#docs", + "id": "387-release-notes/0.3#docs", "title": "Zero 0.3", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -4785,7 +4813,7 @@ "kind": "section" }, { - "id": "385-release-notes/0.3#zbugs", + "id": "388-release-notes/0.3#zbugs", "title": "Zero 0.3", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -4829,7 +4857,7 @@ "kind": "page" }, { - "id": "386-release-notes/0.4#install", + "id": "389-release-notes/0.4#install", "title": "Zero 0.4", "searchTitle": "Install", "sectionTitle": "Install", @@ -4839,7 +4867,7 @@ "kind": "section" }, { - "id": "387-release-notes/0.4#breaking-changes", + "id": "390-release-notes/0.4#breaking-changes", "title": "Zero 0.4", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -4849,7 +4877,7 @@ "kind": "section" }, { - "id": "388-release-notes/0.4#added-or--and--and-not-to-zql-documentation", + "id": "391-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).", @@ -4859,7 +4887,7 @@ "kind": "section" }, { - "id": "389-release-notes/0.4#fixes", + "id": "392-release-notes/0.4#fixes", "title": "Zero 0.4", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4869,7 +4897,7 @@ "kind": "section" }, { - "id": "390-release-notes/0.4#docs", + "id": "393-release-notes/0.4#docs", "title": "Zero 0.4", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -4879,7 +4907,7 @@ "kind": "section" }, { - "id": "391-release-notes/0.4#zbugs", + "id": "394-release-notes/0.4#zbugs", "title": "Zero 0.4", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -4923,7 +4951,7 @@ "kind": "page" }, { - "id": "392-release-notes/0.5#install", + "id": "395-release-notes/0.5#install", "title": "Zero 0.5", "searchTitle": "Install", "sectionTitle": "Install", @@ -4933,7 +4961,7 @@ "kind": "section" }, { - "id": "393-release-notes/0.5#breaking-changes", + "id": "396-release-notes/0.5#breaking-changes", "title": "Zero 0.5", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -4943,7 +4971,7 @@ "kind": "section" }, { - "id": "394-release-notes/0.5#features", + "id": "397-release-notes/0.5#features", "title": "Zero 0.5", "searchTitle": "Features", "sectionTitle": "Features", @@ -4953,7 +4981,7 @@ "kind": "section" }, { - "id": "395-release-notes/0.5#fixes", + "id": "398-release-notes/0.5#fixes", "title": "Zero 0.5", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4963,7 +4991,7 @@ "kind": "section" }, { - "id": "396-release-notes/0.5#docs", + "id": "399-release-notes/0.5#docs", "title": "Zero 0.5", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -4973,7 +5001,7 @@ "kind": "section" }, { - "id": "397-release-notes/0.5#zbugs", + "id": "400-release-notes/0.5#zbugs", "title": "Zero 0.5", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -5017,7 +5045,7 @@ "kind": "page" }, { - "id": "398-release-notes/0.6#install", + "id": "401-release-notes/0.6#install", "title": "Zero 0.6", "searchTitle": "Install", "sectionTitle": "Install", @@ -5027,7 +5055,7 @@ "kind": "section" }, { - "id": "399-release-notes/0.6#upgrade-guide", + "id": "402-release-notes/0.6#upgrade-guide", "title": "Zero 0.6", "searchTitle": "Upgrade Guide", "sectionTitle": "Upgrade Guide", @@ -5037,7 +5065,7 @@ "kind": "section" }, { - "id": "400-release-notes/0.6#breaking-changes", + "id": "403-release-notes/0.6#breaking-changes", "title": "Zero 0.6", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5047,7 +5075,7 @@ "kind": "section" }, { - "id": "401-release-notes/0.6#features", + "id": "404-release-notes/0.6#features", "title": "Zero 0.6", "searchTitle": "Features", "sectionTitle": "Features", @@ -5057,7 +5085,7 @@ "kind": "section" }, { - "id": "402-release-notes/0.6#zbugs", + "id": "405-release-notes/0.6#zbugs", "title": "Zero 0.6", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -5067,7 +5095,7 @@ "kind": "section" }, { - "id": "403-release-notes/0.6#docs", + "id": "406-release-notes/0.6#docs", "title": "Zero 0.6", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -5107,7 +5135,7 @@ "kind": "page" }, { - "id": "404-release-notes/0.7#install", + "id": "407-release-notes/0.7#install", "title": "Zero 0.7", "searchTitle": "Install", "sectionTitle": "Install", @@ -5117,7 +5145,7 @@ "kind": "section" }, { - "id": "405-release-notes/0.7#features", + "id": "408-release-notes/0.7#features", "title": "Zero 0.7", "searchTitle": "Features", "sectionTitle": "Features", @@ -5127,7 +5155,7 @@ "kind": "section" }, { - "id": "406-release-notes/0.7#breaking-changes", + "id": "409-release-notes/0.7#breaking-changes", "title": "Zero 0.7", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5137,7 +5165,7 @@ "kind": "section" }, { - "id": "407-release-notes/0.7#zbugs", + "id": "410-release-notes/0.7#zbugs", "title": "Zero 0.7", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -5147,7 +5175,7 @@ "kind": "section" }, { - "id": "408-release-notes/0.7#docs", + "id": "411-release-notes/0.7#docs", "title": "Zero 0.7", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -5183,7 +5211,7 @@ "kind": "page" }, { - "id": "409-release-notes/0.8#install", + "id": "412-release-notes/0.8#install", "title": "Zero 0.8", "searchTitle": "Install", "sectionTitle": "Install", @@ -5193,7 +5221,7 @@ "kind": "section" }, { - "id": "410-release-notes/0.8#features", + "id": "413-release-notes/0.8#features", "title": "Zero 0.8", "searchTitle": "Features", "sectionTitle": "Features", @@ -5203,7 +5231,7 @@ "kind": "section" }, { - "id": "411-release-notes/0.8#fixes", + "id": "414-release-notes/0.8#fixes", "title": "Zero 0.8", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5213,7 +5241,7 @@ "kind": "section" }, { - "id": "412-release-notes/0.8#breaking-changes", + "id": "415-release-notes/0.8#breaking-changes", "title": "Zero 0.8", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5249,7 +5277,7 @@ "kind": "page" }, { - "id": "413-release-notes/0.9#install", + "id": "416-release-notes/0.9#install", "title": "Zero 0.9", "searchTitle": "Install", "sectionTitle": "Install", @@ -5259,7 +5287,7 @@ "kind": "section" }, { - "id": "414-release-notes/0.9#features", + "id": "417-release-notes/0.9#features", "title": "Zero 0.9", "searchTitle": "Features", "sectionTitle": "Features", @@ -5269,7 +5297,7 @@ "kind": "section" }, { - "id": "415-release-notes/0.9#fixes", + "id": "418-release-notes/0.9#fixes", "title": "Zero 0.9", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5279,7 +5307,7 @@ "kind": "section" }, { - "id": "416-release-notes/0.9#breaking-changes", + "id": "419-release-notes/0.9#breaking-changes", "title": "Zero 0.9", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5319,7 +5347,7 @@ "kind": "page" }, { - "id": "417-release-notes/1.0#installation", + "id": "420-release-notes/1.0#installation", "title": "Zero 1.0", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5329,7 +5357,7 @@ "kind": "section" }, { - "id": "418-release-notes/1.0#overview", + "id": "421-release-notes/1.0#overview", "title": "Zero 1.0", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -5339,7 +5367,7 @@ "kind": "section" }, { - "id": "419-release-notes/1.0#features", + "id": "422-release-notes/1.0#features", "title": "Zero 1.0", "searchTitle": "Features", "sectionTitle": "Features", @@ -5349,7 +5377,7 @@ "kind": "section" }, { - "id": "420-release-notes/1.0#fixes", + "id": "423-release-notes/1.0#fixes", "title": "Zero 1.0", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5359,7 +5387,7 @@ "kind": "section" }, { - "id": "421-release-notes/1.0#breaking-changes", + "id": "424-release-notes/1.0#breaking-changes", "title": "Zero 1.0", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5395,7 +5423,7 @@ "kind": "page" }, { - "id": "422-release-notes/1.1#installation", + "id": "425-release-notes/1.1#installation", "title": "Zero 1.1", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5405,7 +5433,7 @@ "kind": "section" }, { - "id": "423-release-notes/1.1#features", + "id": "426-release-notes/1.1#features", "title": "Zero 1.1", "searchTitle": "Features", "sectionTitle": "Features", @@ -5415,7 +5443,7 @@ "kind": "section" }, { - "id": "424-release-notes/1.1#fixes", + "id": "427-release-notes/1.1#fixes", "title": "Zero 1.1", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5425,7 +5453,7 @@ "kind": "section" }, { - "id": "425-release-notes/1.1#breaking-changes", + "id": "428-release-notes/1.1#breaking-changes", "title": "Zero 1.1", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5465,7 +5493,7 @@ "kind": "page" }, { - "id": "426-release-notes/1.2#installation", + "id": "429-release-notes/1.2#installation", "title": "Zero 1.2", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5475,7 +5503,7 @@ "kind": "section" }, { - "id": "427-release-notes/1.2#features", + "id": "430-release-notes/1.2#features", "title": "Zero 1.2", "searchTitle": "Features", "sectionTitle": "Features", @@ -5485,7 +5513,7 @@ "kind": "section" }, { - "id": "428-release-notes/1.2#performance", + "id": "431-release-notes/1.2#performance", "title": "Zero 1.2", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -5495,7 +5523,7 @@ "kind": "section" }, { - "id": "429-release-notes/1.2#fixes", + "id": "432-release-notes/1.2#fixes", "title": "Zero 1.2", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5505,7 +5533,7 @@ "kind": "section" }, { - "id": "430-release-notes/1.2#breaking-changes", + "id": "433-release-notes/1.2#breaking-changes", "title": "Zero 1.2", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5545,7 +5573,7 @@ "kind": "page" }, { - "id": "431-release-notes/1.3#installation", + "id": "434-release-notes/1.3#installation", "title": "Zero 1.3", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5555,7 +5583,7 @@ "kind": "section" }, { - "id": "432-release-notes/1.3#features", + "id": "435-release-notes/1.3#features", "title": "Zero 1.3", "searchTitle": "Features", "sectionTitle": "Features", @@ -5565,7 +5593,7 @@ "kind": "section" }, { - "id": "433-release-notes/1.3#performance", + "id": "436-release-notes/1.3#performance", "title": "Zero 1.3", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -5575,7 +5603,7 @@ "kind": "section" }, { - "id": "434-release-notes/1.3#fixes", + "id": "437-release-notes/1.3#fixes", "title": "Zero 1.3", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5585,7 +5613,7 @@ "kind": "section" }, { - "id": "435-release-notes/1.3#breaking-changes", + "id": "438-release-notes/1.3#breaking-changes", "title": "Zero 1.3", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5633,7 +5661,7 @@ "kind": "page" }, { - "id": "436-release-notes/1.4#installation", + "id": "439-release-notes/1.4#installation", "title": "Zero 1.4", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5643,7 +5671,7 @@ "kind": "section" }, { - "id": "437-release-notes/1.4#upgrading", + "id": "440-release-notes/1.4#upgrading", "title": "Zero 1.4", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -5653,7 +5681,7 @@ "kind": "section" }, { - "id": "438-release-notes/1.4#userid-anon", + "id": "441-release-notes/1.4#userid-anon", "title": "Zero 1.4", "searchTitle": "userID: \"anon\"", "sectionTitle": "userID: \"anon\"", @@ -5663,7 +5691,7 @@ "kind": "section" }, { - "id": "439-release-notes/1.4#features", + "id": "442-release-notes/1.4#features", "title": "Zero 1.4", "searchTitle": "Features", "sectionTitle": "Features", @@ -5673,7 +5701,7 @@ "kind": "section" }, { - "id": "440-release-notes/1.4#performance", + "id": "443-release-notes/1.4#performance", "title": "Zero 1.4", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -5683,7 +5711,7 @@ "kind": "section" }, { - "id": "441-release-notes/1.4#fixes", + "id": "444-release-notes/1.4#fixes", "title": "Zero 1.4", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5693,7 +5721,7 @@ "kind": "section" }, { - "id": "442-release-notes/1.4#breaking-changes", + "id": "445-release-notes/1.4#breaking-changes", "title": "Zero 1.4", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5745,7 +5773,7 @@ "kind": "page" }, { - "id": "443-release-notes/1.5#installation", + "id": "446-release-notes/1.5#installation", "title": "Zero 1.5", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5755,7 +5783,7 @@ "kind": "section" }, { - "id": "444-release-notes/1.5#upgrading", + "id": "447-release-notes/1.5#upgrading", "title": "Zero 1.5", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -5765,7 +5793,7 @@ "kind": "section" }, { - "id": "445-release-notes/1.5#authenticated-client-groups", + "id": "448-release-notes/1.5#authenticated-client-groups", "title": "Zero 1.5", "searchTitle": "Authenticated Client Groups", "sectionTitle": "Authenticated Client Groups", @@ -5775,7 +5803,7 @@ "kind": "section" }, { - "id": "446-release-notes/1.5#deploy-order", + "id": "449-release-notes/1.5#deploy-order", "title": "Zero 1.5", "searchTitle": "Deploy Order", "sectionTitle": "Deploy Order", @@ -5785,7 +5813,7 @@ "kind": "section" }, { - "id": "447-release-notes/1.5#features", + "id": "450-release-notes/1.5#features", "title": "Zero 1.5", "searchTitle": "Features", "sectionTitle": "Features", @@ -5795,7 +5823,7 @@ "kind": "section" }, { - "id": "448-release-notes/1.5#performance", + "id": "451-release-notes/1.5#performance", "title": "Zero 1.5", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -5805,7 +5833,7 @@ "kind": "section" }, { - "id": "449-release-notes/1.5#fixes", + "id": "452-release-notes/1.5#fixes", "title": "Zero 1.5", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5815,7 +5843,7 @@ "kind": "section" }, { - "id": "450-release-notes/1.5#breaking-changes", + "id": "453-release-notes/1.5#breaking-changes", "title": "Zero 1.5", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5863,7 +5891,7 @@ "kind": "page" }, { - "id": "451-release-notes/1.6#installation", + "id": "454-release-notes/1.6#installation", "title": "Zero 1.6", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5873,7 +5901,7 @@ "kind": "section" }, { - "id": "452-release-notes/1.6#upgrading", + "id": "455-release-notes/1.6#upgrading", "title": "Zero 1.6", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -5883,7 +5911,7 @@ "kind": "section" }, { - "id": "453-release-notes/1.6#planetscale-failover", + "id": "456-release-notes/1.6#planetscale-failover", "title": "Zero 1.6", "searchTitle": "PlanetScale Failover", "sectionTitle": "PlanetScale Failover", @@ -5893,7 +5921,7 @@ "kind": "section" }, { - "id": "454-release-notes/1.6#features", + "id": "457-release-notes/1.6#features", "title": "Zero 1.6", "searchTitle": "Features", "sectionTitle": "Features", @@ -5903,7 +5931,7 @@ "kind": "section" }, { - "id": "455-release-notes/1.6#performance", + "id": "458-release-notes/1.6#performance", "title": "Zero 1.6", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -5913,7 +5941,7 @@ "kind": "section" }, { - "id": "456-release-notes/1.6#fixes", + "id": "459-release-notes/1.6#fixes", "title": "Zero 1.6", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5923,7 +5951,7 @@ "kind": "section" }, { - "id": "457-release-notes/1.6#breaking-changes", + "id": "460-release-notes/1.6#breaking-changes", "title": "Zero 1.6", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5975,7 +6003,7 @@ "kind": "page" }, { - "id": "458-release-notes/1.7#installation", + "id": "461-release-notes/1.7#installation", "title": "Zero 1.7", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5985,7 +6013,7 @@ "kind": "section" }, { - "id": "459-release-notes/1.7#overview", + "id": "462-release-notes/1.7#overview", "title": "Zero 1.7", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -5995,7 +6023,7 @@ "kind": "section" }, { - "id": "460-release-notes/1.7#features", + "id": "463-release-notes/1.7#features", "title": "Zero 1.7", "searchTitle": "Features", "sectionTitle": "Features", @@ -6005,7 +6033,7 @@ "kind": "section" }, { - "id": "461-release-notes/1.7#performance", + "id": "464-release-notes/1.7#performance", "title": "Zero 1.7", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6015,7 +6043,7 @@ "kind": "section" }, { - "id": "462-release-notes/1.7#replication", + "id": "465-release-notes/1.7#replication", "title": "Zero 1.7", "searchTitle": "Replication", "sectionTitle": "Replication", @@ -6025,7 +6053,7 @@ "kind": "section" }, { - "id": "463-release-notes/1.7#flipped-exists-queries", + "id": "466-release-notes/1.7#flipped-exists-queries", "title": "Zero 1.7", "searchTitle": "Flipped Exists Queries", "sectionTitle": "Flipped Exists Queries", @@ -6035,7 +6063,7 @@ "kind": "section" }, { - "id": "464-release-notes/1.7#fixes", + "id": "467-release-notes/1.7#fixes", "title": "Zero 1.7", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6045,7 +6073,7 @@ "kind": "section" }, { - "id": "465-release-notes/1.7#breaking-changes", + "id": "468-release-notes/1.7#breaking-changes", "title": "Zero 1.7", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6059,7 +6087,7 @@ "title": "Zero 1.8", "searchTitle": "Zero 1.8", "url": "/docs/release-notes/1.8", - "content": "Installation npm install @rocicorp/zero@1.8 You can now use zero-cache from GHCR: docker pull rocicorp/zero:1.8.0 # or docker pull ghcr.io/rocicorp/zero:1.8.0 Overview Zero 1.8 improves observability, performance, and reliability. Features Request-header forwarding: zero-cache can forward selected WebSocket upgrade headers to custom APIs using ZERO_MUTATE_ALLOWED_REQUEST_HEADERS and ZERO_QUERY_ALLOWED_REQUEST_HEADERS. (#6144, thanks @tjenkinson!) GHCR Docker images: Zero images are now published to ghcr.io/rocicorp/zero as well as Docker Hub. (#6161) Mutator result type: MutatorResult is now exported from @rocicorp/zero for typing helpers that await .client or .server. (#6223) Operational metrics: zero-cache adds metrics for API calls and startup, initial sync and replication slots, and Litestream backup and restore. (#6203, #6208, #6191, #6199, #6210) Stability metrics: New serving-lag, CVR, and WebSocket metrics and replication flow-control metrics help diagnose delayed updates, reconnects, and backpressure. (#6157, #6214, #6207) Performance Zero 1.8 speeds up replication of large transactions, maintenance of queries with orderBy() and limit(), and local ZQL queries with related(). Replicating Large Transactions Bulk imports, backfills, or migrations often change thousands of rows in a single transaction. Zero 1.8 replicates these transactions about 50% faster. Maintaining orderBy() + limit() Queries For example, an app might show the first 50 open issues, ordered by priority: zql.issue .where('workspaceID', workspaceID) .where('status', 'open') .orderBy('priority', 'desc') .orderBy('created', 'asc') .limit(50) If only a few issues are open, the 50th matching issue can be far down the orderBy() index. When changes move rows in or out of the first 50 results, Zero may need to read more rows after the last row currently returned. Previously, that read could start at the beginning of the index, even though rows before the last returned row could not be next. In 1.8, Zero starts SQLite at the last returned row's sort key, so SQLite can seek into the index and scan from there. When the last returned row was 50,000 rows into the index, incremental updates rose from 248 to 521 updates/sec. At the end of a 100,000-row index, they rose from 250 to 14,977 updates/sec. Running Local ZQL Queries When a local query first runs, Zero hydrates it from data already on the client. Zero 1.8 makes that faster, especially for queries with relationships. For example: zql.issue.related('creator').related('comments') Hydrating 500 issues with creators and comments fell from 2.54 ms to 1.97 ms. Hydrating 500 issues with creators fell from 1.11 ms to 0.84 ms. Fixes Logical replication now reconnects when the inbound Postgres stream goes silent. Postgres writes no longer use sockets after disconnection. The Drizzle adapter now handles array-mode results from Drizzle 1.0 RC prepareQuery. (thanks @typedrat!) z2s now compiles queries using start, and SQLite fetches handle null start-cursor fields. Queries no longer appear complete with stale or empty results after reconnect. React Native reads now work with op-sqlite v17. View-syncers no longer fail while the first backup is uploading or retry before a restorable backup exists on cold start. Zero Docker images now choose the correct default sync-worker count. Change-stream catch-up now respects flow control, preventing unbounded in-memory backlogs. Breaking Changes None.", + "content": "Installation npm install @rocicorp/zero@1.8 You can now use zero-cache from GHCR: docker pull rocicorp/zero:1.8.0 # or docker pull ghcr.io/rocicorp/zero:1.8.0 Overview Zero 1.8 improves observability, performance, and reliability. Features Request-header forwarding: zero-cache can forward selected WebSocket upgrade headers to custom APIs using ZERO_MUTATE_ALLOWED_REQUEST_HEADERS and ZERO_QUERY_ALLOWED_REQUEST_HEADERS. (#6144, thanks @tjenkinson!) GHCR Docker images: Zero images are now published to ghcr.io/rocicorp/zero as well as Docker Hub. (#6161) Mutator result type: MutatorResult is now exported from @rocicorp/zero for typing helpers that await .client or .server. (#6223) Operational metrics: zero-cache adds metrics for API calls and startup, initial sync and replication slots, and Litestream backup and restore. (#6203, #6208, #6191, #6199, #6210) Stability metrics: New serving-lag, CVR, and WebSocket metrics and replication flow-control metrics help diagnose delayed updates, reconnects, and backpressure. (#6157, #6214, #6207) Performance Zero 1.8 speeds up replication of large transactions, maintenance of queries that use limit(), and client-side query hydration. Replicating Large Transactions Bulk imports, backfills, or migrations often change thousands of rows in a single Postgres transaction. These large transactions replicate about 1.5x faster in Zero 1.8. Maintaining limit() Queries Consider a query like this: zql.issue .where('status', 'open') .orderBy('created', 'asc') .limit(50) Zero can fulfill this query using an index on either status or created. If it decides to use the created index, Zero might have to consider many rows before it finds 50 matches. That is unavoidable. But when changes to the data move rows in or out of the first 50 results, Zero 1.7 repeated the work to find the first 50 results, making incremental updates slower than necessary. Zero 1.8 fixes this. In benchmarks, when the last returned row was 50,000 rows into the index, incremental updates were 2x faster in Zero 1.8. When it was 100,000 rows in, updates were over 50x faster in Zero 1.8. Client-Side Hydration Zero runs queries first on the client, then on the server. The initial client-side hydration got faster in Zero 1.8. For example, this query returns initial data from client about 1.3x faster in Zero 1.8: zql.issue.related('creator').related('comments') Fixes Logical replication now reconnects when the inbound Postgres stream goes silent. Postgres writes no longer use sockets after disconnection. The Drizzle adapter now handles array-mode results from Drizzle 1.0 RC prepareQuery. (thanks @typedrat!) z2s now compiles queries using start, and SQLite fetches handle null start-cursor fields. Queries no longer appear complete with stale or empty results after reconnect. React Native reads now work with op-sqlite v17. View-syncers no longer fail while the first backup is uploading or retry before a restorable backup exists on cold start. Zero Docker images now choose the correct default sync-worker count. Change-stream catch-up now respects flow control, preventing unbounded in-memory backlogs. Breaking Changes None.", "headings": [ { "text": "Installation", @@ -6082,12 +6110,12 @@ "id": "replicating-large-transactions" }, { - "text": "Maintaining orderBy() + limit() Queries", - "id": "maintaining-orderby--limit-queries" + "text": "Maintaining limit() Queries", + "id": "maintaining-limit-queries" }, { - "text": "Running Local ZQL Queries", - "id": "running-local-zql-queries" + "text": "Client-Side Hydration", + "id": "client-side-hydration" }, { "text": "Fixes", @@ -6101,7 +6129,7 @@ "kind": "page" }, { - "id": "466-release-notes/1.8#installation", + "id": "469-release-notes/1.8#installation", "title": "Zero 1.8", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6111,7 +6139,7 @@ "kind": "section" }, { - "id": "467-release-notes/1.8#overview", + "id": "470-release-notes/1.8#overview", "title": "Zero 1.8", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6121,7 +6149,7 @@ "kind": "section" }, { - "id": "468-release-notes/1.8#features", + "id": "471-release-notes/1.8#features", "title": "Zero 1.8", "searchTitle": "Features", "sectionTitle": "Features", @@ -6131,47 +6159,47 @@ "kind": "section" }, { - "id": "469-release-notes/1.8#performance", + "id": "472-release-notes/1.8#performance", "title": "Zero 1.8", "searchTitle": "Performance", "sectionTitle": "Performance", "sectionId": "performance", "url": "/docs/release-notes/1.8", - "content": "Zero 1.8 speeds up replication of large transactions, maintenance of queries with orderBy() and limit(), and local ZQL queries with related(). Replicating Large Transactions Bulk imports, backfills, or migrations often change thousands of rows in a single transaction. Zero 1.8 replicates these transactions about 50% faster. Maintaining orderBy() + limit() Queries For example, an app might show the first 50 open issues, ordered by priority: zql.issue .where('workspaceID', workspaceID) .where('status', 'open') .orderBy('priority', 'desc') .orderBy('created', 'asc') .limit(50) If only a few issues are open, the 50th matching issue can be far down the orderBy() index. When changes move rows in or out of the first 50 results, Zero may need to read more rows after the last row currently returned. Previously, that read could start at the beginning of the index, even though rows before the last returned row could not be next. In 1.8, Zero starts SQLite at the last returned row's sort key, so SQLite can seek into the index and scan from there. When the last returned row was 50,000 rows into the index, incremental updates rose from 248 to 521 updates/sec. At the end of a 100,000-row index, they rose from 250 to 14,977 updates/sec. Running Local ZQL Queries When a local query first runs, Zero hydrates it from data already on the client. Zero 1.8 makes that faster, especially for queries with relationships. For example: zql.issue.related('creator').related('comments') Hydrating 500 issues with creators and comments fell from 2.54 ms to 1.97 ms. Hydrating 500 issues with creators fell from 1.11 ms to 0.84 ms.", + "content": "Zero 1.8 speeds up replication of large transactions, maintenance of queries that use limit(), and client-side query hydration. Replicating Large Transactions Bulk imports, backfills, or migrations often change thousands of rows in a single Postgres transaction. These large transactions replicate about 1.5x faster in Zero 1.8. Maintaining limit() Queries Consider a query like this: zql.issue .where('status', 'open') .orderBy('created', 'asc') .limit(50) Zero can fulfill this query using an index on either status or created. If it decides to use the created index, Zero might have to consider many rows before it finds 50 matches. That is unavoidable. But when changes to the data move rows in or out of the first 50 results, Zero 1.7 repeated the work to find the first 50 results, making incremental updates slower than necessary. Zero 1.8 fixes this. In benchmarks, when the last returned row was 50,000 rows into the index, incremental updates were 2x faster in Zero 1.8. When it was 100,000 rows in, updates were over 50x faster in Zero 1.8. Client-Side Hydration Zero runs queries first on the client, then on the server. The initial client-side hydration got faster in Zero 1.8. For example, this query returns initial data from client about 1.3x faster in Zero 1.8: zql.issue.related('creator').related('comments')", "kind": "section" }, { - "id": "470-release-notes/1.8#replicating-large-transactions", + "id": "473-release-notes/1.8#replicating-large-transactions", "title": "Zero 1.8", "searchTitle": "Replicating Large Transactions", "sectionTitle": "Replicating Large Transactions", "sectionId": "replicating-large-transactions", "url": "/docs/release-notes/1.8", - "content": "Bulk imports, backfills, or migrations often change thousands of rows in a single transaction. Zero 1.8 replicates these transactions about 50% faster.", + "content": "Bulk imports, backfills, or migrations often change thousands of rows in a single Postgres transaction. These large transactions replicate about 1.5x faster in Zero 1.8.", "kind": "section" }, { - "id": "471-release-notes/1.8#maintaining-orderby--limit-queries", + "id": "474-release-notes/1.8#maintaining-limit-queries", "title": "Zero 1.8", - "searchTitle": "Maintaining orderBy() + limit() Queries", - "sectionTitle": "Maintaining orderBy() + limit() Queries", - "sectionId": "maintaining-orderby--limit-queries", + "searchTitle": "Maintaining limit() Queries", + "sectionTitle": "Maintaining limit() Queries", + "sectionId": "maintaining-limit-queries", "url": "/docs/release-notes/1.8", - "content": "For example, an app might show the first 50 open issues, ordered by priority: zql.issue .where('workspaceID', workspaceID) .where('status', 'open') .orderBy('priority', 'desc') .orderBy('created', 'asc') .limit(50) If only a few issues are open, the 50th matching issue can be far down the orderBy() index. When changes move rows in or out of the first 50 results, Zero may need to read more rows after the last row currently returned. Previously, that read could start at the beginning of the index, even though rows before the last returned row could not be next. In 1.8, Zero starts SQLite at the last returned row's sort key, so SQLite can seek into the index and scan from there. When the last returned row was 50,000 rows into the index, incremental updates rose from 248 to 521 updates/sec. At the end of a 100,000-row index, they rose from 250 to 14,977 updates/sec.", + "content": "Consider a query like this: zql.issue .where('status', 'open') .orderBy('created', 'asc') .limit(50) Zero can fulfill this query using an index on either status or created. If it decides to use the created index, Zero might have to consider many rows before it finds 50 matches. That is unavoidable. But when changes to the data move rows in or out of the first 50 results, Zero 1.7 repeated the work to find the first 50 results, making incremental updates slower than necessary. Zero 1.8 fixes this. In benchmarks, when the last returned row was 50,000 rows into the index, incremental updates were 2x faster in Zero 1.8. When it was 100,000 rows in, updates were over 50x faster in Zero 1.8.", "kind": "section" }, { - "id": "472-release-notes/1.8#running-local-zql-queries", + "id": "475-release-notes/1.8#client-side-hydration", "title": "Zero 1.8", - "searchTitle": "Running Local ZQL Queries", - "sectionTitle": "Running Local ZQL Queries", - "sectionId": "running-local-zql-queries", + "searchTitle": "Client-Side Hydration", + "sectionTitle": "Client-Side Hydration", + "sectionId": "client-side-hydration", "url": "/docs/release-notes/1.8", - "content": "When a local query first runs, Zero hydrates it from data already on the client. Zero 1.8 makes that faster, especially for queries with relationships. For example: zql.issue.related('creator').related('comments') Hydrating 500 issues with creators and comments fell from 2.54 ms to 1.97 ms. Hydrating 500 issues with creators fell from 1.11 ms to 0.84 ms.", + "content": "Zero runs queries first on the client, then on the server. The initial client-side hydration got faster in Zero 1.8. For example, this query returns initial data from client about 1.3x faster in Zero 1.8: zql.issue.related('creator').related('comments')", "kind": "section" }, { - "id": "473-release-notes/1.8#fixes", + "id": "476-release-notes/1.8#fixes", "title": "Zero 1.8", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6181,7 +6209,7 @@ "kind": "section" }, { - "id": "474-release-notes/1.8#breaking-changes", + "id": "477-release-notes/1.8#breaking-changes", "title": "Zero 1.8", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6191,16 +6219,110 @@ "kind": "section" }, { - "id": "61-release-notes", + "id": "61-release-notes/1.9", + "title": "Zero 1.9", + "searchTitle": "Zero 1.9", + "url": "/docs/release-notes/1.9", + "content": "Installation npm install @rocicorp/zero@1.9 Overview Zero 1.9 improves query/mutation correctness and contains numerous reliability improvements. Features zero_sync_e2e_serving_lag measures completed replicated work from the upstream transaction commit through view-syncer poke. zero_replication_upstream_clock_skew tries to identify measurements biased by clock differences. (#6312) Zero's replication-stream inbound timeout can now be configured separately from PostgreSQL's wal_sender_timeout, avoiding unnecessary reconnects during long gaps in WAL output. (#6351, thanks @gerardatkonvo!) Performance Cold Mutation Latency Before running mutations, Zero Server fetches and caches PostgreSQL schema metadata. This is now 2.7x faster in Zero 1.9 (done in #6292, thanks @diegopereira99!). This is most noticeable with cold-starts in serverless environments like AWS Lambda. Fixes Restores now use Litestream 0.5.15 for legacy-format compatibility, retry transient failures, clean up temporary databases and staged WAL files after failed or interrupted attempts, and retain the previous snapshot generation during active restores. insert now succeeds without changing the row when its Zero primary key already exists; before 1.9, the server returned an error. Ordered queries now return correct results when cursor fields contain NULL. (thanks @YevheniiKotyrlo!) Rebuilt queries now deliver changed rows instead of occasionally leaving clients with stale results. Queries no longer drop rows or emit invalid SQL when given an inapplicable scalar hint, and scalar NOT EXISTS now handles empty or NULL results. Schema construction, CRUD mutators, and materialized views now preserve a key named __proto__ as user data. (thanks @tjenkinson!) SQLite statement caches now retain at most 1,000 idle entries each. Terminated client groups now release custom-query timers and caches. Large replica transactions can spill dirty pages to WAL instead of retaining the complete write set in native memory. Missing replication-lag reports are retried and total_lag no longer grows when reports stop arriving, while serving-lag metrics exclude disconnected or not-yet-validated client groups. zero-cache now recovers from half-open PostgreSQL sockets, including over TLS, and the official image applies the bundled postgres.js disconnect patch. With PostgreSQL wal_sender_timeout=0, replication no longer enters a continuous reconnect loop. See WAL Sender Timeout. Client connection attempts now time out across setup and the server handshake, abandon late sockets, and retry normally. Reconnect confirmations no longer produce false slow-query warnings or inflated materialization metrics. Different integration versions in a pnpm workspace no longer create peer-qualified duplicate copies of @rocicorp/zero, fixing cross-package type and module-augmentation failures. Replicated PostgreSQL type and nullability changes now preserve compound-index column order in SQLite replicas. To repair an affected replica, resync it from Postgres or recreate the PostgreSQL index. Expected schema and replica resets now log warnings instead of errors, and zero-cache skips Litestream restore when backups are not configured. (thanks @asterikx!) Server CRUD updates and upserts no longer assign primary-key columns, avoiding PostgreSQL locks that could block concurrent foreign-key inserts. (thanks @shayonj!) Mutation and query API calls now retry all 5xx responses using the existing four-attempt limit and backoff; 4xx responses still fail without retry. (thanks @shayonj!) SQLite corruption failures now log diagnostics, delete the corrupted replica before exit, and support extended corruption errors, with deeper checks available as an opt-in. Oversized replication updates now identify the transaction, affected column, and value type without logging the value. Fatal replica-writer failures now surface as replication errors and cause zero-cache to exit with a failure instead of silently stopping replication. Replication now recovers from upstream disconnects or stalled PostgreSQL writes while flow control is blocked. Mutations from multiple tabs in the same client group are no longer skipped or sent out of order.", + "headings": [ + { + "text": "Installation", + "id": "installation" + }, + { + "text": "Overview", + "id": "overview" + }, + { + "text": "Features", + "id": "features" + }, + { + "text": "Performance", + "id": "performance" + }, + { + "text": "Cold Mutation Latency", + "id": "cold-mutation-latency" + }, + { + "text": "Fixes", + "id": "fixes" + } + ], + "kind": "page" + }, + { + "id": "478-release-notes/1.9#installation", + "title": "Zero 1.9", + "searchTitle": "Installation", + "sectionTitle": "Installation", + "sectionId": "installation", + "url": "/docs/release-notes/1.9", + "content": "npm install @rocicorp/zero@1.9", + "kind": "section" + }, + { + "id": "479-release-notes/1.9#overview", + "title": "Zero 1.9", + "searchTitle": "Overview", + "sectionTitle": "Overview", + "sectionId": "overview", + "url": "/docs/release-notes/1.9", + "content": "Zero 1.9 improves query/mutation correctness and contains numerous reliability improvements.", + "kind": "section" + }, + { + "id": "480-release-notes/1.9#features", + "title": "Zero 1.9", + "searchTitle": "Features", + "sectionTitle": "Features", + "sectionId": "features", + "url": "/docs/release-notes/1.9", + "content": "zero_sync_e2e_serving_lag measures completed replicated work from the upstream transaction commit through view-syncer poke. zero_replication_upstream_clock_skew tries to identify measurements biased by clock differences. (#6312) Zero's replication-stream inbound timeout can now be configured separately from PostgreSQL's wal_sender_timeout, avoiding unnecessary reconnects during long gaps in WAL output. (#6351, thanks @gerardatkonvo!)", + "kind": "section" + }, + { + "id": "481-release-notes/1.9#performance", + "title": "Zero 1.9", + "searchTitle": "Performance", + "sectionTitle": "Performance", + "sectionId": "performance", + "url": "/docs/release-notes/1.9", + "content": "Cold Mutation Latency Before running mutations, Zero Server fetches and caches PostgreSQL schema metadata. This is now 2.7x faster in Zero 1.9 (done in #6292, thanks @diegopereira99!). This is most noticeable with cold-starts in serverless environments like AWS Lambda.", + "kind": "section" + }, + { + "id": "482-release-notes/1.9#cold-mutation-latency", + "title": "Zero 1.9", + "searchTitle": "Cold Mutation Latency", + "sectionTitle": "Cold Mutation Latency", + "sectionId": "cold-mutation-latency", + "url": "/docs/release-notes/1.9", + "content": "Before running mutations, Zero Server fetches and caches PostgreSQL schema metadata. This is now 2.7x faster in Zero 1.9 (done in #6292, thanks @diegopereira99!). This is most noticeable with cold-starts in serverless environments like AWS Lambda.", + "kind": "section" + }, + { + "id": "483-release-notes/1.9#fixes", + "title": "Zero 1.9", + "searchTitle": "Fixes", + "sectionTitle": "Fixes", + "sectionId": "fixes", + "url": "/docs/release-notes/1.9", + "content": "Restores now use Litestream 0.5.15 for legacy-format compatibility, retry transient failures, clean up temporary databases and staged WAL files after failed or interrupted attempts, and retain the previous snapshot generation during active restores. insert now succeeds without changing the row when its Zero primary key already exists; before 1.9, the server returned an error. Ordered queries now return correct results when cursor fields contain NULL. (thanks @YevheniiKotyrlo!) Rebuilt queries now deliver changed rows instead of occasionally leaving clients with stale results. Queries no longer drop rows or emit invalid SQL when given an inapplicable scalar hint, and scalar NOT EXISTS now handles empty or NULL results. Schema construction, CRUD mutators, and materialized views now preserve a key named __proto__ as user data. (thanks @tjenkinson!) SQLite statement caches now retain at most 1,000 idle entries each. Terminated client groups now release custom-query timers and caches. Large replica transactions can spill dirty pages to WAL instead of retaining the complete write set in native memory. Missing replication-lag reports are retried and total_lag no longer grows when reports stop arriving, while serving-lag metrics exclude disconnected or not-yet-validated client groups. zero-cache now recovers from half-open PostgreSQL sockets, including over TLS, and the official image applies the bundled postgres.js disconnect patch. With PostgreSQL wal_sender_timeout=0, replication no longer enters a continuous reconnect loop. See WAL Sender Timeout. Client connection attempts now time out across setup and the server handshake, abandon late sockets, and retry normally. Reconnect confirmations no longer produce false slow-query warnings or inflated materialization metrics. Different integration versions in a pnpm workspace no longer create peer-qualified duplicate copies of @rocicorp/zero, fixing cross-package type and module-augmentation failures. Replicated PostgreSQL type and nullability changes now preserve compound-index column order in SQLite replicas. To repair an affected replica, resync it from Postgres or recreate the PostgreSQL index. Expected schema and replica resets now log warnings instead of errors, and zero-cache skips Litestream restore when backups are not configured. (thanks @asterikx!) Server CRUD updates and upserts no longer assign primary-key columns, avoiding PostgreSQL locks that could block concurrent foreign-key inserts. (thanks @shayonj!) Mutation and query API calls now retry all 5xx responses using the existing four-attempt limit and backoff; 4xx responses still fail without retry. (thanks @shayonj!) SQLite corruption failures now log diagnostics, delete the corrupted replica before exit, and support extended corruption errors, with deeper checks available as an opt-in. Oversized replication updates now identify the transaction, affected column, and value type without logging the value. Fatal replica-writer failures now surface as replication errors and cause zero-cache to exit with a failure instead of silently stopping replication. Replication now recovers from upstream disconnects or stalled PostgreSQL writes while flow control is blocked. Mutations from multiple tabs in the same client group are no longer skipped or sent out of order.", + "kind": "section" + }, + { + "id": "62-release-notes", "title": "Release Notes", "searchTitle": "Release Notes", "url": "/docs/release-notes", - "content": "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.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": "62-reporting-bugs", + "id": "63-reporting-bugs", "title": "Reporting Bugs", "searchTitle": "Reporting Bugs", "url": "/docs/reporting-bugs", @@ -6218,7 +6340,7 @@ "kind": "page" }, { - "id": "475-reporting-bugs#zbugs", + "id": "484-reporting-bugs#zbugs", "title": "Reporting Bugs", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -6228,7 +6350,7 @@ "kind": "section" }, { - "id": "476-reporting-bugs#discord", + "id": "485-reporting-bugs#discord", "title": "Reporting Bugs", "searchTitle": "Discord", "sectionTitle": "Discord", @@ -6238,7 +6360,7 @@ "kind": "section" }, { - "id": "63-rest", + "id": "64-rest", "title": "REST", "searchTitle": "REST", "url": "/docs/rest", @@ -6264,7 +6386,7 @@ "kind": "page" }, { - "id": "477-rest#pattern", + "id": "486-rest#pattern", "title": "REST", "searchTitle": "Pattern", "sectionTitle": "Pattern", @@ -6274,7 +6396,7 @@ "kind": "section" }, { - "id": "478-rest#tanstack-start-example", + "id": "487-rest#tanstack-start-example", "title": "REST", "searchTitle": "TanStack Start Example", "sectionTitle": "TanStack Start Example", @@ -6284,7 +6406,7 @@ "kind": "section" }, { - "id": "479-rest#openapi-generation", + "id": "488-rest#openapi-generation", "title": "REST", "searchTitle": "OpenAPI Generation", "sectionTitle": "OpenAPI Generation", @@ -6294,7 +6416,7 @@ "kind": "section" }, { - "id": "480-rest#full-working-example", + "id": "489-rest#full-working-example", "title": "REST", "searchTitle": "Full Working Example", "sectionTitle": "Full Working Example", @@ -6304,7 +6426,7 @@ "kind": "section" }, { - "id": "64-roadmap", + "id": "65-roadmap", "title": "Roadmap", "searchTitle": "Roadmap", "url": "/docs/roadmap", @@ -6322,7 +6444,7 @@ "kind": "page" }, { - "id": "481-roadmap#q4-2025", + "id": "490-roadmap#q4-2025", "title": "Roadmap", "searchTitle": "Q4 2025", "sectionTitle": "Q4 2025", @@ -6332,7 +6454,7 @@ "kind": "section" }, { - "id": "482-roadmap#beyond", + "id": "491-roadmap#beyond", "title": "Roadmap", "searchTitle": "Beyond", "sectionTitle": "Beyond", @@ -6342,7 +6464,7 @@ "kind": "section" }, { - "id": "65-samples", + "id": "66-samples", "title": "Samples", "searchTitle": "Samples", "url": "/docs/samples", @@ -6368,7 +6490,7 @@ "kind": "page" }, { - "id": "483-samples#gigabugs", + "id": "492-samples#gigabugs", "title": "Samples", "searchTitle": "Gigabugs", "sectionTitle": "Gigabugs", @@ -6378,7 +6500,7 @@ "kind": "section" }, { - "id": "484-samples#ztunes", + "id": "493-samples#ztunes", "title": "Samples", "searchTitle": "ztunes", "sectionTitle": "ztunes", @@ -6388,7 +6510,7 @@ "kind": "section" }, { - "id": "485-samples#zslack", + "id": "494-samples#zslack", "title": "Samples", "searchTitle": "zslack", "sectionTitle": "zslack", @@ -6398,7 +6520,7 @@ "kind": "section" }, { - "id": "486-samples#zero-music", + "id": "495-samples#zero-music", "title": "Samples", "searchTitle": "zero-music", "sectionTitle": "zero-music", @@ -6408,7 +6530,7 @@ "kind": "section" }, { - "id": "66-schema", + "id": "67-schema", "title": "Zero Schema", "searchTitle": "Zero Schema", "url": "/docs/schema", @@ -6534,7 +6656,7 @@ "kind": "page" }, { - "id": "487-schema#generating-from-database", + "id": "496-schema#generating-from-database", "title": "Zero Schema", "searchTitle": "Generating from Database", "sectionTitle": "Generating from Database", @@ -6544,7 +6666,7 @@ "kind": "section" }, { - "id": "488-schema#writing-by-hand", + "id": "497-schema#writing-by-hand", "title": "Zero Schema", "searchTitle": "Writing by Hand", "sectionTitle": "Writing by Hand", @@ -6554,7 +6676,7 @@ "kind": "section" }, { - "id": "489-schema#table-schemas", + "id": "498-schema#table-schemas", "title": "Zero Schema", "searchTitle": "Table Schemas", "sectionTitle": "Table Schemas", @@ -6564,7 +6686,7 @@ "kind": "section" }, { - "id": "490-schema#name-mapping", + "id": "499-schema#name-mapping", "title": "Zero Schema", "searchTitle": "Name Mapping", "sectionTitle": "Name Mapping", @@ -6574,7 +6696,7 @@ "kind": "section" }, { - "id": "491-schema#multiple-schemas", + "id": "500-schema#multiple-schemas", "title": "Zero Schema", "searchTitle": "Multiple Schemas", "sectionTitle": "Multiple Schemas", @@ -6584,7 +6706,7 @@ "kind": "section" }, { - "id": "492-schema#optional-columns", + "id": "501-schema#optional-columns", "title": "Zero Schema", "searchTitle": "Optional Columns", "sectionTitle": "Optional Columns", @@ -6594,7 +6716,7 @@ "kind": "section" }, { - "id": "493-schema#enumerations", + "id": "502-schema#enumerations", "title": "Zero Schema", "searchTitle": "Enumerations", "sectionTitle": "Enumerations", @@ -6604,7 +6726,7 @@ "kind": "section" }, { - "id": "494-schema#custom-json-types", + "id": "503-schema#custom-json-types", "title": "Zero Schema", "searchTitle": "Custom JSON Types", "sectionTitle": "Custom JSON Types", @@ -6614,7 +6736,7 @@ "kind": "section" }, { - "id": "495-schema#compound-primary-keys", + "id": "504-schema#compound-primary-keys", "title": "Zero Schema", "searchTitle": "Compound Primary Keys", "sectionTitle": "Compound Primary Keys", @@ -6624,7 +6746,7 @@ "kind": "section" }, { - "id": "496-schema#relationships", + "id": "505-schema#relationships", "title": "Zero Schema", "searchTitle": "Relationships", "sectionTitle": "Relationships", @@ -6634,7 +6756,7 @@ "kind": "section" }, { - "id": "497-schema#many-to-many-relationships", + "id": "506-schema#many-to-many-relationships", "title": "Zero Schema", "searchTitle": "Many-to-Many Relationships", "sectionTitle": "Many-to-Many Relationships", @@ -6644,7 +6766,7 @@ "kind": "section" }, { - "id": "498-schema#compound-keys-relationships", + "id": "507-schema#compound-keys-relationships", "title": "Zero Schema", "searchTitle": "Compound Keys Relationships", "sectionTitle": "Compound Keys Relationships", @@ -6654,7 +6776,7 @@ "kind": "section" }, { - "id": "499-schema#circular-relationships", + "id": "508-schema#circular-relationships", "title": "Zero Schema", "searchTitle": "Circular Relationships", "sectionTitle": "Circular Relationships", @@ -6664,7 +6786,7 @@ "kind": "section" }, { - "id": "500-schema#database-schemas", + "id": "509-schema#database-schemas", "title": "Zero Schema", "searchTitle": "Database Schemas", "sectionTitle": "Database Schemas", @@ -6674,7 +6796,7 @@ "kind": "section" }, { - "id": "501-schema#register-schema-type", + "id": "510-schema#register-schema-type", "title": "Zero Schema", "searchTitle": "Register Schema Type", "sectionTitle": "Register Schema Type", @@ -6684,7 +6806,7 @@ "kind": "section" }, { - "id": "502-schema#schema-changes", + "id": "511-schema#schema-changes", "title": "Zero Schema", "searchTitle": "Schema Changes", "sectionTitle": "Schema Changes", @@ -6694,7 +6816,7 @@ "kind": "section" }, { - "id": "503-schema#development", + "id": "512-schema#development", "title": "Zero Schema", "searchTitle": "Development", "sectionTitle": "Development", @@ -6704,7 +6826,7 @@ "kind": "section" }, { - "id": "504-schema#production", + "id": "513-schema#production", "title": "Zero Schema", "searchTitle": "Production", "sectionTitle": "Production", @@ -6714,7 +6836,7 @@ "kind": "section" }, { - "id": "505-schema#expand-changes", + "id": "514-schema#expand-changes", "title": "Zero Schema", "searchTitle": "Expand Changes", "sectionTitle": "Expand Changes", @@ -6724,7 +6846,7 @@ "kind": "section" }, { - "id": "506-schema#contract-changes", + "id": "515-schema#contract-changes", "title": "Zero Schema", "searchTitle": "Contract Changes", "sectionTitle": "Contract Changes", @@ -6734,7 +6856,7 @@ "kind": "section" }, { - "id": "507-schema#compound-changes", + "id": "516-schema#compound-changes", "title": "Zero Schema", "searchTitle": "Compound Changes", "sectionTitle": "Compound Changes", @@ -6744,7 +6866,7 @@ "kind": "section" }, { - "id": "508-schema#examples", + "id": "517-schema#examples", "title": "Zero Schema", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -6754,7 +6876,7 @@ "kind": "section" }, { - "id": "509-schema#adding-a-column", + "id": "518-schema#adding-a-column", "title": "Zero Schema", "searchTitle": "Adding a Column", "sectionTitle": "Adding a Column", @@ -6764,7 +6886,7 @@ "kind": "section" }, { - "id": "510-schema#removing-a-column", + "id": "519-schema#removing-a-column", "title": "Zero Schema", "searchTitle": "Removing a Column", "sectionTitle": "Removing a Column", @@ -6774,7 +6896,7 @@ "kind": "section" }, { - "id": "511-schema#renaming-a-column", + "id": "520-schema#renaming-a-column", "title": "Zero Schema", "searchTitle": "Renaming a Column", "sectionTitle": "Renaming a Column", @@ -6784,7 +6906,7 @@ "kind": "section" }, { - "id": "512-schema#making-a-column-optional", + "id": "521-schema#making-a-column-optional", "title": "Zero Schema", "searchTitle": "Making a Column Optional", "sectionTitle": "Making a Column Optional", @@ -6794,7 +6916,7 @@ "kind": "section" }, { - "id": "513-schema#quick-reference", + "id": "522-schema#quick-reference", "title": "Zero Schema", "searchTitle": "Quick Reference", "sectionTitle": "Quick Reference", @@ -6804,7 +6926,7 @@ "kind": "section" }, { - "id": "514-schema#backfill", + "id": "523-schema#backfill", "title": "Zero Schema", "searchTitle": "Backfill", "sectionTitle": "Backfill", @@ -6814,7 +6936,7 @@ "kind": "section" }, { - "id": "515-schema#monitoring-backfill-progress", + "id": "524-schema#monitoring-backfill-progress", "title": "Zero Schema", "searchTitle": "Monitoring Backfill Progress", "sectionTitle": "Monitoring Backfill Progress", @@ -6824,7 +6946,7 @@ "kind": "section" }, { - "id": "67-self-host", + "id": "68-self-host", "title": "Self-Hosting Zero", "searchTitle": "Self-Hosting Zero", "url": "/docs/self-host", @@ -6886,7 +7008,7 @@ "kind": "page" }, { - "id": "516-self-host#docker-images", + "id": "525-self-host#docker-images", "title": "Self-Hosting Zero", "searchTitle": "Docker Images", "sectionTitle": "Docker Images", @@ -6896,7 +7018,7 @@ "kind": "section" }, { - "id": "517-self-host#minimum-viable-strategy", + "id": "526-self-host#minimum-viable-strategy", "title": "Self-Hosting Zero", "searchTitle": "Minimum Viable Strategy", "sectionTitle": "Minimum Viable Strategy", @@ -6906,7 +7028,7 @@ "kind": "section" }, { - "id": "518-self-host#maximal-strategy", + "id": "527-self-host#maximal-strategy", "title": "Self-Hosting Zero", "searchTitle": "Maximal Strategy", "sectionTitle": "Maximal Strategy", @@ -6916,7 +7038,7 @@ "kind": "section" }, { - "id": "519-self-host#replica-lifecycle", + "id": "528-self-host#replica-lifecycle", "title": "Self-Hosting Zero", "searchTitle": "Replica Lifecycle", "sectionTitle": "Replica Lifecycle", @@ -6926,7 +7048,7 @@ "kind": "section" }, { - "id": "520-self-host#performance", + "id": "529-self-host#performance", "title": "Self-Hosting Zero", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6936,7 +7058,7 @@ "kind": "section" }, { - "id": "521-self-host#hydration", + "id": "530-self-host#hydration", "title": "Self-Hosting Zero", "searchTitle": "Hydration", "sectionTitle": "Hydration", @@ -6946,7 +7068,7 @@ "kind": "section" }, { - "id": "522-self-host#ivm-advancement", + "id": "531-self-host#ivm-advancement", "title": "Self-Hosting Zero", "searchTitle": "IVM advancement", "sectionTitle": "IVM advancement", @@ -6956,7 +7078,7 @@ "kind": "section" }, { - "id": "523-self-host#system-level", + "id": "532-self-host#system-level", "title": "Self-Hosting Zero", "searchTitle": "System-level", "sectionTitle": "System-level", @@ -6966,7 +7088,7 @@ "kind": "section" }, { - "id": "524-self-host#networking", + "id": "533-self-host#networking", "title": "Self-Hosting Zero", "searchTitle": "Networking", "sectionTitle": "Networking", @@ -6976,7 +7098,7 @@ "kind": "section" }, { - "id": "525-self-host#sticky-sessions", + "id": "534-self-host#sticky-sessions", "title": "Self-Hosting Zero", "searchTitle": "Sticky Sessions", "sectionTitle": "Sticky Sessions", @@ -6986,7 +7108,7 @@ "kind": "section" }, { - "id": "526-self-host#rolling-updates", + "id": "535-self-host#rolling-updates", "title": "Self-Hosting Zero", "searchTitle": "Rolling Updates", "sectionTitle": "Rolling Updates", @@ -6996,7 +7118,7 @@ "kind": "section" }, { - "id": "527-self-host#clientserver-version-compatibility", + "id": "536-self-host#clientserver-version-compatibility", "title": "Self-Hosting Zero", "searchTitle": "Client/Server Version Compatibility", "sectionTitle": "Client/Server Version Compatibility", @@ -7006,7 +7128,7 @@ "kind": "section" }, { - "id": "528-self-host#configuration", + "id": "537-self-host#configuration", "title": "Self-Hosting Zero", "searchTitle": "Configuration", "sectionTitle": "Configuration", @@ -7016,7 +7138,7 @@ "kind": "section" }, { - "id": "68-server-zql", + "id": "69-server-zql", "title": "ZQL on the Server", "searchTitle": "ZQL on the Server", "url": "/docs/server-zql", @@ -7042,7 +7164,7 @@ "kind": "page" }, { - "id": "529-server-zql#creating-a-database", + "id": "538-server-zql#creating-a-database", "title": "ZQL on the Server", "searchTitle": "Creating a Database", "sectionTitle": "Creating a Database", @@ -7052,7 +7174,7 @@ "kind": "section" }, { - "id": "530-server-zql#custom-database", + "id": "539-server-zql#custom-database", "title": "ZQL on the Server", "searchTitle": "Custom Database", "sectionTitle": "Custom Database", @@ -7062,7 +7184,7 @@ "kind": "section" }, { - "id": "531-server-zql#running-zql", + "id": "540-server-zql#running-zql", "title": "ZQL on the Server", "searchTitle": "Running ZQL", "sectionTitle": "Running ZQL", @@ -7072,7 +7194,7 @@ "kind": "section" }, { - "id": "532-server-zql#ssr", + "id": "541-server-zql#ssr", "title": "ZQL on the Server", "searchTitle": "SSR", "sectionTitle": "SSR", @@ -7082,7 +7204,7 @@ "kind": "section" }, { - "id": "69-solidjs", + "id": "70-solidjs", "title": "SolidJS", "searchTitle": "SolidJS", "url": "/docs/solidjs", @@ -7104,7 +7226,7 @@ "kind": "page" }, { - "id": "533-solidjs#setup", + "id": "542-solidjs#setup", "title": "SolidJS", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -7114,7 +7236,7 @@ "kind": "section" }, { - "id": "534-solidjs#usage", + "id": "543-solidjs#usage", "title": "SolidJS", "searchTitle": "Usage", "sectionTitle": "Usage", @@ -7124,7 +7246,7 @@ "kind": "section" }, { - "id": "535-solidjs#examples", + "id": "544-solidjs#examples", "title": "SolidJS", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -7134,7 +7256,7 @@ "kind": "section" }, { - "id": "70-status", + "id": "71-status", "title": "Project Status", "searchTitle": "Project Status", "url": "/docs/status", @@ -7160,7 +7282,7 @@ "kind": "page" }, { - "id": "536-status#breaking-changes", + "id": "545-status#breaking-changes", "title": "Project Status", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -7170,7 +7292,7 @@ "kind": "section" }, { - "id": "537-status#roadmap", + "id": "546-status#roadmap", "title": "Project Status", "searchTitle": "Roadmap", "sectionTitle": "Roadmap", @@ -7180,7 +7302,7 @@ "kind": "section" }, { - "id": "538-status#2026", + "id": "547-status#2026", "title": "Project Status", "searchTitle": "2026", "sectionTitle": "2026", @@ -7190,7 +7312,7 @@ "kind": "section" }, { - "id": "539-status#soon", + "id": "548-status#soon", "title": "Project Status", "searchTitle": "Soon", "sectionTitle": "Soon", @@ -7200,7 +7322,7 @@ "kind": "section" }, { - "id": "71-sync", + "id": "72-sync", "title": "What is Sync?", "searchTitle": "What is Sync?", "url": "/docs/sync", @@ -7222,7 +7344,7 @@ "kind": "page" }, { - "id": "540-sync#problem", + "id": "549-sync#problem", "title": "What is Sync?", "searchTitle": "Problem", "sectionTitle": "Problem", @@ -7232,7 +7354,7 @@ "kind": "section" }, { - "id": "541-sync#solution", + "id": "550-sync#solution", "title": "What is Sync?", "searchTitle": "Solution", "sectionTitle": "Solution", @@ -7242,7 +7364,7 @@ "kind": "section" }, { - "id": "542-sync#history-of-sync", + "id": "551-sync#history-of-sync", "title": "What is Sync?", "searchTitle": "History of Sync", "sectionTitle": "History of Sync", @@ -7252,7 +7374,7 @@ "kind": "section" }, { - "id": "72-tutorial", + "id": "73-tutorial", "title": "Tutorial", "searchTitle": "Tutorial", "url": "/docs/tutorial", @@ -7326,7 +7448,7 @@ "kind": "page" }, { - "id": "543-tutorial#setup", + "id": "552-tutorial#setup", "title": "Tutorial", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -7336,7 +7458,7 @@ "kind": "section" }, { - "id": "544-tutorial#create-a-project", + "id": "553-tutorial#create-a-project", "title": "Tutorial", "searchTitle": "Create a Project", "sectionTitle": "Create a Project", @@ -7346,7 +7468,7 @@ "kind": "section" }, { - "id": "545-tutorial#set-up-your-database", + "id": "554-tutorial#set-up-your-database", "title": "Tutorial", "searchTitle": "Set Up Your Database", "sectionTitle": "Set Up Your Database", @@ -7356,7 +7478,7 @@ "kind": "section" }, { - "id": "546-tutorial#install-and-run-zero-cache", + "id": "555-tutorial#install-and-run-zero-cache", "title": "Tutorial", "searchTitle": "Install and Run Zero-Cache", "sectionTitle": "Install and Run Zero-Cache", @@ -7366,7 +7488,7 @@ "kind": "section" }, { - "id": "547-tutorial#integrate-zero", + "id": "556-tutorial#integrate-zero", "title": "Tutorial", "searchTitle": "Integrate Zero", "sectionTitle": "Integrate Zero", @@ -7376,7 +7498,7 @@ "kind": "section" }, { - "id": "548-tutorial#set-up-your-zero-schema", + "id": "557-tutorial#set-up-your-zero-schema", "title": "Tutorial", "searchTitle": "Set Up Your Zero Schema", "sectionTitle": "Set Up Your Zero Schema", @@ -7386,7 +7508,7 @@ "kind": "section" }, { - "id": "549-tutorial#set-up-the-zero-client", + "id": "558-tutorial#set-up-the-zero-client", "title": "Tutorial", "searchTitle": "Set Up the Zero Client", "sectionTitle": "Set Up the Zero Client", @@ -7396,7 +7518,7 @@ "kind": "section" }, { - "id": "550-tutorial#sync-data", + "id": "559-tutorial#sync-data", "title": "Tutorial", "searchTitle": "Sync Data", "sectionTitle": "Sync Data", @@ -7406,7 +7528,7 @@ "kind": "section" }, { - "id": "551-tutorial#define-query", + "id": "560-tutorial#define-query", "title": "Tutorial", "searchTitle": "Define Query", "sectionTitle": "Define Query", @@ -7416,7 +7538,7 @@ "kind": "section" }, { - "id": "552-tutorial#add-query-endpoint", + "id": "561-tutorial#add-query-endpoint", "title": "Tutorial", "searchTitle": "Add Query Endpoint", "sectionTitle": "Add Query Endpoint", @@ -7426,7 +7548,7 @@ "kind": "section" }, { - "id": "553-tutorial#invoke-query", + "id": "562-tutorial#invoke-query", "title": "Tutorial", "searchTitle": "Invoke Query", "sectionTitle": "Invoke Query", @@ -7436,7 +7558,7 @@ "kind": "section" }, { - "id": "554-tutorial#mutate-data", + "id": "563-tutorial#mutate-data", "title": "Tutorial", "searchTitle": "Mutate Data", "sectionTitle": "Mutate Data", @@ -7446,7 +7568,7 @@ "kind": "section" }, { - "id": "555-tutorial#define-mutators", + "id": "564-tutorial#define-mutators", "title": "Tutorial", "searchTitle": "Define Mutators", "sectionTitle": "Define Mutators", @@ -7456,7 +7578,7 @@ "kind": "section" }, { - "id": "556-tutorial#add-mutate-endpoint", + "id": "565-tutorial#add-mutate-endpoint", "title": "Tutorial", "searchTitle": "Add Mutate Endpoint", "sectionTitle": "Add Mutate Endpoint", @@ -7466,7 +7588,7 @@ "kind": "section" }, { - "id": "557-tutorial#invoke-mutators", + "id": "566-tutorial#invoke-mutators", "title": "Tutorial", "searchTitle": "Invoke Mutators", "sectionTitle": "Invoke Mutators", @@ -7476,7 +7598,7 @@ "kind": "section" }, { - "id": "558-tutorial#next-steps", + "id": "567-tutorial#next-steps", "title": "Tutorial", "searchTitle": "Next Steps", "sectionTitle": "Next Steps", @@ -7486,7 +7608,7 @@ "kind": "section" }, { - "id": "73-when-to-use", + "id": "74-when-to-use", "title": "When To Use Zero", "searchTitle": "When To Use Zero", "url": "/docs/when-to-use", @@ -7552,7 +7674,7 @@ "kind": "page" }, { - "id": "559-when-to-use#zero-might-be-a-good-fit", + "id": "568-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", @@ -7562,7 +7684,7 @@ "kind": "section" }, { - "id": "560-when-to-use#you-want-to-sync-only-a-small-subset-of-data-to-client", + "id": "569-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", @@ -7572,7 +7694,7 @@ "kind": "section" }, { - "id": "561-when-to-use#you-need-fine-grained-read-or-write-permissions", + "id": "570-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", @@ -7582,7 +7704,7 @@ "kind": "section" }, { - "id": "562-when-to-use#you-are-building-a-traditional-client-server-web-app", + "id": "571-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", @@ -7592,7 +7714,7 @@ "kind": "section" }, { - "id": "563-when-to-use#you-use-postgresql", + "id": "572-when-to-use#you-use-postgresql", "title": "When To Use Zero", "searchTitle": "You use PostgreSQL", "sectionTitle": "You use PostgreSQL", @@ -7602,7 +7724,7 @@ "kind": "section" }, { - "id": "564-when-to-use#your-app-is-broadly-like-linear", + "id": "573-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\"", @@ -7612,7 +7734,7 @@ "kind": "section" }, { - "id": "565-when-to-use#interaction-performance-is-very-important-to-you", + "id": "574-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", @@ -7622,7 +7744,7 @@ "kind": "section" }, { - "id": "566-when-to-use#zero-might-not-be-a-good-fit", + "id": "575-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", @@ -7632,7 +7754,7 @@ "kind": "section" }, { - "id": "567-when-to-use#you-need-the-privacy-or-data-ownership-benefits-of-local-first", + "id": "576-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", @@ -7642,7 +7764,7 @@ "kind": "section" }, { - "id": "568-when-to-use#you-need-to-support-offline-writes-or-long-periods-offline", + "id": "577-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", @@ -7652,7 +7774,7 @@ "kind": "section" }, { - "id": "569-when-to-use#you-are-building-a-native-mobile-app", + "id": "578-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", @@ -7662,7 +7784,7 @@ "kind": "section" }, { - "id": "570-when-to-use#the-total-backend-dataset-is--100gb", + "id": "579-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", @@ -7672,7 +7794,7 @@ "kind": "section" }, { - "id": "571-when-to-use#zero-might-not-be-a-good-fit-yet", + "id": "580-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", @@ -7682,7 +7804,7 @@ "kind": "section" }, { - "id": "572-when-to-use#alternatives", + "id": "581-when-to-use#alternatives", "title": "When To Use Zero", "searchTitle": "Alternatives", "sectionTitle": "Alternatives", @@ -7692,11 +7814,11 @@ "kind": "section" }, { - "id": "74-zero-cache-config", + "id": "75-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 Max Connections The maximum number of connections to open to the change database. This is used by the change-streamer for catching up zero-cache replication subscriptions. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS default: 5 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 Padding Seconds During periodic flow control checks (every 64kb), this is the amount of time to wait after the majority of subscribers have acked, after which replication continues even if some subscribers have yet to ack. This is not a timeout for the entire send; it starts only after the majority of receivers have acked. This allows a bounded amount of time for backlogged subscribers to catch up on each flush without forcing all subscribers to wait for the entire backlog to be processed. It is also useful for mitigating the effect of unresponsive subscribers due to severed WebSocket connections until liveness checks disconnect them. Set this to a negative number to disable early flow control releases. flag: --change-streamer-flow-control-consensus-padding-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_PADDING_SECONDS default: 1 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. 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. Smaller thresholds may improve read performance, at the expense of creating more files to download when restoring the replica from the backup. 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 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 Incremental Backup Interval Minutes The interval between incremental 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 litestream restore. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES 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. 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). 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. This improves restore time at the expense of bandwidth. Applications with a large database and low write rate can increase this interval to reduce network usage for backups (litestream defaults to 24 hours). 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 and Failover. 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. Because replication lag reports are only issued after the previous one was received, the actual interval between reports may be longer when there is a backlog in the replication stream. 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 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 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. 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 Max Connections The maximum number of connections to open to the change database. This is used by the change-streamer for catching up zero-cache replication subscriptions. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS default: 5 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 Padding Seconds During periodic flow control checks (every 64kb), this is the amount of time to wait after the majority of subscribers have acked, after which replication continues even if some subscribers have yet to ack. This is not a timeout for the entire send; it starts only after the majority of receivers have acked. This allows a bounded amount of time for backlogged subscribers to catch up on each flush without forcing all subscribers to wait for the entire backlog to be processed. It is also useful for mitigating the effect of unresponsive subscribers due to severed WebSocket connections until liveness checks disconnect them. Set this to a negative number to disable early flow control releases. flag: --change-streamer-flow-control-consensus-padding-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_PADDING_SECONDS default: 1 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. 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. Smaller thresholds may improve read performance, at the expense of creating more files to download when restoring the replica from the backup. 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 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 restores when ZERO_LITESTREAM_RESTORE_USING_V5 is enabled. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. The official Zero Docker image includes Litestream 0.5.15 at this path. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores when that executable is configured. If it is unavailable, Zero falls back to the legacy executable. Set this to false to force legacy restore behavior. 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 makes rollback difficult because older versions cannot restore an LTX-only backup. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: false Litestream Incremental Backup Interval Minutes The interval between incremental 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 litestream restore. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES 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. 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). 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. Applications with a large database and low write rate can increase this interval to reduce network usage for backups (litestream defaults to 24 hours). 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 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", @@ -7826,6 +7948,18 @@ "text": "Litestream Executable", "id": "litestream-executable" }, + { + "text": "Litestream V5 Executable", + "id": "litestream-v5-executable" + }, + { + "text": "Litestream Restore Using V5", + "id": "litestream-restore-using-v5" + }, + { + "text": "Litestream Backup Using V5", + "id": "litestream-backup-using-v5" + }, { "text": "Litestream Incremental Backup Interval Minutes", "id": "litestream-incremental-backup-interval-minutes" @@ -7998,6 +8132,10 @@ "text": "Upstream PG Replication Slot Failover", "id": "upstream-pg-replication-slot-failover" }, + { + "text": "Upstream PG Stream Inbound Timeout", + "id": "upstream-pg-stream-inbound-timeout" + }, { "text": "Websocket Compression", "id": "websocket-compression" @@ -8034,7 +8172,7 @@ "kind": "page" }, { - "id": "573-zero-cache-config#required-flags", + "id": "582-zero-cache-config#required-flags", "title": "zero-cache Config", "searchTitle": "Required Flags", "sectionTitle": "Required Flags", @@ -8044,7 +8182,7 @@ "kind": "section" }, { - "id": "574-zero-cache-config#upstream-db", + "id": "583-zero-cache-config#upstream-db", "title": "zero-cache Config", "searchTitle": "Upstream DB", "sectionTitle": "Upstream DB", @@ -8054,7 +8192,7 @@ "kind": "section" }, { - "id": "575-zero-cache-config#admin-password", + "id": "584-zero-cache-config#admin-password", "title": "zero-cache Config", "searchTitle": "Admin Password", "sectionTitle": "Admin Password", @@ -8064,17 +8202,17 @@ "kind": "section" }, { - "id": "576-zero-cache-config#optional-flags", + "id": "585-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 Max Connections The maximum number of connections to open to the change database. This is used by the change-streamer for catching up zero-cache replication subscriptions. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS default: 5 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 Padding Seconds During periodic flow control checks (every 64kb), this is the amount of time to wait after the majority of subscribers have acked, after which replication continues even if some subscribers have yet to ack. This is not a timeout for the entire send; it starts only after the majority of receivers have acked. This allows a bounded amount of time for backlogged subscribers to catch up on each flush without forcing all subscribers to wait for the entire backlog to be processed. It is also useful for mitigating the effect of unresponsive subscribers due to severed WebSocket connections until liveness checks disconnect them. Set this to a negative number to disable early flow control releases. flag: --change-streamer-flow-control-consensus-padding-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_PADDING_SECONDS default: 1 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. 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. Smaller thresholds may improve read performance, at the expense of creating more files to download when restoring the replica from the backup. 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 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 Incremental Backup Interval Minutes The interval between incremental 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 litestream restore. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES 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. 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). 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. This improves restore time at the expense of bandwidth. Applications with a large database and low write rate can increase this interval to reduce network usage for backups (litestream defaults to 24 hours). 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 and Failover. 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. Because replication lag reports are only issued after the previous one was received, the actual interval between reports may be longer when there is a backlog in the replication stream. 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 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": "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 Max Connections The maximum number of connections to open to the change database. This is used by the change-streamer for catching up zero-cache replication subscriptions. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS default: 5 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 Padding Seconds During periodic flow control checks (every 64kb), this is the amount of time to wait after the majority of subscribers have acked, after which replication continues even if some subscribers have yet to ack. This is not a timeout for the entire send; it starts only after the majority of receivers have acked. This allows a bounded amount of time for backlogged subscribers to catch up on each flush without forcing all subscribers to wait for the entire backlog to be processed. It is also useful for mitigating the effect of unresponsive subscribers due to severed WebSocket connections until liveness checks disconnect them. Set this to a negative number to disable early flow control releases. flag: --change-streamer-flow-control-consensus-padding-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_PADDING_SECONDS default: 1 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. 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. Smaller thresholds may improve read performance, at the expense of creating more files to download when restoring the replica from the backup. 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 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 restores when ZERO_LITESTREAM_RESTORE_USING_V5 is enabled. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. The official Zero Docker image includes Litestream 0.5.15 at this path. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores when that executable is configured. If it is unavailable, Zero falls back to the legacy executable. Set this to false to force legacy restore behavior. 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 makes rollback difficult because older versions cannot restore an LTX-only backup. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: false Litestream Incremental Backup Interval Minutes The interval between incremental 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 litestream restore. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES 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. 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). 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. Applications with a large database and low write rate can increase this interval to reduce network usage for backups (litestream defaults to 24 hours). 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", "kind": "section" }, { - "id": "577-zero-cache-config#app-id", + "id": "586-zero-cache-config#app-id", "title": "zero-cache Config", "searchTitle": "App ID", "sectionTitle": "App ID", @@ -8084,7 +8222,7 @@ "kind": "section" }, { - "id": "578-zero-cache-config#app-publications", + "id": "587-zero-cache-config#app-publications", "title": "zero-cache Config", "searchTitle": "App Publications", "sectionTitle": "App Publications", @@ -8094,7 +8232,7 @@ "kind": "section" }, { - "id": "579-zero-cache-config#auth-revalidate-interval-seconds", + "id": "588-zero-cache-config#auth-revalidate-interval-seconds", "title": "zero-cache Config", "searchTitle": "Auth Revalidate Interval Seconds", "sectionTitle": "Auth Revalidate Interval Seconds", @@ -8104,7 +8242,7 @@ "kind": "section" }, { - "id": "580-zero-cache-config#auth-retransform-interval-seconds", + "id": "589-zero-cache-config#auth-retransform-interval-seconds", "title": "zero-cache Config", "searchTitle": "Auth Retransform Interval Seconds", "sectionTitle": "Auth Retransform Interval Seconds", @@ -8114,7 +8252,7 @@ "kind": "section" }, { - "id": "581-zero-cache-config#auto-reset", + "id": "590-zero-cache-config#auto-reset", "title": "zero-cache Config", "searchTitle": "Auto Reset", "sectionTitle": "Auto Reset", @@ -8124,7 +8262,7 @@ "kind": "section" }, { - "id": "582-zero-cache-config#change-db", + "id": "591-zero-cache-config#change-db", "title": "zero-cache Config", "searchTitle": "Change DB", "sectionTitle": "Change DB", @@ -8134,7 +8272,7 @@ "kind": "section" }, { - "id": "583-zero-cache-config#change-max-connections", + "id": "592-zero-cache-config#change-max-connections", "title": "zero-cache Config", "searchTitle": "Change Max Connections", "sectionTitle": "Change Max Connections", @@ -8144,7 +8282,7 @@ "kind": "section" }, { - "id": "584-zero-cache-config#change-streamer-back-pressure-limit-heap-proportion", + "id": "593-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", @@ -8154,7 +8292,7 @@ "kind": "section" }, { - "id": "585-zero-cache-config#change-streamer-flow-control-consensus-padding-seconds", + "id": "594-zero-cache-config#change-streamer-flow-control-consensus-padding-seconds", "title": "zero-cache Config", "searchTitle": "Change Streamer Flow Control Consensus Padding Seconds", "sectionTitle": "Change Streamer Flow Control Consensus Padding Seconds", @@ -8164,7 +8302,7 @@ "kind": "section" }, { - "id": "586-zero-cache-config#change-streamer-mode", + "id": "595-zero-cache-config#change-streamer-mode", "title": "zero-cache Config", "searchTitle": "Change Streamer Mode", "sectionTitle": "Change Streamer Mode", @@ -8174,7 +8312,7 @@ "kind": "section" }, { - "id": "587-zero-cache-config#change-streamer-port", + "id": "596-zero-cache-config#change-streamer-port", "title": "zero-cache Config", "searchTitle": "Change Streamer Port", "sectionTitle": "Change Streamer Port", @@ -8184,7 +8322,7 @@ "kind": "section" }, { - "id": "588-zero-cache-config#change-streamer-startup-delay-ms", + "id": "597-zero-cache-config#change-streamer-startup-delay-ms", "title": "zero-cache Config", "searchTitle": "Change Streamer Startup Delay (ms)", "sectionTitle": "Change Streamer Startup Delay (ms)", @@ -8194,7 +8332,7 @@ "kind": "section" }, { - "id": "589-zero-cache-config#change-streamer-uri", + "id": "598-zero-cache-config#change-streamer-uri", "title": "zero-cache Config", "searchTitle": "Change Streamer URI", "sectionTitle": "Change Streamer URI", @@ -8204,7 +8342,7 @@ "kind": "section" }, { - "id": "590-zero-cache-config#cvr-db", + "id": "599-zero-cache-config#cvr-db", "title": "zero-cache Config", "searchTitle": "CVR DB", "sectionTitle": "CVR DB", @@ -8214,7 +8352,7 @@ "kind": "section" }, { - "id": "591-zero-cache-config#cvr-garbage-collection-inactivity-threshold-hours", + "id": "600-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", @@ -8224,7 +8362,7 @@ "kind": "section" }, { - "id": "592-zero-cache-config#cvr-garbage-collection-initial-batch-size", + "id": "601-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", @@ -8234,7 +8372,7 @@ "kind": "section" }, { - "id": "593-zero-cache-config#cvr-garbage-collection-initial-interval-seconds", + "id": "602-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", @@ -8244,7 +8382,7 @@ "kind": "section" }, { - "id": "594-zero-cache-config#cvr-max-connections", + "id": "603-zero-cache-config#cvr-max-connections", "title": "zero-cache Config", "searchTitle": "CVR Max Connections", "sectionTitle": "CVR Max Connections", @@ -8254,7 +8392,7 @@ "kind": "section" }, { - "id": "595-zero-cache-config#enable-query-planner", + "id": "604-zero-cache-config#enable-query-planner", "title": "zero-cache Config", "searchTitle": "Enable Query Planner", "sectionTitle": "Enable Query Planner", @@ -8264,7 +8402,7 @@ "kind": "section" }, { - "id": "596-zero-cache-config#enable-crud-mutations", + "id": "605-zero-cache-config#enable-crud-mutations", "title": "zero-cache Config", "searchTitle": "Enable CRUD Mutations", "sectionTitle": "Enable CRUD Mutations", @@ -8274,7 +8412,7 @@ "kind": "section" }, { - "id": "597-zero-cache-config#enable-telemetry", + "id": "606-zero-cache-config#enable-telemetry", "title": "zero-cache Config", "searchTitle": "Enable Telemetry", "sectionTitle": "Enable Telemetry", @@ -8284,7 +8422,7 @@ "kind": "section" }, { - "id": "598-zero-cache-config#initial-sync-table-copy-workers", + "id": "607-zero-cache-config#initial-sync-table-copy-workers", "title": "zero-cache Config", "searchTitle": "Initial Sync Table Copy Workers", "sectionTitle": "Initial Sync Table Copy Workers", @@ -8294,7 +8432,7 @@ "kind": "section" }, { - "id": "599-zero-cache-config#lazy-startup", + "id": "608-zero-cache-config#lazy-startup", "title": "zero-cache Config", "searchTitle": "Lazy Startup", "sectionTitle": "Lazy Startup", @@ -8304,7 +8442,7 @@ "kind": "section" }, { - "id": "600-zero-cache-config#litestream-backup-url", + "id": "609-zero-cache-config#litestream-backup-url", "title": "zero-cache Config", "searchTitle": "Litestream Backup URL", "sectionTitle": "Litestream Backup URL", @@ -8314,7 +8452,7 @@ "kind": "section" }, { - "id": "601-zero-cache-config#litestream-endpoint", + "id": "610-zero-cache-config#litestream-endpoint", "title": "zero-cache Config", "searchTitle": "Litestream Endpoint", "sectionTitle": "Litestream Endpoint", @@ -8324,7 +8462,7 @@ "kind": "section" }, { - "id": "602-zero-cache-config#litestream-checkpoint-threshold-mb", + "id": "611-zero-cache-config#litestream-checkpoint-threshold-mb", "title": "zero-cache Config", "searchTitle": "Litestream Checkpoint Threshold MB", "sectionTitle": "Litestream Checkpoint Threshold MB", @@ -8334,7 +8472,7 @@ "kind": "section" }, { - "id": "603-zero-cache-config#litestream-config-path", + "id": "612-zero-cache-config#litestream-config-path", "title": "zero-cache Config", "searchTitle": "Litestream Config Path", "sectionTitle": "Litestream Config Path", @@ -8344,7 +8482,7 @@ "kind": "section" }, { - "id": "604-zero-cache-config#litestream-executable", + "id": "613-zero-cache-config#litestream-executable", "title": "zero-cache Config", "searchTitle": "Litestream Executable", "sectionTitle": "Litestream Executable", @@ -8354,7 +8492,37 @@ "kind": "section" }, { - "id": "605-zero-cache-config#litestream-incremental-backup-interval-minutes", + "id": "614-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 restores when ZERO_LITESTREAM_RESTORE_USING_V5 is enabled. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. The official Zero Docker image includes Litestream 0.5.15 at this path. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5", + "kind": "section" + }, + { + "id": "615-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 when that executable is configured. If it is unavailable, Zero falls back to the legacy executable. Set this to false to force legacy restore behavior. 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": "616-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 makes rollback difficult because older versions cannot restore an LTX-only backup. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: false", + "kind": "section" + }, + { + "id": "617-zero-cache-config#litestream-incremental-backup-interval-minutes", "title": "zero-cache Config", "searchTitle": "Litestream Incremental Backup Interval Minutes", "sectionTitle": "Litestream Incremental Backup Interval Minutes", @@ -8364,7 +8532,7 @@ "kind": "section" }, { - "id": "606-zero-cache-config#litestream-maximum-checkpoint-page-count", + "id": "618-zero-cache-config#litestream-maximum-checkpoint-page-count", "title": "zero-cache Config", "searchTitle": "Litestream Maximum Checkpoint Page Count", "sectionTitle": "Litestream Maximum Checkpoint Page Count", @@ -8374,7 +8542,7 @@ "kind": "section" }, { - "id": "607-zero-cache-config#litestream-minimum-checkpoint-page-count", + "id": "619-zero-cache-config#litestream-minimum-checkpoint-page-count", "title": "zero-cache Config", "searchTitle": "Litestream Minimum Checkpoint Page Count", "sectionTitle": "Litestream Minimum Checkpoint Page Count", @@ -8384,7 +8552,7 @@ "kind": "section" }, { - "id": "608-zero-cache-config#litestream-multipart-concurrency", + "id": "620-zero-cache-config#litestream-multipart-concurrency", "title": "zero-cache Config", "searchTitle": "Litestream Multipart Concurrency", "sectionTitle": "Litestream Multipart Concurrency", @@ -8394,7 +8562,7 @@ "kind": "section" }, { - "id": "609-zero-cache-config#litestream-multipart-size", + "id": "621-zero-cache-config#litestream-multipart-size", "title": "zero-cache Config", "searchTitle": "Litestream Multipart Size", "sectionTitle": "Litestream Multipart Size", @@ -8404,7 +8572,7 @@ "kind": "section" }, { - "id": "610-zero-cache-config#litestream-log-level", + "id": "622-zero-cache-config#litestream-log-level", "title": "zero-cache Config", "searchTitle": "Litestream Log Level", "sectionTitle": "Litestream Log Level", @@ -8414,7 +8582,7 @@ "kind": "section" }, { - "id": "611-zero-cache-config#litestream-port", + "id": "623-zero-cache-config#litestream-port", "title": "zero-cache Config", "searchTitle": "Litestream Port", "sectionTitle": "Litestream Port", @@ -8424,7 +8592,7 @@ "kind": "section" }, { - "id": "612-zero-cache-config#litestream-region", + "id": "624-zero-cache-config#litestream-region", "title": "zero-cache Config", "searchTitle": "Litestream Region", "sectionTitle": "Litestream Region", @@ -8434,7 +8602,7 @@ "kind": "section" }, { - "id": "613-zero-cache-config#litestream-restore-parallelism", + "id": "625-zero-cache-config#litestream-restore-parallelism", "title": "zero-cache Config", "searchTitle": "Litestream Restore Parallelism", "sectionTitle": "Litestream Restore Parallelism", @@ -8444,17 +8612,17 @@ "kind": "section" }, { - "id": "614-zero-cache-config#litestream-snapshot-backup-interval-hours", + "id": "626-zero-cache-config#litestream-snapshot-backup-interval-hours", "title": "zero-cache Config", "searchTitle": "Litestream Snapshot Backup Interval Hours", "sectionTitle": "Litestream Snapshot Backup Interval Hours", "sectionId": "litestream-snapshot-backup-interval-hours", "url": "/docs/zero-cache-config", - "content": "The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new litestream generation. This improves restore time at the expense of bandwidth. Applications with a large database and low write rate can increase this interval to reduce network usage for backups (litestream defaults to 24 hours). flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12", + "content": "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. Applications with a large database and low write rate can increase this interval to reduce network usage for backups (litestream defaults to 24 hours). flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12", "kind": "section" }, { - "id": "615-zero-cache-config#log-format", + "id": "627-zero-cache-config#log-format", "title": "zero-cache Config", "searchTitle": "Log Format", "sectionTitle": "Log Format", @@ -8464,7 +8632,7 @@ "kind": "section" }, { - "id": "616-zero-cache-config#log-ivm-sampling", + "id": "628-zero-cache-config#log-ivm-sampling", "title": "zero-cache Config", "searchTitle": "Log IVM Sampling", "sectionTitle": "Log IVM Sampling", @@ -8474,7 +8642,7 @@ "kind": "section" }, { - "id": "617-zero-cache-config#log-level", + "id": "629-zero-cache-config#log-level", "title": "zero-cache Config", "searchTitle": "Log Level", "sectionTitle": "Log Level", @@ -8484,7 +8652,7 @@ "kind": "section" }, { - "id": "618-zero-cache-config#log-slow-hydrate-threshold", + "id": "630-zero-cache-config#log-slow-hydrate-threshold", "title": "zero-cache Config", "searchTitle": "Log Slow Hydrate Threshold", "sectionTitle": "Log Slow Hydrate Threshold", @@ -8494,7 +8662,7 @@ "kind": "section" }, { - "id": "619-zero-cache-config#log-slow-row-threshold", + "id": "631-zero-cache-config#log-slow-row-threshold", "title": "zero-cache Config", "searchTitle": "Log Slow Row Threshold", "sectionTitle": "Log Slow Row Threshold", @@ -8504,7 +8672,7 @@ "kind": "section" }, { - "id": "620-zero-cache-config#mutate-api-key", + "id": "632-zero-cache-config#mutate-api-key", "title": "zero-cache Config", "searchTitle": "Mutate API Key", "sectionTitle": "Mutate API Key", @@ -8514,7 +8682,7 @@ "kind": "section" }, { - "id": "621-zero-cache-config#mutate-allowed-client-headers", + "id": "633-zero-cache-config#mutate-allowed-client-headers", "title": "zero-cache Config", "searchTitle": "Mutate Allowed Client Headers", "sectionTitle": "Mutate Allowed Client Headers", @@ -8524,7 +8692,7 @@ "kind": "section" }, { - "id": "622-zero-cache-config#mutate-allowed-request-headers", + "id": "634-zero-cache-config#mutate-allowed-request-headers", "title": "zero-cache Config", "searchTitle": "Mutate Allowed Request Headers", "sectionTitle": "Mutate Allowed Request Headers", @@ -8534,7 +8702,7 @@ "kind": "section" }, { - "id": "623-zero-cache-config#mutate-forward-cookies", + "id": "635-zero-cache-config#mutate-forward-cookies", "title": "zero-cache Config", "searchTitle": "Mutate Forward Cookies", "sectionTitle": "Mutate Forward Cookies", @@ -8544,7 +8712,7 @@ "kind": "section" }, { - "id": "624-zero-cache-config#mutate-url", + "id": "636-zero-cache-config#mutate-url", "title": "zero-cache Config", "searchTitle": "Mutate URL", "sectionTitle": "Mutate URL", @@ -8554,7 +8722,7 @@ "kind": "section" }, { - "id": "625-zero-cache-config#number-of-sync-workers", + "id": "637-zero-cache-config#number-of-sync-workers", "title": "zero-cache Config", "searchTitle": "Number of Sync Workers", "sectionTitle": "Number of Sync Workers", @@ -8564,7 +8732,7 @@ "kind": "section" }, { - "id": "626-zero-cache-config#per-user-mutation-limit-max", + "id": "638-zero-cache-config#per-user-mutation-limit-max", "title": "zero-cache Config", "searchTitle": "Per User Mutation Limit Max", "sectionTitle": "Per User Mutation Limit Max", @@ -8574,7 +8742,7 @@ "kind": "section" }, { - "id": "627-zero-cache-config#per-user-mutation-limit-window-ms", + "id": "639-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)", @@ -8584,17 +8752,17 @@ "kind": "section" }, { - "id": "628-zero-cache-config#pg-replication-slot-failover", + "id": "640-zero-cache-config#pg-replication-slot-failover", "title": "zero-cache Config", "searchTitle": "PG Replication Slot Failover", "sectionTitle": "PG Replication Slot Failover", "sectionId": "pg-replication-slot-failover", "url": "/docs/zero-cache-config", - "content": "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 and Failover. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false", + "content": "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", "kind": "section" }, { - "id": "629-zero-cache-config#port", + "id": "641-zero-cache-config#port", "title": "zero-cache Config", "searchTitle": "Port", "sectionTitle": "Port", @@ -8604,7 +8772,7 @@ "kind": "section" }, { - "id": "630-zero-cache-config#query-api-key", + "id": "642-zero-cache-config#query-api-key", "title": "zero-cache Config", "searchTitle": "Query API Key", "sectionTitle": "Query API Key", @@ -8614,7 +8782,7 @@ "kind": "section" }, { - "id": "631-zero-cache-config#query-allowed-client-headers", + "id": "643-zero-cache-config#query-allowed-client-headers", "title": "zero-cache Config", "searchTitle": "Query Allowed Client Headers", "sectionTitle": "Query Allowed Client Headers", @@ -8624,7 +8792,7 @@ "kind": "section" }, { - "id": "632-zero-cache-config#query-allowed-request-headers", + "id": "644-zero-cache-config#query-allowed-request-headers", "title": "zero-cache Config", "searchTitle": "Query Allowed Request Headers", "sectionTitle": "Query Allowed Request Headers", @@ -8634,7 +8802,7 @@ "kind": "section" }, { - "id": "633-zero-cache-config#query-forward-cookies", + "id": "645-zero-cache-config#query-forward-cookies", "title": "zero-cache Config", "searchTitle": "Query Forward Cookies", "sectionTitle": "Query Forward Cookies", @@ -8644,7 +8812,7 @@ "kind": "section" }, { - "id": "634-zero-cache-config#query-hydration-stats", + "id": "646-zero-cache-config#query-hydration-stats", "title": "zero-cache Config", "searchTitle": "Query Hydration Stats", "sectionTitle": "Query Hydration Stats", @@ -8654,7 +8822,7 @@ "kind": "section" }, { - "id": "635-zero-cache-config#query-url", + "id": "647-zero-cache-config#query-url", "title": "zero-cache Config", "searchTitle": "Query URL", "sectionTitle": "Query URL", @@ -8664,7 +8832,7 @@ "kind": "section" }, { - "id": "636-zero-cache-config#replica-file", + "id": "648-zero-cache-config#replica-file", "title": "zero-cache Config", "searchTitle": "Replica File", "sectionTitle": "Replica File", @@ -8674,7 +8842,7 @@ "kind": "section" }, { - "id": "637-zero-cache-config#replica-vacuum-interval-hours", + "id": "649-zero-cache-config#replica-vacuum-interval-hours", "title": "zero-cache Config", "searchTitle": "Replica Vacuum Interval Hours", "sectionTitle": "Replica Vacuum Interval Hours", @@ -8684,17 +8852,17 @@ "kind": "section" }, { - "id": "638-zero-cache-config#replication-lag-report-interval-ms", + "id": "650-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. Because replication lag reports are only issued after the previous one was received, the actual interval between reports may be longer when there is a backlog in the replication stream. 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 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", "kind": "section" }, { - "id": "639-zero-cache-config#server-version", + "id": "651-zero-cache-config#server-version", "title": "zero-cache Config", "searchTitle": "Server Version", "sectionTitle": "Server Version", @@ -8704,7 +8872,7 @@ "kind": "section" }, { - "id": "640-zero-cache-config#shadow-sync-enabled", + "id": "652-zero-cache-config#shadow-sync-enabled", "title": "zero-cache Config", "searchTitle": "Shadow Sync Enabled", "sectionTitle": "Shadow Sync Enabled", @@ -8714,7 +8882,7 @@ "kind": "section" }, { - "id": "641-zero-cache-config#shadow-sync-interval-hours", + "id": "653-zero-cache-config#shadow-sync-interval-hours", "title": "zero-cache Config", "searchTitle": "Shadow Sync Interval Hours", "sectionTitle": "Shadow Sync Interval Hours", @@ -8724,7 +8892,7 @@ "kind": "section" }, { - "id": "642-zero-cache-config#shadow-sync-sample-rate", + "id": "654-zero-cache-config#shadow-sync-sample-rate", "title": "zero-cache Config", "searchTitle": "Shadow Sync Sample Rate", "sectionTitle": "Shadow Sync Sample Rate", @@ -8734,7 +8902,7 @@ "kind": "section" }, { - "id": "643-zero-cache-config#shadow-sync-max-rows-per-table", + "id": "655-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", @@ -8744,7 +8912,7 @@ "kind": "section" }, { - "id": "644-zero-cache-config#storage-db-temp-dir", + "id": "656-zero-cache-config#storage-db-temp-dir", "title": "zero-cache Config", "searchTitle": "Storage DB Temp Dir", "sectionTitle": "Storage DB Temp Dir", @@ -8754,7 +8922,7 @@ "kind": "section" }, { - "id": "645-zero-cache-config#task-id", + "id": "657-zero-cache-config#task-id", "title": "zero-cache Config", "searchTitle": "Task ID", "sectionTitle": "Task ID", @@ -8764,7 +8932,7 @@ "kind": "section" }, { - "id": "646-zero-cache-config#upstream-max-connections", + "id": "658-zero-cache-config#upstream-max-connections", "title": "zero-cache Config", "searchTitle": "Upstream Max Connections", "sectionTitle": "Upstream Max Connections", @@ -8774,7 +8942,7 @@ "kind": "section" }, { - "id": "647-zero-cache-config#upstream-pg-replication-slot-failover", + "id": "659-zero-cache-config#upstream-pg-replication-slot-failover", "title": "zero-cache Config", "searchTitle": "Upstream PG Replication Slot Failover", "sectionTitle": "Upstream PG Replication Slot Failover", @@ -8784,7 +8952,17 @@ "kind": "section" }, { - "id": "648-zero-cache-config#websocket-compression", + "id": "660-zero-cache-config#upstream-pg-stream-inbound-timeout", + "title": "zero-cache Config", + "searchTitle": "Upstream PG Stream Inbound Timeout", + "sectionTitle": "Upstream PG Stream Inbound Timeout", + "sectionId": "upstream-pg-stream-inbound-timeout", + "url": "/docs/zero-cache-config", + "content": "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", + "kind": "section" + }, + { + "id": "661-zero-cache-config#websocket-compression", "title": "zero-cache Config", "searchTitle": "Websocket Compression", "sectionTitle": "Websocket Compression", @@ -8794,7 +8972,7 @@ "kind": "section" }, { - "id": "649-zero-cache-config#websocket-compression-options", + "id": "662-zero-cache-config#websocket-compression-options", "title": "zero-cache Config", "searchTitle": "Websocket Compression Options", "sectionTitle": "Websocket Compression Options", @@ -8804,7 +8982,7 @@ "kind": "section" }, { - "id": "650-zero-cache-config#websocket-max-payload-bytes", + "id": "663-zero-cache-config#websocket-max-payload-bytes", "title": "zero-cache Config", "searchTitle": "Websocket Max Payload Bytes", "sectionTitle": "Websocket Max Payload Bytes", @@ -8814,7 +8992,7 @@ "kind": "section" }, { - "id": "651-zero-cache-config#yield-threshold-ms", + "id": "664-zero-cache-config#yield-threshold-ms", "title": "zero-cache Config", "searchTitle": "Yield Threshold (ms)", "sectionTitle": "Yield Threshold (ms)", @@ -8824,7 +9002,7 @@ "kind": "section" }, { - "id": "652-zero-cache-config#deprecated-flags", + "id": "665-zero-cache-config#deprecated-flags", "title": "zero-cache Config", "searchTitle": "Deprecated Flags", "sectionTitle": "Deprecated Flags", @@ -8834,7 +9012,7 @@ "kind": "section" }, { - "id": "653-zero-cache-config#auth-jwk", + "id": "666-zero-cache-config#auth-jwk", "title": "zero-cache Config", "searchTitle": "Auth JWK", "sectionTitle": "Auth JWK", @@ -8844,7 +9022,7 @@ "kind": "section" }, { - "id": "654-zero-cache-config#auth-jwks-url", + "id": "667-zero-cache-config#auth-jwks-url", "title": "zero-cache Config", "searchTitle": "Auth JWKS URL", "sectionTitle": "Auth JWKS URL", @@ -8854,7 +9032,7 @@ "kind": "section" }, { - "id": "655-zero-cache-config#auth-secret", + "id": "668-zero-cache-config#auth-secret", "title": "zero-cache Config", "searchTitle": "Auth Secret", "sectionTitle": "Auth Secret", @@ -8864,7 +9042,7 @@ "kind": "section" }, { - "id": "75-zql", + "id": "76-zql", "title": "ZQL", "searchTitle": "ZQL", "url": "/docs/zql", @@ -8974,7 +9152,7 @@ "kind": "page" }, { - "id": "656-zql#create-a-builder", + "id": "669-zql#create-a-builder", "title": "ZQL", "searchTitle": "Create a Builder", "sectionTitle": "Create a Builder", @@ -8984,7 +9162,7 @@ "kind": "section" }, { - "id": "657-zql#select", + "id": "670-zql#select", "title": "ZQL", "searchTitle": "Select", "sectionTitle": "Select", @@ -8994,7 +9172,7 @@ "kind": "section" }, { - "id": "658-zql#ordering", + "id": "671-zql#ordering", "title": "ZQL", "searchTitle": "Ordering", "sectionTitle": "Ordering", @@ -9004,7 +9182,7 @@ "kind": "section" }, { - "id": "659-zql#limit", + "id": "672-zql#limit", "title": "ZQL", "searchTitle": "Limit", "sectionTitle": "Limit", @@ -9014,7 +9192,7 @@ "kind": "section" }, { - "id": "660-zql#paging", + "id": "673-zql#paging", "title": "ZQL", "searchTitle": "Paging", "sectionTitle": "Paging", @@ -9024,7 +9202,7 @@ "kind": "section" }, { - "id": "661-zql#getting-a-single-result", + "id": "674-zql#getting-a-single-result", "title": "ZQL", "searchTitle": "Getting a Single Result", "sectionTitle": "Getting a Single Result", @@ -9034,7 +9212,7 @@ "kind": "section" }, { - "id": "662-zql#relationships", + "id": "675-zql#relationships", "title": "ZQL", "searchTitle": "Relationships", "sectionTitle": "Relationships", @@ -9044,7 +9222,7 @@ "kind": "section" }, { - "id": "663-zql#refining-relationships", + "id": "676-zql#refining-relationships", "title": "ZQL", "searchTitle": "Refining Relationships", "sectionTitle": "Refining Relationships", @@ -9054,7 +9232,7 @@ "kind": "section" }, { - "id": "664-zql#nested-relationships", + "id": "677-zql#nested-relationships", "title": "ZQL", "searchTitle": "Nested Relationships", "sectionTitle": "Nested Relationships", @@ -9064,7 +9242,7 @@ "kind": "section" }, { - "id": "665-zql#where", + "id": "678-zql#where", "title": "ZQL", "searchTitle": "Where", "sectionTitle": "Where", @@ -9074,7 +9252,7 @@ "kind": "section" }, { - "id": "666-zql#comparison-operators", + "id": "679-zql#comparison-operators", "title": "ZQL", "searchTitle": "Comparison Operators", "sectionTitle": "Comparison Operators", @@ -9084,7 +9262,7 @@ "kind": "section" }, { - "id": "667-zql#equals-is-the-default-comparison-operator", + "id": "680-zql#equals-is-the-default-comparison-operator", "title": "ZQL", "searchTitle": "Equals is the Default Comparison Operator", "sectionTitle": "Equals is the Default Comparison Operator", @@ -9094,7 +9272,7 @@ "kind": "section" }, { - "id": "668-zql#comparing-to-null", + "id": "681-zql#comparing-to-null", "title": "ZQL", "searchTitle": "Comparing to null", "sectionTitle": "Comparing to null", @@ -9104,7 +9282,7 @@ "kind": "section" }, { - "id": "669-zql#comparing-to-undefined", + "id": "682-zql#comparing-to-undefined", "title": "ZQL", "searchTitle": "Comparing to undefined", "sectionTitle": "Comparing to undefined", @@ -9114,7 +9292,7 @@ "kind": "section" }, { - "id": "670-zql#compound-filters", + "id": "683-zql#compound-filters", "title": "ZQL", "searchTitle": "Compound Filters", "sectionTitle": "Compound Filters", @@ -9124,7 +9302,7 @@ "kind": "section" }, { - "id": "671-zql#comparing-literal-values", + "id": "684-zql#comparing-literal-values", "title": "ZQL", "searchTitle": "Comparing Literal Values", "sectionTitle": "Comparing Literal Values", @@ -9134,7 +9312,7 @@ "kind": "section" }, { - "id": "672-zql#relationship-filters", + "id": "685-zql#relationship-filters", "title": "ZQL", "searchTitle": "Relationship Filters", "sectionTitle": "Relationship Filters", @@ -9144,7 +9322,7 @@ "kind": "section" }, { - "id": "673-zql#type-helpers", + "id": "686-zql#type-helpers", "title": "ZQL", "searchTitle": "Type Helpers", "sectionTitle": "Type Helpers", @@ -9154,7 +9332,7 @@ "kind": "section" }, { - "id": "674-zql#planning", + "id": "687-zql#planning", "title": "ZQL", "searchTitle": "Planning", "sectionTitle": "Planning", @@ -9164,7 +9342,7 @@ "kind": "section" }, { - "id": "675-zql#inspecting-query-plans", + "id": "688-zql#inspecting-query-plans", "title": "ZQL", "searchTitle": "Inspecting Query Plans", "sectionTitle": "Inspecting Query Plans", @@ -9174,7 +9352,7 @@ "kind": "section" }, { - "id": "676-zql#manually-flipping-joins", + "id": "689-zql#manually-flipping-joins", "title": "ZQL", "searchTitle": "Manually Flipping Joins", "sectionTitle": "Manually Flipping Joins", @@ -9184,7 +9362,7 @@ "kind": "section" }, { - "id": "677-zql#scalar-subqueries", + "id": "690-zql#scalar-subqueries", "title": "ZQL", "searchTitle": "Scalar Subqueries", "sectionTitle": "Scalar Subqueries", @@ -9194,7 +9372,7 @@ "kind": "section" }, { - "id": "678-zql#why-it-matters", + "id": "691-zql#why-it-matters", "title": "ZQL", "searchTitle": "Why It Matters", "sectionTitle": "Why It Matters", @@ -9204,7 +9382,7 @@ "kind": "section" }, { - "id": "679-zql#trade-offs", + "id": "692-zql#trade-offs", "title": "ZQL", "searchTitle": "Trade-offs", "sectionTitle": "Trade-offs", @@ -9214,7 +9392,7 @@ "kind": "section" }, { - "id": "680-zql#future-work", + "id": "693-zql#future-work", "title": "ZQL", "searchTitle": "Future Work", "sectionTitle": "Future Work", diff --git a/contents/docs/connecting-to-postgres.mdx b/contents/docs/connecting-to-postgres.mdx index 0f86aadb..f4da5b65 100644 --- a/contents/docs/connecting-to-postgres.mdx +++ b/contents/docs/connecting-to-postgres.mdx @@ -57,6 +57,20 @@ After your server restarts, show the `wal_level` again to ensure it has changed: psql -c 'SHOW wal_level' ``` +### Socket Inactivity Timeout + +`zero-cache` monitors wire activity on its Postgres connections so it can recover when a proxy or network failure leaves a half-open socket. The watchdog samples each connection every 120,000 milliseconds and resets it after one to two intervals without any bytes read or written. In-flight queries on a reset connection are rejected and can recover through their normal retry or restart paths. + +Wire activity resets the watchdog, so streaming operations such as `COPY` remain active. A statement that legitimately computes without sending any data for several minutes can be interrupted. + +### WAL Sender Timeout + +`zero-cache` uses Postgres's `wal_sender_timeout` setting to monitor its replication connection. When the timeout is greater than `0`, Zero sends keepalives and reconnects if the replication stream stops responding. The inbound timeout defaults to twice `wal_sender_timeout`. + +A healthy WAL sender can sometimes remain silent longer than this while decoding WAL from unpublished tables or assembling a large transaction. Set [`ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS`](/docs/zero-cache-config#upstream-pg-stream-inbound-timeout) to widen Zero's inbound threshold without changing the server's timeout. Manual keepalive timing remains derived from `wal_sender_timeout`. + +Setting `wal_sender_timeout` to `0` disables the timeout in Postgres and the related keepalive and reconnect checks in Zero, even when an inbound timeout override is configured. Other connection failure detection remains active. + ### Bounding WAL Size For development databases, you can set a `max_slot_wal_keep_size` value in Postgres. This will help limit the amount of WAL kept around. diff --git a/contents/docs/connection.mdx b/contents/docs/connection.mdx index e0ae6299..7a197c73 100644 --- a/contents/docs/connection.mdx +++ b/contents/docs/connection.mdx @@ -183,9 +183,9 @@ Reads are allowed while `disconnected`, but writes are rejected and return an of ### Error -If `zero-cache` itself crashes, or if the [mutate](/docs/mutators) or [query](/docs/queries) endpoints return a network or HTTP error, Zero transitions to the `error` state. +If `zero-cache` crashes, or [mutate](/docs/mutators) or [query](/docs/queries) endpoints fail, Zero enters the `error` state. If the response code is `5xx`, `zero-cache` will retry up to four times. -This type of error is unlikely to resolve just by retrying, so Zero doesn't try. The app can retry the connection manually by calling `zero.connection.connect()`. +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. diff --git a/contents/docs/mutators.mdx b/contents/docs/mutators.mdx index 322e6888..919ea384 100644 --- a/contents/docs/mutators.mdx +++ b/contents/docs/mutators.mdx @@ -111,6 +111,8 @@ tx.mutate.user.insert({ }) ``` +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): ```tsx @@ -856,7 +858,7 @@ app.post('/api/zero/mutate', async c => { -If Zero receives any response from the mutate endpoint other than HTTP 200, 401, or 403, it will disconnect and enter the [error state](/docs/connection#error). +Responses other than 200, 401, or 403 enter the [error state](/docs/connection#error). `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. diff --git a/contents/docs/otel.mdx b/contents/docs/otel.mdx index 485c9298..321226ec 100644 --- a/contents/docs/otel.mdx +++ b/contents/docs/otel.mdx @@ -106,129 +106,140 @@ This callback is called before sending WebSocket messages that trigger API serve ## Metrics Reference - `view_syncer_lag` and `view_syncer_hydration` require - OpenTelemetry exponential histogram support. Prometheus - users must enable native histograms. Use the existing - `serving_lag` gauges if your backend does not support - them. + `zero_sync_view_syncer_lag`, + `zero_sync_view_syncer_hydration`, and + `zero_sync_e2e_serving_lag` require OpenTelemetry + exponential histogram support. Prometheus users must + enable native histograms. Use the existing + `zero_sync_serving_lag` gauges if your backend does not + support them.
### zero.server -| Metric | Type | Unit | Description | -| ------------------------- | ------------- | ---- | ------------------------------------------------------------------------------------- | -| `uptime` | Gauge | s | Cumulative uptime, starting from when requests are served | -| `api.requests` | Counter | | Calls to user mutate and query APIs, including cleanup and auth-validation operations | -| `api.request_duration` | Histogram | s | End-to-end user API request duration, including retries | -| `api.attempts` | Counter | | HTTP fetch attempts made while calling user API endpoints | -| `api.attempt_duration` | Histogram | s | Duration of each API HTTP attempt, excluding retry delays | -| `api.in_flight` | UpDownCounter | | API requests currently in flight | -| `startup_duration` | Histogram | s | Time from starting `zero-cache` until it is ready | -| `worker_startup_duration` | Histogram | s | Time from starting a worker until it is ready | +| Metric | Type | Unit | Description | +| ------------------------------------- | ------------- | ---- | ------------------------------------------------------------------------------------- | +| `zero_server_uptime` | Gauge | s | Cumulative uptime, starting from when requests are served | +| `zero_server_api_requests` | Counter | | Calls to user mutate and query APIs, including cleanup and auth-validation operations | +| `zero_server_api_request_duration` | Histogram | s | End-to-end user API request duration, including retries | +| `zero_server_api_attempts` | Counter | | HTTP fetch attempts made while calling user API endpoints | +| `zero_server_api_attempt_duration` | Histogram | s | Duration of each API HTTP attempt, excluding retry delays | +| `zero_server_api_in_flight` | UpDownCounter | | API requests currently in flight | +| `zero_server_startup_duration` | Histogram | s | Time from starting `zero-cache` until it is ready | +| `zero_server_worker_startup_duration` | Histogram | s | Time from starting a worker until it is ready | ### zero.replica -| Metric | Type | Unit | Description | -| ------------------------------------------ | --------- | ----- | ----------------------------------------------------------------------------------------------------------------------- | -| `db_size` | Gauge | bytes | Size of the replica's main db file (excludes WAL) | -| `wal_size` | Gauge | bytes | Size of the replica's WAL file | -| `wal2_size` | Gauge | bytes | Size of the replica's WAL2 file (only if using wal2 mode) | -| `backup_lag` | Gauge | ms | Time since last litestream backup. Expected to sawtooth from 0 to `ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES` | -| `purge_blocked` | Counter | | Number of change-log purges blocked because the actual backup state could not be verified or is stale | -| `litestream.restore.runs` | Counter | | Litestream restore runs | -| `litestream.restore.attempts` | Counter | | Litestream restore subprocess attempts | -| `litestream.restore.db_bytes` | Counter | bytes | SQLite database bytes restored by successful Litestream restores | -| `litestream.restore.duration` | Histogram | s | Wall-clock duration of Litestream restore runs | -| `litestream.restore.wait_duration` | Histogram | s | Time spent waiting for replication-manager snapshot status before restoring | -| `litestream.restore.process_duration` | Histogram | s | Wall-clock duration of Litestream restore subprocesses | -| `litestream.restore.validation_duration` | Histogram | s | Time spent validating restored replica databases | -| `litestream.backup.process_runs` | Counter | | Litestream backup process exits | -| `litestream.backup.process_duration` | Histogram | s | Runtime of Litestream backup subprocesses before exit | -| `litestream.backup.list_duration` | Histogram | s | Time to list the Litestream backup destination | -| `litestream.backup.verification_duration` | Histogram | s | Time to verify backup state in the destination | -| `litestream.snapshot.reservation_duration` | Histogram | s | Time snapshot reservations are held while view-syncers restore and subscribe | +| Metric | Type | Unit | Description | +| ------------------------------------------------------- | --------- | ----- | ----------------------------------------------------------------------------------------------------------------------- | +| `zero_replica_db_size` | Gauge | bytes | Size of the replica's main db file (excludes WAL) | +| `zero_replica_wal_size` | Gauge | bytes | Size of the replica's WAL file | +| `zero_replica_wal2_size` | Gauge | bytes | Size of the replica's WAL2 file (only if using wal2 mode) | +| `zero_replica_backup_lag` | Gauge | ms | Time since last litestream backup. Expected to sawtooth from 0 to `ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES` | +| `zero_replica_purge_blocked` | Counter | | Number of change-log purges blocked because the actual backup state could not be verified or is stale | +| `zero_replica_litestream_restore_runs` | Counter | | Litestream restore runs | +| `zero_replica_litestream_restore_attempts` | Counter | | Litestream restore subprocess attempts | +| `zero_replica_litestream_restore_db_bytes` | Counter | bytes | SQLite database bytes restored by successful Litestream restores | +| `zero_replica_litestream_restore_duration` | Histogram | s | Wall-clock duration of Litestream restore runs | +| `zero_replica_litestream_restore_wait_duration` | Histogram | s | Time spent waiting for replication-manager snapshot status before restoring | +| `zero_replica_litestream_restore_process_duration` | Histogram | s | Wall-clock duration of Litestream restore subprocesses | +| `zero_replica_litestream_restore_validation_duration` | Histogram | s | Time spent validating restored replica databases | +| `zero_replica_litestream_backup_process_runs` | Counter | | Litestream backup process exits | +| `zero_replica_litestream_backup_process_duration` | Histogram | s | Runtime of Litestream backup subprocesses before exit | +| `zero_replica_litestream_backup_list_duration` | Histogram | s | Time to list the Litestream backup destination | +| `zero_replica_litestream_backup_verification_duration` | Histogram | s | Time to verify backup state in the destination | +| `zero_replica_litestream_snapshot_reservation_duration` | Histogram | s | Time snapshot reservations are held while view-syncers restore and subscribe | ### zero.replication -| Metric | Type | Unit | Description | -| ------------------------------------ | --------- | ----- | ------------------------------------------------------------------------------------------------------------------- | -| `upstream_lag` | Gauge | ms | Latency from sending a replication report to receiving it in the stream | -| `replica_lag` | Gauge | ms | Latency from receiving a replication report to it reaching the replica | -| `total_lag` | Gauge | ms | End-to-end replication latency. Grows as an estimate if the next report hasn't arrived | -| `last_total_lag` | Gauge | ms | End-to-end latency of the most recently received report. Unlike `total_lag`, does not grow if reports stop arriving | -| `events` | Counter | | Number of replication events processed | -| `transactions` | Counter | | Count of replicated transactions | -| `changes` | Counter | | Count of replicated changes, including DML and DDL statements | -| `slot_health` | Gauge | 1 | One-hot status for the active logical replication slot: `ok`, `unreserved`, `lost`, `missing`, or `unknown` | -| `slot_retained_wal_bytes` | Gauge | bytes | WAL bytes retained by the active logical replication slot | -| `slot_safe_wal_bytes` | Gauge | bytes | Remaining WAL capacity before the active logical replication slot is lost; omitted when Postgres reports no value | -| `initial_sync_runs` | Counter | | Number of initial-sync runs | -| `initial_sync_duration` | Histogram | s | Wall-clock duration of an initial-sync run | -| `initial_sync_copy_duration` | Histogram | s | Wall-clock duration of the COPY phase for a successful initial-sync run | -| `initial_sync_copy_other_duration` | Histogram | s | Initial-sync duration excluding SQLite flush and index time for a successful run | -| `initial_sync_flush_duration` | Histogram | s | Total SQLite flush time for a successful initial-sync run | -| `initial_sync_index_duration` | Histogram | s | SQLite index creation time for a successful initial-sync run | -| `initial_sync_rows` | Counter | | Rows copied during successful initial-sync runs | -| `initial_sync_copy_stream` | Counter | bytes | PostgreSQL COPY stream bytes processed during initial sync, including in-progress and failed runs | -| `initial_sync_completed_copy_stream` | Counter | bytes | PostgreSQL COPY stream bytes processed during successful initial-sync runs | -| `initial_sync_copy_chunks` | Counter | | PostgreSQL COPY stream chunks processed during initial sync | -| `shadow-sync-runs` | Counter | | Number of [shadow initial-sync](/docs/zero-cache-config#shadow-sync-enabled) runs, labeled by `result` | -| `shadow-sync-duration` | Histogram | s | Wall-clock duration of a shadow initial-sync run, labeled by `result` | -| `flow_control.active_subscribers` | Gauge | | Active change-stream subscribers receiving live changes | -| `flow_control.queued_subscribers` | Gauge | | Change-stream subscribers waiting for the current transaction to finish before activation | -| `flow_control.pending_messages` | Gauge | | Downstream change-stream messages not yet acknowledged by subscribers | -| `flow_control.backlog_messages` | Gauge | | Live change-stream messages buffered while subscribers catch up | -| `flow_control.backlog_bytes` | Gauge | bytes | Live change-stream bytes buffered while subscribers catch up | -| `flow_control.max_backlog_bytes` | Gauge | bytes | Maximum live change-stream bytes buffered by a single subscriber | -| `flow_control.waits` | Counter | | Completed flow-control checkpoints | -| `flow_control.wait_duration` | Histogram | s | Time replication waits at flow-control checkpoints | +| Metric | Type | Unit | Description | +| ----------------------------------------------------- | --------- | ----- | ------------------------------------------------------------------------------------------------------------------------------ | +| `zero_replication_upstream_lag` | Gauge | ms | Latency from sending a replication report to receiving it in the stream | +| `zero_replication_replica_lag` | Gauge | ms | Latency from receiving a replication report to it reaching the replica | +| `zero_replication_total_lag` | Gauge | ms | Measured end-to-end latency of the most recently received replication report; does not grow if reports stop arriving | +| `zero_replication_last_total_lag` | Gauge | ms | Alias of `zero_replication_total_lag`, retained for dashboards that explicitly use the non-extrapolated metric | +| `zero_replication_upstream_clock_skew` | Gauge | ms | Estimated offset of the upstream database clock relative to `zero-cache`; positive values mean upstream is ahead | +| `zero_replication_lag_report_retries` | Counter | | Replication lag reports retried because an expected report did not arrive before the next report interval | +| `zero_replication_events` | Counter | | Number of replication events processed | +| `zero_replication_transactions` | Counter | | Count of replicated transactions | +| `zero_replication_changes` | Counter | | Count of replicated changes, including DML and DDL statements | +| `zero_replication_slot_health` | Gauge | 1 | One-hot status for the active logical replication slot: `ok`, `unreserved`, `lost`, `missing`, or `unknown` | +| `zero_replication_slot_retained_wal_bytes` | Gauge | bytes | WAL bytes retained by the active logical replication slot | +| `zero_replication_slot_safe_wal_bytes` | Gauge | bytes | Remaining WAL capacity before the active logical replication slot is lost; omitted when Postgres reports no value | +| `zero_replication_initial_sync_runs` | Counter | | Number of initial-sync runs | +| `zero_replication_initial_sync_duration` | Histogram | s | Wall-clock duration of an initial-sync run | +| `zero_replication_initial_sync_copy_duration` | Histogram | s | Wall-clock duration of the COPY phase for a successful initial-sync run | +| `zero_replication_initial_sync_copy_other_duration` | Histogram | s | Initial-sync duration excluding SQLite flush and index time for a successful run | +| `zero_replication_initial_sync_flush_duration` | Histogram | s | Total SQLite flush time for a successful initial-sync run | +| `zero_replication_initial_sync_index_duration` | Histogram | s | SQLite index creation time for a successful initial-sync run | +| `zero_replication_initial_sync_rows` | Counter | | Rows copied during successful initial-sync runs | +| `zero_replication_initial_sync_copy_stream` | Counter | bytes | PostgreSQL COPY stream bytes, including failed runs; reported in approximately 8 MiB batches and flushed when the stream ends | +| `zero_replication_initial_sync_completed_copy_stream` | Counter | bytes | PostgreSQL COPY stream bytes processed during successful initial-sync runs | +| `zero_replication_initial_sync_copy_chunks` | Counter | | PostgreSQL COPY stream chunks processed during initial sync; batched with COPY-stream updates and flushed when the stream ends | +| `zero_replication_shadow_sync_runs` | Counter | | Number of [shadow initial-sync](/docs/zero-cache-config#shadow-sync-enabled) runs, labeled by `result` | +| `zero_replication_shadow_sync_duration` | Histogram | s | Wall-clock duration of a shadow initial-sync run, labeled by `result` | +| `zero_replication_flow_control_active_subscribers` | Gauge | | Active change-stream subscribers receiving live changes | +| `zero_replication_flow_control_queued_subscribers` | Gauge | | Change-stream subscribers waiting for the current transaction to finish before activation | +| `zero_replication_flow_control_pending_messages` | Gauge | | Downstream change-stream messages not yet acknowledged by subscribers | +| `zero_replication_flow_control_backlog_messages` | Gauge | | Live change-stream messages buffered while subscribers catch up | +| `zero_replication_flow_control_backlog_bytes` | Gauge | bytes | Live change-stream bytes buffered while subscribers catch up | +| `zero_replication_flow_control_max_backlog_bytes` | Gauge | bytes | Maximum live change-stream bytes buffered by a single subscriber | +| `zero_replication_flow_control_waits` | Counter | | Completed flow-control checkpoints | +| `zero_replication_flow_control_wait_duration` | Histogram | s | Time replication waits at flow-control checkpoints | + +`zero_replication_total_lag` and `zero_replication_last_total_lag` now report the same latest measured round trip and do not grow when reports stop arriving. Use `zero_replication_lag_report_retries` to detect a stalled or missing report stream. ### zero.sync -| Metric | Type | Unit | Description | -| ----------------------------------- | ------------- | ---- | -------------------------------------------------------------------------------------------------------------- | -| `max-protocol-version` | Gauge | | Highest sync protocol version seen from connecting clients | -| `active-clients` | UpDownCounter | | Number of currently connected sync clients | -| `active-client-groups` | Gauge | | Number of active ViewSyncerService instances in a syncer worker | -| `queries` | Gauge | | Active IVM pipelines across all client groups in a syncer worker | -| `rows` | Gauge | | CVR-tracked rows across all client groups in a syncer worker | -| `serving_lag` | Gauge | ms | Longest time locally ready replica changes have remained unserved across active ViewSyncer client groups | -| `serving_lag_stats` | Gauge | ms | Distribution of serving lag across active ViewSyncer client groups | -| `serving_lagging_client_groups` | Gauge | | Active ViewSyncer client groups with locally ready replica changes not yet served to clients | -| `view_syncer_lag` | Histogram | s | Time from replica changes becoming ready to ViewSyncer output, sampled once per minute per active client group | -| `view_syncer_hydration` | Histogram | s | Time from a ViewSyncer query sync requiring hydration until output, per client group | -| `lock-wait-time` | Histogram | s | Time spent waiting to acquire the ViewSyncerService lock per operation | -| `pipeline-resets` | Counter | | Count of pipeline resets, labeled by `reason` | -| `hydration` | Counter | | Number of query hydrations | -| `hydration-time` | Histogram | s | Time to hydrate a query | -| `advance-time` | Histogram | s | Time to advance all queries for a client group after applying a transaction | -| `poke.time` | Histogram | s | Time per poke transaction (excludes canceled/noop pokes) | -| `poke.transactions` | Counter | | Count of poke transactions | -| `poke.rows` | Counter | | Count of poked rows | -| `cvr.load_attempts` | Counter | | CVR load attempts | -| `cvr.load_duration` | Histogram | s | Time to load a CVR | -| `cvr.flush_attempts` | Counter | | CVR flush attempts | -| `cvr.flush-time` | Histogram | s | Time to flush a CVR transaction | -| `cvr.rows-flushed` | Counter | | Number of changed rows flushed to a CVR | -| `websocket.open_connections` | UpDownCounter | | Open client WebSocket connections | -| `websocket.connection_attempts` | Counter | | Client WebSocket connection attempts | -| `websocket.connection_successes` | Counter | | Client WebSocket connections successfully initialized | -| `websocket.connection_failures` | Counter | | Client WebSocket connection attempts that failed before initialization | -| `websocket.errors` | Counter | | Client WebSocket error events | -| `ivm.advance-time` | Histogram | s | Time to advance IVM queries in response to a single change | -| `ivm.conflict-rows-deleted` | Counter | | Rows deleted because they conflicted with an added row | -| `query.transformations` | Counter | | Number of query transformations performed | -| `query.transformation-time` | Histogram | s | Time to transform custom queries via API server | -| `query.transformation-hash-changes` | Counter | | Times a query transformation hash changed | -| `query.transformation-no-ops` | Counter | | Times a query transformation was a no-op | -| `query.row-set-signature-drifts` | Counter | | Unchanged query rehydrations whose row-set signature differs from the CVR, forcing a config-version bump | +| Metric | Type | Unit | Description | +| ---------------------------------------------------- | ------------- | ---- | --------------------------------------------------------------------------------------------------------- | +| `zero_sync_max_protocol_version` | Gauge | | Highest sync protocol version seen from connecting clients | +| `zero_sync_active_clients` | UpDownCounter | | Number of currently connected sync clients | +| `zero_sync_active_client_groups` | Gauge | | Number of active ViewSyncerService instances in a syncer worker | +| `zero_sync_queries` | Gauge | | Active IVM pipelines across all client groups in a syncer worker | +| `zero_sync_rows` | Gauge | | CVR-tracked rows across all client groups in a syncer worker | +| `zero_sync_serving_lag` | Gauge | ms | Longest time locally ready replica changes have remained unserved across eligible active client groups | +| `zero_sync_serving_lag_stats` | Gauge | ms | Distribution of serving lag across eligible active client groups | +| `zero_sync_serving_lagging_client_groups` | Gauge | | Eligible active client groups with locally ready replica changes not yet served to clients | +| `zero_sync_view_syncer_lag` | Histogram | s | Time from replica changes becoming ready to ViewSyncer output, sampled once per minute per eligible group | +| `zero_sync_view_syncer_hydration` | Histogram | s | Time from a ViewSyncer query sync requiring hydration until output, per client group | +| `zero_sync_e2e_serving_lag` | Histogram | s | Completion latency from the upstream transaction commit through ViewSyncer output | +| `zero_sync_e2e_serving_lag_clamps` | Counter | | Negative end-to-end lag observations clamped to zero because the upstream clock was ahead | +| `zero_sync_lock_wait_time` | Histogram | s | Time spent waiting to acquire the ViewSyncerService lock per operation | +| `zero_sync_pipeline_resets` | Counter | | Count of pipeline resets, labeled by `reason` | +| `zero_sync_hydration` | Counter | | Number of query hydrations | +| `zero_sync_hydration_time` | Histogram | s | Time to hydrate a query | +| `zero_sync_advance_time` | Histogram | s | Time to advance all queries for a client group after applying a transaction | +| `zero_sync_poke_time` | Histogram | s | Time per poke transaction (excludes canceled/noop pokes) | +| `zero_sync_poke_transactions` | Counter | | Count of poke transactions | +| `zero_sync_poke_rows` | Counter | | Count of poked rows | +| `zero_sync_cvr_load_attempts` | Counter | | CVR load attempts | +| `zero_sync_cvr_load_duration` | Histogram | s | Time to load a CVR | +| `zero_sync_cvr_flush_attempts` | Counter | | CVR flush attempts | +| `zero_sync_cvr_flush_time` | Histogram | s | Time to flush a CVR transaction | +| `zero_sync_cvr_rows_flushed` | Counter | | Number of changed rows flushed to a CVR | +| `zero_sync_websocket_open_connections` | UpDownCounter | | Open client WebSocket connections | +| `zero_sync_websocket_connection_attempts` | Counter | | Client WebSocket connection attempts | +| `zero_sync_websocket_connection_successes` | Counter | | Client WebSocket connections successfully initialized | +| `zero_sync_websocket_connection_failures` | Counter | | Client WebSocket connection attempts that failed before initialization | +| `zero_sync_websocket_errors` | Counter | | Client WebSocket error events | +| `zero_sync_ivm_advance_time` | Histogram | s | Time to advance IVM queries in response to a single change | +| `zero_sync_ivm_conflict_rows_deleted` | Counter | | Rows deleted because they conflicted with an added row | +| `zero_sync_query_transformations` | Counter | | Number of query transformations performed | +| `zero_sync_query_transformation_time` | Histogram | s | Time to transform custom queries via API server | +| `zero_sync_query_transformation_hash_changes` | Counter | | Times a query transformation hash changed | +| `zero_sync_query_transformation_no_ops` | Counter | | Times a query transformation was a no-op | +| `zero_sync_query_row_set_signature_drifts` | Counter | | Unchanged query rehydrations whose row-set signature differs from the CVR, forcing a config-version bump | +| `zero_sync_query_same_hash_rehydrations_forced_bump` | Counter | | Same-hash query rehydrations that force a config-version bump so changed rows are delivered | + +Serving-lag metrics include only client groups with at least one connected client and a validated background connection context. Retained groups without an eligible connection do not contribute lag. ### zero.mutation -| Metric | Type | Unit | Description | -| -------- | ------- | ---- | ------------------------------------ | -| `crud` | Counter | | Number of CRUD mutations processed | -| `custom` | Counter | | Number of custom mutations processed | -| `pushes` | Counter | | Number of pushes processed | +| Metric | Type | Unit | Description | +| ---------------------- | ------- | ---- | ------------------------------------ | +| `zero_mutation_crud` | Counter | | Number of CRUD mutations processed | +| `zero_mutation_custom` | Counter | | Number of custom mutations processed | +| `zero_mutation_pushes` | Counter | | Number of pushes processed | diff --git a/contents/docs/release-notes/1.9.mdx b/contents/docs/release-notes/1.9.mdx new file mode 100644 index 00000000..9babdfd2 --- /dev/null +++ b/contents/docs/release-notes/1.9.mdx @@ -0,0 +1,73 @@ +--- +title: Zero 1.9 +description: Stability and Query Correctness +--- + +## Installation + +```bash +npm install @rocicorp/zero@1.9 +``` + +## Overview + +Zero 1.9 improves query/mutation correctness and contains numerous reliability improvements. + +## Features + +- [`zero_sync_e2e_serving_lag`](/docs/otel#zerosync) measures completed replicated work from the upstream transaction commit through view-syncer poke. [`zero_replication_upstream_clock_skew`](/docs/otel#zeroreplication) tries to identify measurements biased by clock differences. ([#6312](https://github.com/rocicorp/mono/pull/6312)) +- Zero's replication-stream inbound timeout can now be [configured separately from PostgreSQL's `wal_sender_timeout`](/docs/connecting-to-postgres#wal-sender-timeout), avoiding unnecessary reconnects during long gaps in WAL output. ([#6351](https://github.com/rocicorp/mono/pull/6351), thanks [@gerardatkonvo](https://github.com/gerardatkonvo)!) + +## Performance + +### Cold Mutation Latency + +Before running mutations, Zero Server fetches and caches PostgreSQL schema metadata. This is now **2.7x faster** in Zero 1.9 (done in [#6292](https://github.com/rocicorp/mono/pull/6292), thanks [@diegopereira99](https://github.com/diegopereira99)!). This is most noticeable with cold-starts in serverless environments like AWS Lambda. + + + +## Fixes + +- [Restores now use Litestream 0.5.15 for legacy-format compatibility](https://github.com/rocicorp/mono/pull/6260), [retry transient failures](https://github.com/rocicorp/mono/pull/6347), [clean up temporary databases and staged WAL files after failed or interrupted attempts](https://github.com/rocicorp/mono/pull/6355), and [retain the previous snapshot generation during active restores](https://github.com/rocicorp/mono/pull/6267). +- [`insert` now succeeds without changing the row when its Zero primary key already exists; before 1.9, the server returned an error.](https://github.com/rocicorp/mono/pull/6251) +- [Ordered queries now return correct results when cursor fields contain `NULL`.](https://github.com/rocicorp/mono/pull/6121) (thanks [@YevheniiKotyrlo](https://github.com/YevheniiKotyrlo)!) +- [Rebuilt queries now deliver changed rows instead of occasionally leaving clients with stale results.](https://github.com/rocicorp/mono/pull/6196) +- [Queries no longer drop rows or emit invalid SQL when given an inapplicable scalar hint, and scalar `NOT EXISTS` now handles empty or `NULL` results.](https://github.com/rocicorp/mono/pull/6306) +- [Schema construction, CRUD mutators, and materialized views now preserve a key named `__proto__` as user data.](https://github.com/rocicorp/mono/pull/6185) (thanks [@tjenkinson](https://github.com/tjenkinson)!) +- SQLite statement caches now [retain at most 1,000 idle entries each](https://github.com/rocicorp/mono/pull/6202). +- Terminated client groups now [release custom-query timers and caches](https://github.com/rocicorp/mono/pull/6228). +- Large replica transactions [can spill dirty pages to WAL instead of retaining the complete write set in native memory](https://github.com/rocicorp/mono/pull/6311). +- [Missing replication-lag reports are retried and `total_lag` no longer grows when reports stop arriving](https://github.com/rocicorp/mono/pull/6187), while [serving-lag metrics exclude disconnected or not-yet-validated client groups](https://github.com/rocicorp/mono/pull/6219). +- `zero-cache` now recovers from [half-open PostgreSQL sockets](https://github.com/rocicorp/mono/pull/6220), [including over TLS](https://github.com/rocicorp/mono/pull/6221), and [the official image applies the bundled postgres.js disconnect patch](https://github.com/rocicorp/mono/pull/6310). +- [With PostgreSQL `wal_sender_timeout=0`, replication no longer enters a continuous reconnect loop.](https://github.com/rocicorp/mono/pull/6244) See [WAL Sender Timeout](/docs/connecting-to-postgres#wal-sender-timeout). +- [Client connection attempts now time out across setup and the server handshake, abandon late sockets, and retry normally.](https://github.com/rocicorp/mono/pull/6299) +- [Reconnect confirmations no longer produce false slow-query warnings or inflated materialization metrics.](https://github.com/rocicorp/mono/pull/6308) +- [Different integration versions in a pnpm workspace no longer create peer-qualified duplicate copies of `@rocicorp/zero`, fixing cross-package type and module-augmentation failures.](https://github.com/rocicorp/mono/pull/6231) +- [Replicated PostgreSQL type and nullability changes now preserve compound-index column order in SQLite replicas.](https://github.com/rocicorp/mono/pull/6225) To repair an affected replica, resync it from Postgres or recreate the PostgreSQL index. +- [Expected schema and replica resets now log warnings instead of errors](https://github.com/rocicorp/mono/pull/6248), and [`zero-cache` skips Litestream restore when backups are not configured](https://github.com/rocicorp/mono/pull/6259). (thanks [@asterikx](https://github.com/asterikx)!) +- [Server CRUD updates and upserts no longer assign primary-key columns, avoiding PostgreSQL locks that could block concurrent foreign-key inserts.](https://github.com/rocicorp/mono/pull/6280) (thanks [@shayonj](https://github.com/shayonj)!) +- [Mutation and query API calls now retry all `5xx` responses using the existing four-attempt limit and backoff; `4xx` responses still fail without retry.](https://github.com/rocicorp/mono/pull/6315) (thanks [@shayonj](https://github.com/shayonj)!) +- [SQLite corruption failures now log diagnostics](https://github.com/rocicorp/mono/pull/6215), [delete the corrupted replica before exit](https://github.com/rocicorp/mono/pull/6342), and support [extended corruption errors](https://github.com/rocicorp/mono/pull/6339), with [deeper checks available as an opt-in](https://github.com/rocicorp/mono/pull/6341). +- [Oversized replication updates now identify the transaction, affected column, and value type without logging the value.](https://github.com/rocicorp/mono/pull/6318) +- [Fatal replica-writer failures now surface as replication errors and cause `zero-cache` to exit with a failure instead of silently stopping replication.](https://github.com/rocicorp/mono/pull/6326) +- [Replication now recovers from upstream disconnects](https://github.com/rocicorp/mono/pull/6346) or [stalled PostgreSQL writes](https://github.com/rocicorp/mono/pull/6348) while flow control is blocked. +- [Mutations from multiple tabs in the same client group are no longer skipped or sent out of order.](https://github.com/rocicorp/mono/pull/6340) diff --git a/contents/docs/release-notes/index.mdx b/contents/docs/release-notes/index.mdx index 9692efe0..b6d29f75 100644 --- a/contents/docs/release-notes/index.mdx +++ b/contents/docs/release-notes/index.mdx @@ -2,6 +2,7 @@ title: Release Notes --- +- [Zero 1.9: Stability and Query Correctness](/docs/release-notes/1.9) - [Zero 1.8: Observability and Reliability](/docs/release-notes/1.8) - [Zero 1.7: Query Correctness and Performance](/docs/release-notes/1.7) - [Zero 1.6: PlanetScale Failover Support](/docs/release-notes/1.6) diff --git a/contents/docs/zero-cache-config.mdx b/contents/docs/zero-cache-config.mdx index 2b0f6809..19c30d58 100644 --- a/contents/docs/zero-cache-config.mdx +++ b/contents/docs/zero-cache-config.mdx @@ -332,6 +332,31 @@ Path to the litestream executable. This must be built from the `rocicorp/litestr flag: `--litestream-executable`
env: `ZERO_LITESTREAM_EXECUTABLE`
+### Litestream V5 Executable + +Path to the official Litestream v0.5.x executable used for restores when `ZERO_LITESTREAM_RESTORE_USING_V5` is enabled. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. The official Zero Docker image includes Litestream 0.5.15 at this path. + +flag: `--litestream-executable-v5`
+env: `ZERO_LITESTREAM_EXECUTABLE_V5`
+ +### Litestream Restore Using V5 + +Use `ZERO_LITESTREAM_EXECUTABLE_V5` for restores when that executable is configured. If it is unavailable, Zero falls back to the legacy executable. Set this to `false` to force legacy restore behavior. + +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 makes rollback difficult because older versions cannot restore an LTX-only backup. + +flag: `--litestream-backup-using-v5`
+env: `ZERO_LITESTREAM_BACKUP_USING_V5`
+default: `false` + ### Litestream Incremental Backup Interval Minutes The interval between incremental 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 litestream restore. @@ -407,7 +432,7 @@ 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. This improves restore time at the expense of bandwidth. Applications with a large database and low write rate can increase this interval to reduce network usage for backups (litestream defaults to 24 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. Applications with a large database and low write rate can increase this interval to reduce network usage for backups (litestream defaults to 24 hours). flag: `--litestream-snapshot-backup-interval-hours`
env: `ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS`
@@ -545,7 +570,7 @@ 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 and Failover](/docs/connecting-to-postgres#high-availability-and-failover). Has no effect on Postgres versions before 17. +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](/docs/connecting-to-postgres#high-availability). Has no effect on Postgres versions before 17. flag: `--upstream-pg-replication-slot-failover`
env: `ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER`
@@ -651,7 +676,7 @@ 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](/docs/otel). Because replication lag reports are only issued after the previous one was received, the actual interval between reports may be longer when there is a backlog in the replication stream. +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`. 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. @@ -734,6 +759,17 @@ 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](/docs/connecting-to-postgres#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