From 2c54fe9a99dde63d44364e9e2b49b6e86b0f8d50 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Wed, 11 Nov 2026 12:00:34 +0100 Subject: [PATCH 1/3] docs: Zero 1.12 release notes Adds the 1.12 release notes and the docs for what ships with it: aggregates, groupBy, distinct and JSON path filters in ZQL (restoring what was reverted until the features ship), and the drain, heapz and per-replica slot config options. --- assets/search-index.json | 1810 +++++++++++++++---------- contents/docs/release-notes/1.12.mdx | 260 ++++ contents/docs/release-notes/index.mdx | 1 + contents/docs/zero-cache-config.mdx | 52 + contents/docs/zql.mdx | 134 ++ 5 files changed, 1560 insertions(+), 697 deletions(-) create mode 100644 contents/docs/release-notes/1.12.mdx diff --git a/assets/search-index.json b/assets/search-index.json index 7dbf38d4..39aa1ff9 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -14,7 +14,7 @@ "kind": "page" }, { - "id": "79-agents#options", + "id": "80-agents#options", "title": "Agent Support", "searchTitle": "Options", "sectionTitle": "Options", @@ -126,7 +126,7 @@ "kind": "page" }, { - "id": "80-auth#set-userid-on-client", + "id": "81-auth#set-userid-on-client", "title": "Authentication", "searchTitle": "Set userID on Client", "sectionTitle": "Set userID on Client", @@ -136,7 +136,7 @@ "kind": "section" }, { - "id": "81-auth#define-the-context-type", + "id": "82-auth#define-the-context-type", "title": "Authentication", "searchTitle": "Define the Context Type", "sectionTitle": "Define the Context Type", @@ -146,7 +146,7 @@ "kind": "section" }, { - "id": "82-auth#send-credentials", + "id": "83-auth#send-credentials", "title": "Authentication", "searchTitle": "Send Credentials", "sectionTitle": "Send Credentials", @@ -156,7 +156,7 @@ "kind": "section" }, { - "id": "83-auth#cookies", + "id": "84-auth#cookies", "title": "Authentication", "searchTitle": "Cookies", "sectionTitle": "Cookies", @@ -166,7 +166,7 @@ "kind": "section" }, { - "id": "84-auth#cookie-deployment", + "id": "85-auth#cookie-deployment", "title": "Authentication", "searchTitle": "Cookie Deployment", "sectionTitle": "Cookie Deployment", @@ -176,7 +176,7 @@ "kind": "section" }, { - "id": "85-auth#tokens", + "id": "86-auth#tokens", "title": "Authentication", "searchTitle": "Tokens", "sectionTitle": "Tokens", @@ -186,7 +186,7 @@ "kind": "section" }, { - "id": "86-auth#implement-api-endpoints", + "id": "87-auth#implement-api-endpoints", "title": "Authentication", "searchTitle": "Implement API Endpoints", "sectionTitle": "Implement API Endpoints", @@ -196,7 +196,7 @@ "kind": "section" }, { - "id": "87-auth#query", + "id": "88-auth#query", "title": "Authentication", "searchTitle": "Query", "sectionTitle": "Query", @@ -206,7 +206,7 @@ "kind": "section" }, { - "id": "88-auth#mutate", + "id": "89-auth#mutate", "title": "Authentication", "searchTitle": "Mutate", "sectionTitle": "Mutate", @@ -216,7 +216,7 @@ "kind": "section" }, { - "id": "89-auth#updating-tokens", + "id": "90-auth#updating-tokens", "title": "Authentication", "searchTitle": "Updating Tokens", "sectionTitle": "Updating Tokens", @@ -226,7 +226,7 @@ "kind": "section" }, { - "id": "90-auth#auth-failure-and-refresh", + "id": "91-auth#auth-failure-and-refresh", "title": "Authentication", "searchTitle": "Auth Failure and Refresh", "sectionTitle": "Auth Failure and Refresh", @@ -236,7 +236,7 @@ "kind": "section" }, { - "id": "91-auth#permission-patterns", + "id": "92-auth#permission-patterns", "title": "Authentication", "searchTitle": "Permission Patterns", "sectionTitle": "Permission Patterns", @@ -246,7 +246,7 @@ "kind": "section" }, { - "id": "92-auth#read-permissions", + "id": "93-auth#read-permissions", "title": "Authentication", "searchTitle": "Read Permissions", "sectionTitle": "Read Permissions", @@ -256,7 +256,7 @@ "kind": "section" }, { - "id": "93-auth#only-owned-rows", + "id": "94-auth#only-owned-rows", "title": "Authentication", "searchTitle": "Only Owned Rows", "sectionTitle": "Only Owned Rows", @@ -266,7 +266,7 @@ "kind": "section" }, { - "id": "94-auth#owned-or-shared-rows", + "id": "95-auth#owned-or-shared-rows", "title": "Authentication", "searchTitle": "Owned or Shared Rows", "sectionTitle": "Owned or Shared Rows", @@ -276,7 +276,7 @@ "kind": "section" }, { - "id": "95-auth#owned-rows-or-all-if-admin", + "id": "96-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": "96-auth#deny-by-returning-no-rows", + "id": "97-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": "97-auth#write-permissions", + "id": "98-auth#write-permissions", "title": "Authentication", "searchTitle": "Write Permissions", "sectionTitle": "Write Permissions", @@ -306,7 +306,7 @@ "kind": "section" }, { - "id": "98-auth#enforce-ownership", + "id": "99-auth#enforce-ownership", "title": "Authentication", "searchTitle": "Enforce Ownership", "sectionTitle": "Enforce Ownership", @@ -316,7 +316,7 @@ "kind": "section" }, { - "id": "99-auth#edit-owned-rows", + "id": "100-auth#edit-owned-rows", "title": "Authentication", "searchTitle": "Edit Owned Rows", "sectionTitle": "Edit Owned Rows", @@ -326,7 +326,7 @@ "kind": "section" }, { - "id": "100-auth#edit-owned-or-shared-rows", + "id": "101-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": "101-auth#edit-owned-or-all-if-admin", + "id": "102-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": "102-auth#logging-out", + "id": "103-auth#logging-out", "title": "Authentication", "searchTitle": "Logging Out", "sectionTitle": "Logging Out", @@ -383,7 +383,7 @@ "kind": "page" }, { - "id": "103-community#ui-frameworks", + "id": "104-community#ui-frameworks", "title": "From the Community", "searchTitle": "UI Frameworks", "sectionTitle": "UI Frameworks", @@ -393,7 +393,7 @@ "kind": "section" }, { - "id": "104-community#miscellaneous", + "id": "105-community#miscellaneous", "title": "From the Community", "searchTitle": "Miscellaneous", "sectionTitle": "Miscellaneous", @@ -517,7 +517,7 @@ "kind": "page" }, { - "id": "105-connecting-to-postgres#event-triggers", + "id": "106-connecting-to-postgres#event-triggers", "title": "Connecting to Postgres", "searchTitle": "Event Triggers", "sectionTitle": "Event Triggers", @@ -527,7 +527,7 @@ "kind": "section" }, { - "id": "106-connecting-to-postgres#configuration", + "id": "107-connecting-to-postgres#configuration", "title": "Connecting to Postgres", "searchTitle": "Configuration", "sectionTitle": "Configuration", @@ -537,7 +537,7 @@ "kind": "section" }, { - "id": "107-connecting-to-postgres#wal-level", + "id": "108-connecting-to-postgres#wal-level", "title": "Connecting to Postgres", "searchTitle": "WAL Level", "sectionTitle": "WAL Level", @@ -547,7 +547,7 @@ "kind": "section" }, { - "id": "108-connecting-to-postgres#change-database-stall-detection", + "id": "109-connecting-to-postgres#change-database-stall-detection", "title": "Connecting to Postgres", "searchTitle": "Change Database Stall Detection", "sectionTitle": "Change Database Stall Detection", @@ -557,7 +557,7 @@ "kind": "section" }, { - "id": "109-connecting-to-postgres#wal-sender-timeout", + "id": "110-connecting-to-postgres#wal-sender-timeout", "title": "Connecting to Postgres", "searchTitle": "WAL Sender Timeout", "sectionTitle": "WAL Sender Timeout", @@ -567,7 +567,7 @@ "kind": "section" }, { - "id": "110-connecting-to-postgres#bounding-wal-size", + "id": "111-connecting-to-postgres#bounding-wal-size", "title": "Connecting to Postgres", "searchTitle": "Bounding WAL Size", "sectionTitle": "Bounding WAL Size", @@ -577,7 +577,7 @@ "kind": "section" }, { - "id": "111-connecting-to-postgres#provider-specific-notes", + "id": "112-connecting-to-postgres#provider-specific-notes", "title": "Connecting to Postgres", "searchTitle": "Provider-Specific Notes", "sectionTitle": "Provider-Specific Notes", @@ -587,7 +587,7 @@ "kind": "section" }, { - "id": "112-connecting-to-postgres#planetscale-for-postgres", + "id": "113-connecting-to-postgres#planetscale-for-postgres", "title": "Connecting to Postgres", "searchTitle": "PlanetScale for Postgres", "sectionTitle": "PlanetScale for Postgres", @@ -597,7 +597,7 @@ "kind": "section" }, { - "id": "113-connecting-to-postgres#roles", + "id": "114-connecting-to-postgres#roles", "title": "Connecting to Postgres", "searchTitle": "Roles", "sectionTitle": "Roles", @@ -607,7 +607,7 @@ "kind": "section" }, { - "id": "114-connecting-to-postgres#connection-limits", + "id": "115-connecting-to-postgres#connection-limits", "title": "Connecting to Postgres", "searchTitle": "Connection Limits", "sectionTitle": "Connection Limits", @@ -617,7 +617,7 @@ "kind": "section" }, { - "id": "115-connecting-to-postgres#pooling", + "id": "116-connecting-to-postgres#pooling", "title": "Connecting to Postgres", "searchTitle": "Pooling", "sectionTitle": "Pooling", @@ -627,7 +627,7 @@ "kind": "section" }, { - "id": "116-connecting-to-postgres#high-availability", + "id": "117-connecting-to-postgres#high-availability", "title": "Connecting to Postgres", "searchTitle": "High Availability", "sectionTitle": "High Availability", @@ -637,7 +637,7 @@ "kind": "section" }, { - "id": "117-connecting-to-postgres#neon", + "id": "118-connecting-to-postgres#neon", "title": "Connecting to Postgres", "searchTitle": "Neon", "sectionTitle": "Neon", @@ -647,7 +647,7 @@ "kind": "section" }, { - "id": "118-connecting-to-postgres#logical-replication", + "id": "119-connecting-to-postgres#logical-replication", "title": "Connecting to Postgres", "searchTitle": "Logical Replication", "sectionTitle": "Logical Replication", @@ -657,7 +657,7 @@ "kind": "section" }, { - "id": "119-connecting-to-postgres#branching", + "id": "120-connecting-to-postgres#branching", "title": "Connecting to Postgres", "searchTitle": "Branching", "sectionTitle": "Branching", @@ -667,7 +667,7 @@ "kind": "section" }, { - "id": "120-connecting-to-postgres#flyio", + "id": "121-connecting-to-postgres#flyio", "title": "Connecting to Postgres", "searchTitle": "Fly.io", "sectionTitle": "Fly.io", @@ -677,7 +677,7 @@ "kind": "section" }, { - "id": "121-connecting-to-postgres#networking", + "id": "122-connecting-to-postgres#networking", "title": "Connecting to Postgres", "searchTitle": "Networking", "sectionTitle": "Networking", @@ -687,7 +687,7 @@ "kind": "section" }, { - "id": "122-connecting-to-postgres#permissions", + "id": "123-connecting-to-postgres#permissions", "title": "Connecting to Postgres", "searchTitle": "Permissions", "sectionTitle": "Permissions", @@ -697,7 +697,7 @@ "kind": "section" }, { - "id": "123-connecting-to-postgres#pooling-1", + "id": "124-connecting-to-postgres#pooling-1", "title": "Connecting to Postgres", "searchTitle": "Pooling", "sectionTitle": "Pooling", @@ -707,7 +707,7 @@ "kind": "section" }, { - "id": "124-connecting-to-postgres#supabase", + "id": "125-connecting-to-postgres#supabase", "title": "Connecting to Postgres", "searchTitle": "Supabase", "sectionTitle": "Supabase", @@ -717,7 +717,7 @@ "kind": "section" }, { - "id": "125-connecting-to-postgres#publication-changes", + "id": "126-connecting-to-postgres#publication-changes", "title": "Connecting to Postgres", "searchTitle": "Publication Changes", "sectionTitle": "Publication Changes", @@ -727,7 +727,7 @@ "kind": "section" }, { - "id": "126-connecting-to-postgres#ipv4", + "id": "127-connecting-to-postgres#ipv4", "title": "Connecting to Postgres", "searchTitle": "IPv4", "sectionTitle": "IPv4", @@ -737,7 +737,7 @@ "kind": "section" }, { - "id": "127-connecting-to-postgres#high-availability-1", + "id": "128-connecting-to-postgres#high-availability-1", "title": "Connecting to Postgres", "searchTitle": "High Availability", "sectionTitle": "High Availability", @@ -747,7 +747,7 @@ "kind": "section" }, { - "id": "128-connecting-to-postgres#render", + "id": "129-connecting-to-postgres#render", "title": "Connecting to Postgres", "searchTitle": "Render", "sectionTitle": "Render", @@ -757,7 +757,7 @@ "kind": "section" }, { - "id": "129-connecting-to-postgres#google-cloud-sql", + "id": "130-connecting-to-postgres#google-cloud-sql", "title": "Connecting to Postgres", "searchTitle": "Google Cloud SQL", "sectionTitle": "Google Cloud SQL", @@ -767,7 +767,7 @@ "kind": "section" }, { - "id": "130-connecting-to-postgres#schema-change-hooks", + "id": "131-connecting-to-postgres#schema-change-hooks", "title": "Connecting to Postgres", "searchTitle": "Schema Change Hooks", "sectionTitle": "Schema Change Hooks", @@ -859,7 +859,7 @@ "kind": "page" }, { - "id": "131-connection#overview", + "id": "132-connection#overview", "title": "Connection Status", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -869,7 +869,7 @@ "kind": "section" }, { - "id": "132-connection#usage", + "id": "133-connection#usage", "title": "Connection Status", "searchTitle": "Usage", "sectionTitle": "Usage", @@ -879,7 +879,7 @@ "kind": "section" }, { - "id": "133-connection#offline", + "id": "134-connection#offline", "title": "Connection Status", "searchTitle": "Offline", "sectionTitle": "Offline", @@ -889,7 +889,7 @@ "kind": "section" }, { - "id": "134-connection#offline-ui", + "id": "135-connection#offline-ui", "title": "Connection Status", "searchTitle": "Offline UI", "sectionTitle": "Offline UI", @@ -899,7 +899,7 @@ "kind": "section" }, { - "id": "135-connection#details", + "id": "136-connection#details", "title": "Connection Status", "searchTitle": "Details", "sectionTitle": "Details", @@ -909,7 +909,7 @@ "kind": "section" }, { - "id": "136-connection#initializing", + "id": "137-connection#initializing", "title": "Connection Status", "searchTitle": "Initializing", "sectionTitle": "Initializing", @@ -919,7 +919,7 @@ "kind": "section" }, { - "id": "137-connection#connecting", + "id": "138-connection#connecting", "title": "Connection Status", "searchTitle": "Connecting", "sectionTitle": "Connecting", @@ -929,7 +929,7 @@ "kind": "section" }, { - "id": "138-connection#connected", + "id": "139-connection#connected", "title": "Connection Status", "searchTitle": "Connected", "sectionTitle": "Connected", @@ -939,7 +939,7 @@ "kind": "section" }, { - "id": "139-connection#disconnected", + "id": "140-connection#disconnected", "title": "Connection Status", "searchTitle": "Disconnected", "sectionTitle": "Disconnected", @@ -949,7 +949,7 @@ "kind": "section" }, { - "id": "140-connection#error", + "id": "141-connection#error", "title": "Connection Status", "searchTitle": "Error", "sectionTitle": "Error", @@ -959,7 +959,7 @@ "kind": "section" }, { - "id": "141-connection#needs-auth", + "id": "142-connection#needs-auth", "title": "Connection Status", "searchTitle": "Needs-Auth", "sectionTitle": "Needs-Auth", @@ -969,7 +969,7 @@ "kind": "section" }, { - "id": "142-connection#closed", + "id": "143-connection#closed", "title": "Connection Status", "searchTitle": "Closed", "sectionTitle": "Closed", @@ -979,7 +979,7 @@ "kind": "section" }, { - "id": "143-connection#storage-failures", + "id": "144-connection#storage-failures", "title": "Connection Status", "searchTitle": "Storage Failures", "sectionTitle": "Storage Failures", @@ -989,7 +989,7 @@ "kind": "section" }, { - "id": "144-connection#checking-the-store", + "id": "145-connection#checking-the-store", "title": "Connection Status", "searchTitle": "Checking the Store", "sectionTitle": "Checking the Store", @@ -999,7 +999,7 @@ "kind": "section" }, { - "id": "145-connection#why-zero-doesnt-support-offline-writes", + "id": "146-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", @@ -1009,7 +1009,7 @@ "kind": "section" }, { - "id": "146-connection#example", + "id": "147-connection#example", "title": "Connection Status", "searchTitle": "Example", "sectionTitle": "Example", @@ -1019,7 +1019,7 @@ "kind": "section" }, { - "id": "147-connection#tradeoffs", + "id": "148-connection#tradeoffs", "title": "Connection Status", "searchTitle": "Tradeoffs", "sectionTitle": "Tradeoffs", @@ -1029,7 +1029,7 @@ "kind": "section" }, { - "id": "148-connection#zeros-position", + "id": "149-connection#zeros-position", "title": "Connection Status", "searchTitle": "Zero's Position", "sectionTitle": "Zero's Position", @@ -1077,7 +1077,7 @@ "kind": "page" }, { - "id": "149-debug/analyze-query-cli#set-up", + "id": "150-debug/analyze-query-cli#set-up", "title": "Analyze Query CLI", "searchTitle": "Set Up", "sectionTitle": "Set Up", @@ -1087,7 +1087,7 @@ "kind": "section" }, { - "id": "150-debug/analyze-query-cli#run-zql-queries", + "id": "151-debug/analyze-query-cli#run-zql-queries", "title": "Analyze Query CLI", "searchTitle": "Run ZQL Queries", "sectionTitle": "Run ZQL Queries", @@ -1097,7 +1097,7 @@ "kind": "section" }, { - "id": "151-debug/analyze-query-cli#production-use", + "id": "152-debug/analyze-query-cli#production-use", "title": "Analyze Query CLI", "searchTitle": "Production Use", "sectionTitle": "Production Use", @@ -1107,7 +1107,7 @@ "kind": "section" }, { - "id": "152-debug/analyze-query-cli#env-var-shorthand", + "id": "153-debug/analyze-query-cli#env-var-shorthand", "title": "Analyze Query CLI", "searchTitle": "Env Var Shorthand", "sectionTitle": "Env Var Shorthand", @@ -1117,7 +1117,7 @@ "kind": "section" }, { - "id": "153-debug/analyze-query-cli#other-input-modes", + "id": "154-debug/analyze-query-cli#other-input-modes", "title": "Analyze Query CLI", "searchTitle": "Other Input Modes", "sectionTitle": "Other Input Modes", @@ -1127,7 +1127,7 @@ "kind": "section" }, { - "id": "154-debug/analyze-query-cli#output", + "id": "155-debug/analyze-query-cli#output", "title": "Analyze Query CLI", "searchTitle": "Output", "sectionTitle": "Output", @@ -1137,7 +1137,7 @@ "kind": "section" }, { - "id": "155-debug/analyze-query-cli#optional-output", + "id": "156-debug/analyze-query-cli#optional-output", "title": "Analyze Query CLI", "searchTitle": "Optional Output", "sectionTitle": "Optional Output", @@ -1197,7 +1197,7 @@ "kind": "page" }, { - "id": "156-debug/inspector#accessing-the-inspector", + "id": "157-debug/inspector#accessing-the-inspector", "title": "Inspector", "searchTitle": "Accessing the Inspector", "sectionTitle": "Accessing the Inspector", @@ -1207,7 +1207,7 @@ "kind": "section" }, { - "id": "157-debug/inspector#clients-and-groups", + "id": "158-debug/inspector#clients-and-groups", "title": "Inspector", "searchTitle": "Clients and Groups", "sectionTitle": "Clients and Groups", @@ -1217,7 +1217,7 @@ "kind": "section" }, { - "id": "158-debug/inspector#queries", + "id": "159-debug/inspector#queries", "title": "Inspector", "searchTitle": "Queries", "sectionTitle": "Queries", @@ -1227,7 +1227,7 @@ "kind": "section" }, { - "id": "159-debug/inspector#analyzing-queries", + "id": "160-debug/inspector#analyzing-queries", "title": "Inspector", "searchTitle": "Analyzing Queries", "sectionTitle": "Analyzing Queries", @@ -1237,7 +1237,7 @@ "kind": "section" }, { - "id": "160-debug/inspector#interpreting-query-analysis", + "id": "161-debug/inspector#interpreting-query-analysis", "title": "Inspector", "searchTitle": "Interpreting Query Analysis", "sectionTitle": "Interpreting Query Analysis", @@ -1247,7 +1247,7 @@ "kind": "section" }, { - "id": "161-debug/inspector#viewing-sqlite-plans", + "id": "162-debug/inspector#viewing-sqlite-plans", "title": "Inspector", "searchTitle": "Viewing SQLite Plans", "sectionTitle": "Viewing SQLite Plans", @@ -1257,7 +1257,7 @@ "kind": "section" }, { - "id": "162-debug/inspector#viewing-zero-plans", + "id": "163-debug/inspector#viewing-zero-plans", "title": "Inspector", "searchTitle": "Viewing Zero Plans", "sectionTitle": "Viewing Zero Plans", @@ -1267,7 +1267,7 @@ "kind": "section" }, { - "id": "163-debug/inspector#analyzing-arbitrary-zql", + "id": "164-debug/inspector#analyzing-arbitrary-zql", "title": "Inspector", "searchTitle": "Analyzing Arbitrary ZQL", "sectionTitle": "Analyzing Arbitrary ZQL", @@ -1277,7 +1277,7 @@ "kind": "section" }, { - "id": "164-debug/inspector#table-data", + "id": "165-debug/inspector#table-data", "title": "Inspector", "searchTitle": "Table Data", "sectionTitle": "Table Data", @@ -1287,7 +1287,7 @@ "kind": "section" }, { - "id": "165-debug/inspector#server-version", + "id": "166-debug/inspector#server-version", "title": "Inspector", "searchTitle": "Server Version", "sectionTitle": "Server Version", @@ -1328,7 +1328,7 @@ "kind": "page" }, { - "id": "166-debug/replication#resetting", + "id": "167-debug/replication#resetting", "title": "Replication", "searchTitle": "Resetting", "sectionTitle": "Resetting", @@ -1338,7 +1338,7 @@ "kind": "section" }, { - "id": "167-debug/replication#inspecting", + "id": "168-debug/replication#inspecting", "title": "Replication", "searchTitle": "Inspecting", "sectionTitle": "Inspecting", @@ -1348,7 +1348,7 @@ "kind": "section" }, { - "id": "168-debug/replication#miscellaneous", + "id": "169-debug/replication#miscellaneous", "title": "Replication", "searchTitle": "Miscellaneous", "sectionTitle": "Miscellaneous", @@ -1388,7 +1388,7 @@ "kind": "page" }, { - "id": "169-debug/slow-queries#analyze-queries", + "id": "170-debug/slow-queries#analyze-queries", "title": "Slow Queries", "searchTitle": "Analyze Queries", "sectionTitle": "Analyze Queries", @@ -1398,7 +1398,7 @@ "kind": "section" }, { - "id": "170-debug/slow-queries#check-ttl", + "id": "171-debug/slow-queries#check-ttl", "title": "Slow Queries", "searchTitle": "Check ttl", "sectionTitle": "Check ttl", @@ -1408,7 +1408,7 @@ "kind": "section" }, { - "id": "171-debug/slow-queries#locality", + "id": "172-debug/slow-queries#locality", "title": "Slow Queries", "searchTitle": "Locality", "sectionTitle": "Locality", @@ -1418,7 +1418,7 @@ "kind": "section" }, { - "id": "172-debug/slow-queries#check-storage", + "id": "173-debug/slow-queries#check-storage", "title": "Slow Queries", "searchTitle": "Check Storage", "sectionTitle": "Check Storage", @@ -1428,7 +1428,7 @@ "kind": "section" }, { - "id": "173-debug/slow-queries#statz", + "id": "174-debug/slow-queries#statz", "title": "Slow Queries", "searchTitle": "/statz", "sectionTitle": "/statz", @@ -1461,7 +1461,7 @@ "kind": "page" }, { - "id": "174-deprecated/ad-hoc-queries#overview", + "id": "175-deprecated/ad-hoc-queries#overview", "title": "Ad-Hoc Queries (Deprecated)", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -1485,7 +1485,7 @@ "kind": "page" }, { - "id": "175-deprecated/crud-mutators#overview", + "id": "176-deprecated/crud-mutators#overview", "title": "CRUD Mutators (Deprecated)", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -1557,7 +1557,7 @@ "kind": "page" }, { - "id": "176-deprecated/rls-permissions#define-permissions", + "id": "177-deprecated/rls-permissions#define-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Define Permissions", "sectionTitle": "Define Permissions", @@ -1567,7 +1567,7 @@ "kind": "section" }, { - "id": "177-deprecated/rls-permissions#access-is-denied-by-default", + "id": "178-deprecated/rls-permissions#access-is-denied-by-default", "title": "RLS Permissions (Deprecated)", "searchTitle": "Access is Denied by Default", "sectionTitle": "Access is Denied by Default", @@ -1577,7 +1577,7 @@ "kind": "section" }, { - "id": "178-deprecated/rls-permissions#permission-evaluation", + "id": "179-deprecated/rls-permissions#permission-evaluation", "title": "RLS Permissions (Deprecated)", "searchTitle": "Permission Evaluation", "sectionTitle": "Permission Evaluation", @@ -1587,7 +1587,7 @@ "kind": "section" }, { - "id": "179-deprecated/rls-permissions#permission-deployment", + "id": "180-deprecated/rls-permissions#permission-deployment", "title": "RLS Permissions (Deprecated)", "searchTitle": "Permission Deployment", "sectionTitle": "Permission Deployment", @@ -1597,7 +1597,7 @@ "kind": "section" }, { - "id": "180-deprecated/rls-permissions#rules", + "id": "181-deprecated/rls-permissions#rules", "title": "RLS Permissions (Deprecated)", "searchTitle": "Rules", "sectionTitle": "Rules", @@ -1607,7 +1607,7 @@ "kind": "section" }, { - "id": "181-deprecated/rls-permissions#select-permissions", + "id": "182-deprecated/rls-permissions#select-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Select Permissions", "sectionTitle": "Select Permissions", @@ -1617,7 +1617,7 @@ "kind": "section" }, { - "id": "182-deprecated/rls-permissions#insert-permissions", + "id": "183-deprecated/rls-permissions#insert-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Insert Permissions", "sectionTitle": "Insert Permissions", @@ -1627,7 +1627,7 @@ "kind": "section" }, { - "id": "183-deprecated/rls-permissions#update-permissions", + "id": "184-deprecated/rls-permissions#update-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Update Permissions", "sectionTitle": "Update Permissions", @@ -1637,7 +1637,7 @@ "kind": "section" }, { - "id": "184-deprecated/rls-permissions#delete-permissions", + "id": "185-deprecated/rls-permissions#delete-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Delete Permissions", "sectionTitle": "Delete Permissions", @@ -1647,7 +1647,7 @@ "kind": "section" }, { - "id": "185-deprecated/rls-permissions#permissions-based-on-auth-data", + "id": "186-deprecated/rls-permissions#permissions-based-on-auth-data", "title": "RLS Permissions (Deprecated)", "searchTitle": "Permissions Based on Auth Data", "sectionTitle": "Permissions Based on Auth Data", @@ -1657,7 +1657,7 @@ "kind": "section" }, { - "id": "186-deprecated/rls-permissions#debugging", + "id": "187-deprecated/rls-permissions#debugging", "title": "RLS Permissions (Deprecated)", "searchTitle": "Debugging", "sectionTitle": "Debugging", @@ -1667,7 +1667,7 @@ "kind": "section" }, { - "id": "187-deprecated/rls-permissions#read-permissions", + "id": "188-deprecated/rls-permissions#read-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Read Permissions", "sectionTitle": "Read Permissions", @@ -1677,7 +1677,7 @@ "kind": "section" }, { - "id": "188-deprecated/rls-permissions#write-permissions", + "id": "189-deprecated/rls-permissions#write-permissions", "title": "RLS Permissions (Deprecated)", "searchTitle": "Write Permissions", "sectionTitle": "Write Permissions", @@ -1757,7 +1757,7 @@ "kind": "page" }, { - "id": "189-install#integrate-zero", + "id": "190-install#integrate-zero", "title": "Install Zero", "searchTitle": "Integrate Zero", "sectionTitle": "Integrate Zero", @@ -1767,7 +1767,7 @@ "kind": "section" }, { - "id": "190-install#set-up-your-database", + "id": "191-install#set-up-your-database", "title": "Install Zero", "searchTitle": "Set Up Your Database", "sectionTitle": "Set Up Your Database", @@ -1777,7 +1777,7 @@ "kind": "section" }, { - "id": "191-install#install-zero", + "id": "192-install#install-zero", "title": "Install Zero", "searchTitle": "Install Zero", "sectionTitle": "Install Zero", @@ -1787,7 +1787,7 @@ "kind": "section" }, { - "id": "192-install#set-up-your-zero-schema", + "id": "193-install#set-up-your-zero-schema", "title": "Install Zero", "searchTitle": "Set Up Your Zero Schema", "sectionTitle": "Set Up Your Zero Schema", @@ -1797,7 +1797,7 @@ "kind": "section" }, { - "id": "193-install#set-up-the-zero-client", + "id": "194-install#set-up-the-zero-client", "title": "Install Zero", "searchTitle": "Set Up the Zero Client", "sectionTitle": "Set Up the Zero Client", @@ -1807,7 +1807,7 @@ "kind": "section" }, { - "id": "194-install#sync-data", + "id": "195-install#sync-data", "title": "Install Zero", "searchTitle": "Sync Data", "sectionTitle": "Sync Data", @@ -1817,7 +1817,7 @@ "kind": "section" }, { - "id": "195-install#define-query", + "id": "196-install#define-query", "title": "Install Zero", "searchTitle": "Define Query", "sectionTitle": "Define Query", @@ -1827,7 +1827,7 @@ "kind": "section" }, { - "id": "196-install#add-query-endpoint", + "id": "197-install#add-query-endpoint", "title": "Install Zero", "searchTitle": "Add Query Endpoint", "sectionTitle": "Add Query Endpoint", @@ -1837,7 +1837,7 @@ "kind": "section" }, { - "id": "197-install#invoke-query", + "id": "198-install#invoke-query", "title": "Install Zero", "searchTitle": "Invoke Query", "sectionTitle": "Invoke Query", @@ -1847,7 +1847,7 @@ "kind": "section" }, { - "id": "198-install#more-about-queries", + "id": "199-install#more-about-queries", "title": "Install Zero", "searchTitle": "More about Queries", "sectionTitle": "More about Queries", @@ -1857,7 +1857,7 @@ "kind": "section" }, { - "id": "199-install#mutate-data", + "id": "200-install#mutate-data", "title": "Install Zero", "searchTitle": "Mutate Data", "sectionTitle": "Mutate Data", @@ -1867,7 +1867,7 @@ "kind": "section" }, { - "id": "200-install#define-mutators", + "id": "201-install#define-mutators", "title": "Install Zero", "searchTitle": "Define Mutators", "sectionTitle": "Define Mutators", @@ -1877,7 +1877,7 @@ "kind": "section" }, { - "id": "201-install#add-mutate-endpoint", + "id": "202-install#add-mutate-endpoint", "title": "Install Zero", "searchTitle": "Add Mutate Endpoint", "sectionTitle": "Add Mutate Endpoint", @@ -1887,7 +1887,7 @@ "kind": "section" }, { - "id": "202-install#invoke-mutators", + "id": "203-install#invoke-mutators", "title": "Install Zero", "searchTitle": "Invoke Mutators", "sectionTitle": "Invoke Mutators", @@ -1897,7 +1897,7 @@ "kind": "section" }, { - "id": "203-install#more-about-mutators", + "id": "204-install#more-about-mutators", "title": "Install Zero", "searchTitle": "More about Mutators", "sectionTitle": "More about Mutators", @@ -2050,7 +2050,7 @@ "kind": "page" }, { - "id": "204-mutators#architecture", + "id": "205-mutators#architecture", "title": "Mutators", "searchTitle": "Architecture", "sectionTitle": "Architecture", @@ -2060,7 +2060,7 @@ "kind": "section" }, { - "id": "205-mutators#life-of-a-mutation", + "id": "206-mutators#life-of-a-mutation", "title": "Mutators", "searchTitle": "Life of a Mutation", "sectionTitle": "Life of a Mutation", @@ -2070,7 +2070,7 @@ "kind": "section" }, { - "id": "206-mutators#defining-mutators", + "id": "207-mutators#defining-mutators", "title": "Mutators", "searchTitle": "Defining Mutators", "sectionTitle": "Defining Mutators", @@ -2080,7 +2080,7 @@ "kind": "section" }, { - "id": "207-mutators#basics", + "id": "208-mutators#basics", "title": "Mutators", "searchTitle": "Basics", "sectionTitle": "Basics", @@ -2090,7 +2090,7 @@ "kind": "section" }, { - "id": "208-mutators#writing-data", + "id": "209-mutators#writing-data", "title": "Mutators", "searchTitle": "Writing Data", "sectionTitle": "Writing Data", @@ -2100,7 +2100,7 @@ "kind": "section" }, { - "id": "209-mutators#insert", + "id": "210-mutators#insert", "title": "Mutators", "searchTitle": "Insert", "sectionTitle": "Insert", @@ -2110,7 +2110,7 @@ "kind": "section" }, { - "id": "210-mutators#upsert", + "id": "211-mutators#upsert", "title": "Mutators", "searchTitle": "Upsert", "sectionTitle": "Upsert", @@ -2120,7 +2120,7 @@ "kind": "section" }, { - "id": "211-mutators#update", + "id": "212-mutators#update", "title": "Mutators", "searchTitle": "Update", "sectionTitle": "Update", @@ -2130,7 +2130,7 @@ "kind": "section" }, { - "id": "212-mutators#delete", + "id": "213-mutators#delete", "title": "Mutators", "searchTitle": "Delete", "sectionTitle": "Delete", @@ -2140,7 +2140,7 @@ "kind": "section" }, { - "id": "213-mutators#arguments", + "id": "214-mutators#arguments", "title": "Mutators", "searchTitle": "Arguments", "sectionTitle": "Arguments", @@ -2150,7 +2150,7 @@ "kind": "section" }, { - "id": "214-mutators#reading-data", + "id": "215-mutators#reading-data", "title": "Mutators", "searchTitle": "Reading Data", "sectionTitle": "Reading Data", @@ -2160,7 +2160,7 @@ "kind": "section" }, { - "id": "215-mutators#context", + "id": "216-mutators#context", "title": "Mutators", "searchTitle": "Context", "sectionTitle": "Context", @@ -2170,7 +2170,7 @@ "kind": "section" }, { - "id": "216-mutators#mutator-registries", + "id": "217-mutators#mutator-registries", "title": "Mutators", "searchTitle": "Mutator Registries", "sectionTitle": "Mutator Registries", @@ -2180,7 +2180,7 @@ "kind": "section" }, { - "id": "217-mutators#mutator-names", + "id": "218-mutators#mutator-names", "title": "Mutators", "searchTitle": "Mutator Names", "sectionTitle": "Mutator Names", @@ -2190,7 +2190,7 @@ "kind": "section" }, { - "id": "218-mutators#mutatorsts", + "id": "219-mutators#mutatorsts", "title": "Mutators", "searchTitle": "mutators.ts", "sectionTitle": "mutators.ts", @@ -2200,7 +2200,7 @@ "kind": "section" }, { - "id": "219-mutators#registration", + "id": "220-mutators#registration", "title": "Mutators", "searchTitle": "Registration", "sectionTitle": "Registration", @@ -2210,7 +2210,7 @@ "kind": "section" }, { - "id": "220-mutators#server-setup", + "id": "221-mutators#server-setup", "title": "Mutators", "searchTitle": "Server Setup", "sectionTitle": "Server Setup", @@ -2220,7 +2220,7 @@ "kind": "section" }, { - "id": "221-mutators#registering-the-endpoint", + "id": "222-mutators#registering-the-endpoint", "title": "Mutators", "searchTitle": "Registering the Endpoint", "sectionTitle": "Registering the Endpoint", @@ -2230,7 +2230,7 @@ "kind": "section" }, { - "id": "222-mutators#implementing-the-endpoint", + "id": "223-mutators#implementing-the-endpoint", "title": "Mutators", "searchTitle": "Implementing the Endpoint", "sectionTitle": "Implementing the Endpoint", @@ -2240,7 +2240,7 @@ "kind": "section" }, { - "id": "223-mutators#handling-errors", + "id": "224-mutators#handling-errors", "title": "Mutators", "searchTitle": "Handling Errors", "sectionTitle": "Handling Errors", @@ -2250,7 +2250,7 @@ "kind": "section" }, { - "id": "224-mutators#retrying-mutators", + "id": "225-mutators#retrying-mutators", "title": "Mutators", "searchTitle": "Retrying Mutators", "sectionTitle": "Retrying Mutators", @@ -2260,7 +2260,7 @@ "kind": "section" }, { - "id": "225-mutators#custom-mutate-url", + "id": "226-mutators#custom-mutate-url", "title": "Mutators", "searchTitle": "Custom Mutate URL", "sectionTitle": "Custom Mutate URL", @@ -2270,7 +2270,7 @@ "kind": "section" }, { - "id": "226-mutators#url-patterns", + "id": "227-mutators#url-patterns", "title": "Mutators", "searchTitle": "URL Patterns", "sectionTitle": "URL Patterns", @@ -2280,7 +2280,7 @@ "kind": "section" }, { - "id": "227-mutators#server-specific-code", + "id": "228-mutators#server-specific-code", "title": "Mutators", "searchTitle": "Server-Specific Code", "sectionTitle": "Server-Specific Code", @@ -2290,7 +2290,7 @@ "kind": "section" }, { - "id": "228-mutators#running-mutators", + "id": "229-mutators#running-mutators", "title": "Mutators", "searchTitle": "Running Mutators", "sectionTitle": "Running Mutators", @@ -2300,7 +2300,7 @@ "kind": "section" }, { - "id": "229-mutators#waiting-for-results", + "id": "230-mutators#waiting-for-results", "title": "Mutators", "searchTitle": "Waiting for Results", "sectionTitle": "Waiting for Results", @@ -2310,7 +2310,7 @@ "kind": "section" }, { - "id": "230-mutators#returning-data", + "id": "231-mutators#returning-data", "title": "Mutators", "searchTitle": "Returning Data", "sectionTitle": "Returning Data", @@ -2320,7 +2320,7 @@ "kind": "section" }, { - "id": "231-mutators#permissions", + "id": "232-mutators#permissions", "title": "Mutators", "searchTitle": "Permissions", "sectionTitle": "Permissions", @@ -2330,7 +2330,7 @@ "kind": "section" }, { - "id": "232-mutators#dropping-down-to-raw-sql", + "id": "233-mutators#dropping-down-to-raw-sql", "title": "Mutators", "searchTitle": "Dropping Down to Raw SQL", "sectionTitle": "Dropping Down to Raw SQL", @@ -2340,7 +2340,7 @@ "kind": "section" }, { - "id": "233-mutators#notifications-and-async-work", + "id": "234-mutators#notifications-and-async-work", "title": "Mutators", "searchTitle": "Notifications and Async Work", "sectionTitle": "Notifications and Async Work", @@ -2350,7 +2350,7 @@ "kind": "section" }, { - "id": "234-mutators#custom-mutate-implementation", + "id": "235-mutators#custom-mutate-implementation", "title": "Mutators", "searchTitle": "Custom Mutate Implementation", "sectionTitle": "Custom Mutate Implementation", @@ -2374,7 +2374,7 @@ "kind": "page" }, { - "id": "235-open-source#business-model", + "id": "236-open-source#business-model", "title": "Zero is Open Source Software", "searchTitle": "Business Model", "sectionTitle": "Business Model", @@ -2426,7 +2426,7 @@ "kind": "page" }, { - "id": "236-otel#grafana-cloud-walkthrough", + "id": "237-otel#grafana-cloud-walkthrough", "title": "OpenTelemetry", "searchTitle": "Grafana Cloud Walkthrough", "sectionTitle": "Grafana Cloud Walkthrough", @@ -2436,7 +2436,7 @@ "kind": "section" }, { - "id": "237-otel#distributed-tracing", + "id": "238-otel#distributed-tracing", "title": "OpenTelemetry", "searchTitle": "Distributed Tracing", "sectionTitle": "Distributed Tracing", @@ -2446,7 +2446,7 @@ "kind": "section" }, { - "id": "238-otel#metrics-reference", + "id": "239-otel#metrics-reference", "title": "OpenTelemetry", "searchTitle": "Metrics Reference", "sectionTitle": "Metrics Reference", @@ -2456,7 +2456,7 @@ "kind": "section" }, { - "id": "239-otel#zeroserver", + "id": "240-otel#zeroserver", "title": "OpenTelemetry", "searchTitle": "zero.server", "sectionTitle": "zero.server", @@ -2466,7 +2466,7 @@ "kind": "section" }, { - "id": "240-otel#zeroreplica", + "id": "241-otel#zeroreplica", "title": "OpenTelemetry", "searchTitle": "zero.replica", "sectionTitle": "zero.replica", @@ -2476,7 +2476,7 @@ "kind": "section" }, { - "id": "241-otel#zeroreplication", + "id": "242-otel#zeroreplication", "title": "OpenTelemetry", "searchTitle": "zero.replication", "sectionTitle": "zero.replication", @@ -2486,7 +2486,7 @@ "kind": "section" }, { - "id": "242-otel#zerosync", + "id": "243-otel#zerosync", "title": "OpenTelemetry", "searchTitle": "zero.sync", "sectionTitle": "zero.sync", @@ -2496,7 +2496,7 @@ "kind": "section" }, { - "id": "243-otel#zeromutation", + "id": "244-otel#zeromutation", "title": "OpenTelemetry", "searchTitle": "zero.mutation", "sectionTitle": "zero.mutation", @@ -2556,7 +2556,7 @@ "kind": "page" }, { - "id": "244-postgres-support#object-names", + "id": "245-postgres-support#object-names", "title": "Supported Postgres Features", "searchTitle": "Object Names", "sectionTitle": "Object Names", @@ -2566,7 +2566,7 @@ "kind": "section" }, { - "id": "245-postgres-support#object-types", + "id": "246-postgres-support#object-types", "title": "Supported Postgres Features", "searchTitle": "Object Types", "sectionTitle": "Object Types", @@ -2576,7 +2576,7 @@ "kind": "section" }, { - "id": "246-postgres-support#column-types", + "id": "247-postgres-support#column-types", "title": "Supported Postgres Features", "searchTitle": "Column Types", "sectionTitle": "Column Types", @@ -2586,7 +2586,7 @@ "kind": "section" }, { - "id": "247-postgres-support#column-defaults", + "id": "248-postgres-support#column-defaults", "title": "Supported Postgres Features", "searchTitle": "Column Defaults", "sectionTitle": "Column Defaults", @@ -2596,7 +2596,7 @@ "kind": "section" }, { - "id": "248-postgres-support#ids", + "id": "249-postgres-support#ids", "title": "Supported Postgres Features", "searchTitle": "IDs", "sectionTitle": "IDs", @@ -2606,7 +2606,7 @@ "kind": "section" }, { - "id": "249-postgres-support#primary-keys", + "id": "250-postgres-support#primary-keys", "title": "Supported Postgres Features", "searchTitle": "Primary Keys", "sectionTitle": "Primary Keys", @@ -2616,7 +2616,7 @@ "kind": "section" }, { - "id": "250-postgres-support#limiting-replication", + "id": "251-postgres-support#limiting-replication", "title": "Supported Postgres Features", "searchTitle": "Limiting Replication", "sectionTitle": "Limiting Replication", @@ -2626,7 +2626,7 @@ "kind": "section" }, { - "id": "251-postgres-support#zero-cache-replication", + "id": "252-postgres-support#zero-cache-replication", "title": "Supported Postgres Features", "searchTitle": "zero-cache replication", "sectionTitle": "zero-cache replication", @@ -2636,7 +2636,7 @@ "kind": "section" }, { - "id": "252-postgres-support#browser-client-replication", + "id": "253-postgres-support#browser-client-replication", "title": "Supported Postgres Features", "searchTitle": "Browser client replication", "sectionTitle": "Browser client replication", @@ -2646,7 +2646,7 @@ "kind": "section" }, { - "id": "253-postgres-support#schema-changes", + "id": "254-postgres-support#schema-changes", "title": "Supported Postgres Features", "searchTitle": "Schema changes", "sectionTitle": "Schema changes", @@ -2682,7 +2682,7 @@ "kind": "page" }, { - "id": "254-previews#overview", + "id": "255-previews#overview", "title": "Previews", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -2692,7 +2692,7 @@ "kind": "section" }, { - "id": "255-previews#configure-allowed-endpoint-patterns", + "id": "256-previews#configure-allowed-endpoint-patterns", "title": "Previews", "searchTitle": "Configure Allowed Endpoint Patterns", "sectionTitle": "Configure Allowed Endpoint Patterns", @@ -2702,7 +2702,7 @@ "kind": "section" }, { - "id": "256-previews#choose-endpoint-urls-in-the-client", + "id": "257-previews#choose-endpoint-urls-in-the-client", "title": "Previews", "searchTitle": "Choose Endpoint URLs in the Client", "sectionTitle": "Choose Endpoint URLs in the Client", @@ -2712,7 +2712,7 @@ "kind": "section" }, { - "id": "257-previews#schema-changes-in-previews", + "id": "258-previews#schema-changes-in-previews", "title": "Previews", "searchTitle": "Schema Changes in Previews", "sectionTitle": "Schema Changes in Previews", @@ -2860,7 +2860,7 @@ "kind": "page" }, { - "id": "258-queries#architecture", + "id": "259-queries#architecture", "title": "Queries", "searchTitle": "Architecture", "sectionTitle": "Architecture", @@ -2870,7 +2870,7 @@ "kind": "section" }, { - "id": "259-queries#life-of-a-query", + "id": "260-queries#life-of-a-query", "title": "Queries", "searchTitle": "Life of a Query", "sectionTitle": "Life of a Query", @@ -2880,7 +2880,7 @@ "kind": "section" }, { - "id": "260-queries#defining-queries", + "id": "261-queries#defining-queries", "title": "Queries", "searchTitle": "Defining Queries", "sectionTitle": "Defining Queries", @@ -2890,7 +2890,7 @@ "kind": "section" }, { - "id": "261-queries#basics", + "id": "262-queries#basics", "title": "Queries", "searchTitle": "Basics", "sectionTitle": "Basics", @@ -2900,7 +2900,7 @@ "kind": "section" }, { - "id": "262-queries#arguments", + "id": "263-queries#arguments", "title": "Queries", "searchTitle": "Arguments", "sectionTitle": "Arguments", @@ -2910,7 +2910,7 @@ "kind": "section" }, { - "id": "263-queries#query-registries", + "id": "264-queries#query-registries", "title": "Queries", "searchTitle": "Query Registries", "sectionTitle": "Query Registries", @@ -2920,7 +2920,7 @@ "kind": "section" }, { - "id": "264-queries#query-names", + "id": "265-queries#query-names", "title": "Queries", "searchTitle": "Query Names", "sectionTitle": "Query Names", @@ -2930,7 +2930,7 @@ "kind": "section" }, { - "id": "265-queries#context", + "id": "266-queries#context", "title": "Queries", "searchTitle": "Context", "sectionTitle": "Context", @@ -2940,7 +2940,7 @@ "kind": "section" }, { - "id": "266-queries#queriests", + "id": "267-queries#queriests", "title": "Queries", "searchTitle": "queries.ts", "sectionTitle": "queries.ts", @@ -2950,7 +2950,7 @@ "kind": "section" }, { - "id": "267-queries#server-setup", + "id": "268-queries#server-setup", "title": "Queries", "searchTitle": "Server Setup", "sectionTitle": "Server Setup", @@ -2960,7 +2960,7 @@ "kind": "section" }, { - "id": "268-queries#registering-the-endpoint", + "id": "269-queries#registering-the-endpoint", "title": "Queries", "searchTitle": "Registering the Endpoint", "sectionTitle": "Registering the Endpoint", @@ -2970,7 +2970,7 @@ "kind": "section" }, { - "id": "269-queries#implementing-the-endpoint", + "id": "270-queries#implementing-the-endpoint", "title": "Queries", "searchTitle": "Implementing the Endpoint", "sectionTitle": "Implementing the Endpoint", @@ -2980,7 +2980,7 @@ "kind": "section" }, { - "id": "270-queries#custom-query-url", + "id": "271-queries#custom-query-url", "title": "Queries", "searchTitle": "Custom Query URL", "sectionTitle": "Custom Query URL", @@ -2990,7 +2990,7 @@ "kind": "section" }, { - "id": "271-queries#url-patterns", + "id": "272-queries#url-patterns", "title": "Queries", "searchTitle": "URL Patterns", "sectionTitle": "URL Patterns", @@ -3000,7 +3000,7 @@ "kind": "section" }, { - "id": "272-queries#running-queries", + "id": "273-queries#running-queries", "title": "Queries", "searchTitle": "Running Queries", "sectionTitle": "Running Queries", @@ -3010,7 +3010,7 @@ "kind": "section" }, { - "id": "273-queries#reactively", + "id": "274-queries#reactively", "title": "Queries", "searchTitle": "Reactively", "sectionTitle": "Reactively", @@ -3020,7 +3020,7 @@ "kind": "section" }, { - "id": "274-queries#conditionally", + "id": "275-queries#conditionally", "title": "Queries", "searchTitle": "Conditionally", "sectionTitle": "Conditionally", @@ -3030,7 +3030,7 @@ "kind": "section" }, { - "id": "275-queries#once", + "id": "276-queries#once", "title": "Queries", "searchTitle": "Once", "sectionTitle": "Once", @@ -3040,7 +3040,7 @@ "kind": "section" }, { - "id": "276-queries#for-preloading", + "id": "277-queries#for-preloading", "title": "Queries", "searchTitle": "For Preloading", "sectionTitle": "For Preloading", @@ -3050,7 +3050,7 @@ "kind": "section" }, { - "id": "277-queries#missing-data", + "id": "278-queries#missing-data", "title": "Queries", "searchTitle": "Missing Data", "sectionTitle": "Missing Data", @@ -3060,7 +3060,7 @@ "kind": "section" }, { - "id": "278-queries#partial-data", + "id": "279-queries#partial-data", "title": "Queries", "searchTitle": "Partial Data", "sectionTitle": "Partial Data", @@ -3070,7 +3070,7 @@ "kind": "section" }, { - "id": "279-queries#cached-results", + "id": "280-queries#cached-results", "title": "Queries", "searchTitle": "Cached Results", "sectionTitle": "Cached Results", @@ -3080,7 +3080,7 @@ "kind": "section" }, { - "id": "280-queries#handling-errors", + "id": "281-queries#handling-errors", "title": "Queries", "searchTitle": "Handling Errors", "sectionTitle": "Handling Errors", @@ -3090,7 +3090,7 @@ "kind": "section" }, { - "id": "281-queries#granular-updates", + "id": "282-queries#granular-updates", "title": "Queries", "searchTitle": "Granular Updates", "sectionTitle": "Granular Updates", @@ -3100,7 +3100,7 @@ "kind": "section" }, { - "id": "282-queries#query-caching", + "id": "283-queries#query-caching", "title": "Queries", "searchTitle": "Query Caching", "sectionTitle": "Query Caching", @@ -3110,7 +3110,7 @@ "kind": "section" }, { - "id": "283-queries#ttls", + "id": "284-queries#ttls", "title": "Queries", "searchTitle": "TTLs", "sectionTitle": "TTLs", @@ -3120,7 +3120,7 @@ "kind": "section" }, { - "id": "284-queries#ttl-defaults", + "id": "285-queries#ttl-defaults", "title": "Queries", "searchTitle": "TTL Defaults", "sectionTitle": "TTL Defaults", @@ -3130,7 +3130,7 @@ "kind": "section" }, { - "id": "285-queries#setting-different-ttls", + "id": "286-queries#setting-different-ttls", "title": "Queries", "searchTitle": "Setting Different TTLs", "sectionTitle": "Setting Different TTLs", @@ -3140,7 +3140,7 @@ "kind": "section" }, { - "id": "286-queries#why-zero-ttls-are-short", + "id": "287-queries#why-zero-ttls-are-short", "title": "Queries", "searchTitle": "Why Zero TTLs are Short", "sectionTitle": "Why Zero TTLs are Short", @@ -3150,7 +3150,7 @@ "kind": "section" }, { - "id": "287-queries#local-only-queries", + "id": "288-queries#local-only-queries", "title": "Queries", "searchTitle": "Local-Only Queries", "sectionTitle": "Local-Only Queries", @@ -3160,7 +3160,7 @@ "kind": "section" }, { - "id": "288-queries#custom-server-implementation", + "id": "289-queries#custom-server-implementation", "title": "Queries", "searchTitle": "Custom Server Implementation", "sectionTitle": "Custom Server Implementation", @@ -3170,7 +3170,7 @@ "kind": "section" }, { - "id": "289-queries#consistency", + "id": "290-queries#consistency", "title": "Queries", "searchTitle": "Consistency", "sectionTitle": "Consistency", @@ -3202,7 +3202,7 @@ "kind": "page" }, { - "id": "290-quickstart#hello-zero-solid", + "id": "291-quickstart#hello-zero-solid", "title": "Quickstart", "searchTitle": "hello-zero-solid", "sectionTitle": "hello-zero-solid", @@ -3212,7 +3212,7 @@ "kind": "section" }, { - "id": "291-quickstart#hello-zero-cf", + "id": "292-quickstart#hello-zero-cf", "title": "Quickstart", "searchTitle": "hello-zero-cf", "sectionTitle": "hello-zero-cf", @@ -3222,7 +3222,7 @@ "kind": "section" }, { - "id": "292-quickstart#hello-zero", + "id": "293-quickstart#hello-zero", "title": "Quickstart", "searchTitle": "hello-zero", "sectionTitle": "hello-zero", @@ -3271,7 +3271,7 @@ "kind": "page" }, { - "id": "293-react#setup", + "id": "294-react#setup", "title": "React", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -3281,7 +3281,7 @@ "kind": "section" }, { - "id": "294-react#usage", + "id": "295-react#usage", "title": "React", "searchTitle": "Usage", "sectionTitle": "Usage", @@ -3291,7 +3291,7 @@ "kind": "section" }, { - "id": "295-react#suspense", + "id": "296-react#suspense", "title": "React", "searchTitle": "Suspense", "sectionTitle": "Suspense", @@ -3301,7 +3301,7 @@ "kind": "section" }, { - "id": "296-react#query-identity", + "id": "297-react#query-identity", "title": "React", "searchTitle": "Query Identity", "sectionTitle": "Query Identity", @@ -3311,7 +3311,7 @@ "kind": "section" }, { - "id": "297-react#examples", + "id": "298-react#examples", "title": "React", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -3343,7 +3343,7 @@ "kind": "page" }, { - "id": "298-release-notes/0.1#breaking-changes", + "id": "299-release-notes/0.1#breaking-changes", "title": "Zero 0.1", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -3353,7 +3353,7 @@ "kind": "section" }, { - "id": "299-release-notes/0.1#features", + "id": "300-release-notes/0.1#features", "title": "Zero 0.1", "searchTitle": "Features", "sectionTitle": "Features", @@ -3363,7 +3363,7 @@ "kind": "section" }, { - "id": "300-release-notes/0.1#source-tree-fixes", + "id": "301-release-notes/0.1#source-tree-fixes", "title": "Zero 0.1", "searchTitle": "Source tree fixes", "sectionTitle": "Source tree fixes", @@ -3399,7 +3399,7 @@ "kind": "page" }, { - "id": "301-release-notes/0.10#install", + "id": "302-release-notes/0.10#install", "title": "Zero 0.10", "searchTitle": "Install", "sectionTitle": "Install", @@ -3409,7 +3409,7 @@ "kind": "section" }, { - "id": "302-release-notes/0.10#features", + "id": "303-release-notes/0.10#features", "title": "Zero 0.10", "searchTitle": "Features", "sectionTitle": "Features", @@ -3419,7 +3419,7 @@ "kind": "section" }, { - "id": "303-release-notes/0.10#fixes", + "id": "304-release-notes/0.10#fixes", "title": "Zero 0.10", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3429,7 +3429,7 @@ "kind": "section" }, { - "id": "304-release-notes/0.10#breaking-changes", + "id": "305-release-notes/0.10#breaking-changes", "title": "Zero 0.10", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3465,7 +3465,7 @@ "kind": "page" }, { - "id": "305-release-notes/0.11#install", + "id": "306-release-notes/0.11#install", "title": "Zero 0.11", "searchTitle": "Install", "sectionTitle": "Install", @@ -3475,7 +3475,7 @@ "kind": "section" }, { - "id": "306-release-notes/0.11#features", + "id": "307-release-notes/0.11#features", "title": "Zero 0.11", "searchTitle": "Features", "sectionTitle": "Features", @@ -3485,7 +3485,7 @@ "kind": "section" }, { - "id": "307-release-notes/0.11#fixes", + "id": "308-release-notes/0.11#fixes", "title": "Zero 0.11", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3495,7 +3495,7 @@ "kind": "section" }, { - "id": "308-release-notes/0.11#breaking-changes", + "id": "309-release-notes/0.11#breaking-changes", "title": "Zero 0.11", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3531,7 +3531,7 @@ "kind": "page" }, { - "id": "309-release-notes/0.12#install", + "id": "310-release-notes/0.12#install", "title": "Zero 0.12", "searchTitle": "Install", "sectionTitle": "Install", @@ -3541,7 +3541,7 @@ "kind": "section" }, { - "id": "310-release-notes/0.12#features", + "id": "311-release-notes/0.12#features", "title": "Zero 0.12", "searchTitle": "Features", "sectionTitle": "Features", @@ -3551,7 +3551,7 @@ "kind": "section" }, { - "id": "311-release-notes/0.12#fixes", + "id": "312-release-notes/0.12#fixes", "title": "Zero 0.12", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3561,7 +3561,7 @@ "kind": "section" }, { - "id": "312-release-notes/0.12#breaking-changes", + "id": "313-release-notes/0.12#breaking-changes", "title": "Zero 0.12", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3597,7 +3597,7 @@ "kind": "page" }, { - "id": "313-release-notes/0.13#install", + "id": "314-release-notes/0.13#install", "title": "Zero 0.13", "searchTitle": "Install", "sectionTitle": "Install", @@ -3607,7 +3607,7 @@ "kind": "section" }, { - "id": "314-release-notes/0.13#features", + "id": "315-release-notes/0.13#features", "title": "Zero 0.13", "searchTitle": "Features", "sectionTitle": "Features", @@ -3617,7 +3617,7 @@ "kind": "section" }, { - "id": "315-release-notes/0.13#fixes", + "id": "316-release-notes/0.13#fixes", "title": "Zero 0.13", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3627,7 +3627,7 @@ "kind": "section" }, { - "id": "316-release-notes/0.13#breaking-changes", + "id": "317-release-notes/0.13#breaking-changes", "title": "Zero 0.13", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3663,7 +3663,7 @@ "kind": "page" }, { - "id": "317-release-notes/0.14#install", + "id": "318-release-notes/0.14#install", "title": "Zero 0.14", "searchTitle": "Install", "sectionTitle": "Install", @@ -3673,7 +3673,7 @@ "kind": "section" }, { - "id": "318-release-notes/0.14#features", + "id": "319-release-notes/0.14#features", "title": "Zero 0.14", "searchTitle": "Features", "sectionTitle": "Features", @@ -3683,7 +3683,7 @@ "kind": "section" }, { - "id": "319-release-notes/0.14#fixes", + "id": "320-release-notes/0.14#fixes", "title": "Zero 0.14", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3693,7 +3693,7 @@ "kind": "section" }, { - "id": "320-release-notes/0.14#breaking-changes", + "id": "321-release-notes/0.14#breaking-changes", "title": "Zero 0.14", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3733,7 +3733,7 @@ "kind": "page" }, { - "id": "321-release-notes/0.15#install", + "id": "322-release-notes/0.15#install", "title": "Zero 0.15", "searchTitle": "Install", "sectionTitle": "Install", @@ -3743,7 +3743,7 @@ "kind": "section" }, { - "id": "322-release-notes/0.15#upgrade-guide", + "id": "323-release-notes/0.15#upgrade-guide", "title": "Zero 0.15", "searchTitle": "Upgrade Guide", "sectionTitle": "Upgrade Guide", @@ -3753,7 +3753,7 @@ "kind": "section" }, { - "id": "323-release-notes/0.15#features", + "id": "324-release-notes/0.15#features", "title": "Zero 0.15", "searchTitle": "Features", "sectionTitle": "Features", @@ -3763,7 +3763,7 @@ "kind": "section" }, { - "id": "324-release-notes/0.15#fixes", + "id": "325-release-notes/0.15#fixes", "title": "Zero 0.15", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3773,7 +3773,7 @@ "kind": "section" }, { - "id": "325-release-notes/0.15#breaking-changes", + "id": "326-release-notes/0.15#breaking-changes", "title": "Zero 0.15", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3813,7 +3813,7 @@ "kind": "page" }, { - "id": "326-release-notes/0.16#install", + "id": "327-release-notes/0.16#install", "title": "Zero 0.16", "searchTitle": "Install", "sectionTitle": "Install", @@ -3823,7 +3823,7 @@ "kind": "section" }, { - "id": "327-release-notes/0.16#upgrading", + "id": "328-release-notes/0.16#upgrading", "title": "Zero 0.16", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -3833,7 +3833,7 @@ "kind": "section" }, { - "id": "328-release-notes/0.16#features", + "id": "329-release-notes/0.16#features", "title": "Zero 0.16", "searchTitle": "Features", "sectionTitle": "Features", @@ -3843,7 +3843,7 @@ "kind": "section" }, { - "id": "329-release-notes/0.16#fixes", + "id": "330-release-notes/0.16#fixes", "title": "Zero 0.16", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3853,7 +3853,7 @@ "kind": "section" }, { - "id": "330-release-notes/0.16#breaking-changes", + "id": "331-release-notes/0.16#breaking-changes", "title": "Zero 0.16", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3893,7 +3893,7 @@ "kind": "page" }, { - "id": "331-release-notes/0.17#install", + "id": "332-release-notes/0.17#install", "title": "Zero 0.17", "searchTitle": "Install", "sectionTitle": "Install", @@ -3903,7 +3903,7 @@ "kind": "section" }, { - "id": "332-release-notes/0.17#upgrading", + "id": "333-release-notes/0.17#upgrading", "title": "Zero 0.17", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -3913,7 +3913,7 @@ "kind": "section" }, { - "id": "333-release-notes/0.17#features", + "id": "334-release-notes/0.17#features", "title": "Zero 0.17", "searchTitle": "Features", "sectionTitle": "Features", @@ -3923,7 +3923,7 @@ "kind": "section" }, { - "id": "334-release-notes/0.17#fixes", + "id": "335-release-notes/0.17#fixes", "title": "Zero 0.17", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -3933,7 +3933,7 @@ "kind": "section" }, { - "id": "335-release-notes/0.17#breaking-changes", + "id": "336-release-notes/0.17#breaking-changes", "title": "Zero 0.17", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -3973,7 +3973,7 @@ "kind": "page" }, { - "id": "336-release-notes/0.18#install", + "id": "337-release-notes/0.18#install", "title": "Zero 0.18", "searchTitle": "Install", "sectionTitle": "Install", @@ -3983,7 +3983,7 @@ "kind": "section" }, { - "id": "337-release-notes/0.18#upgrading", + "id": "338-release-notes/0.18#upgrading", "title": "Zero 0.18", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -3993,7 +3993,7 @@ "kind": "section" }, { - "id": "338-release-notes/0.18#features", + "id": "339-release-notes/0.18#features", "title": "Zero 0.18", "searchTitle": "Features", "sectionTitle": "Features", @@ -4003,7 +4003,7 @@ "kind": "section" }, { - "id": "339-release-notes/0.18#fixes", + "id": "340-release-notes/0.18#fixes", "title": "Zero 0.18", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4013,7 +4013,7 @@ "kind": "section" }, { - "id": "340-release-notes/0.18#breaking-changes", + "id": "341-release-notes/0.18#breaking-changes", "title": "Zero 0.18", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4053,7 +4053,7 @@ "kind": "page" }, { - "id": "341-release-notes/0.19#install", + "id": "342-release-notes/0.19#install", "title": "Zero 0.19", "searchTitle": "Install", "sectionTitle": "Install", @@ -4063,7 +4063,7 @@ "kind": "section" }, { - "id": "342-release-notes/0.19#upgrading", + "id": "343-release-notes/0.19#upgrading", "title": "Zero 0.19", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4073,7 +4073,7 @@ "kind": "section" }, { - "id": "343-release-notes/0.19#features", + "id": "344-release-notes/0.19#features", "title": "Zero 0.19", "searchTitle": "Features", "sectionTitle": "Features", @@ -4083,7 +4083,7 @@ "kind": "section" }, { - "id": "344-release-notes/0.19#fixes", + "id": "345-release-notes/0.19#fixes", "title": "Zero 0.19", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4093,7 +4093,7 @@ "kind": "section" }, { - "id": "345-release-notes/0.19#breaking-changes", + "id": "346-release-notes/0.19#breaking-changes", "title": "Zero 0.19", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4137,7 +4137,7 @@ "kind": "page" }, { - "id": "346-release-notes/0.2#breaking-changes", + "id": "347-release-notes/0.2#breaking-changes", "title": "Zero 0.2", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -4147,7 +4147,7 @@ "kind": "section" }, { - "id": "347-release-notes/0.2#features", + "id": "348-release-notes/0.2#features", "title": "Zero 0.2", "searchTitle": "Features", "sectionTitle": "Features", @@ -4157,7 +4157,7 @@ "kind": "section" }, { - "id": "348-release-notes/0.2#fixes", + "id": "349-release-notes/0.2#fixes", "title": "Zero 0.2", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4167,7 +4167,7 @@ "kind": "section" }, { - "id": "349-release-notes/0.2#docs", + "id": "350-release-notes/0.2#docs", "title": "Zero 0.2", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -4177,7 +4177,7 @@ "kind": "section" }, { - "id": "350-release-notes/0.2#source-tree-fixes", + "id": "351-release-notes/0.2#source-tree-fixes", "title": "Zero 0.2", "searchTitle": "Source tree fixes", "sectionTitle": "Source tree fixes", @@ -4187,7 +4187,7 @@ "kind": "section" }, { - "id": "351-release-notes/0.2#zbugs", + "id": "352-release-notes/0.2#zbugs", "title": "Zero 0.2", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -4227,7 +4227,7 @@ "kind": "page" }, { - "id": "352-release-notes/0.20#install", + "id": "353-release-notes/0.20#install", "title": "Zero 0.20", "searchTitle": "Install", "sectionTitle": "Install", @@ -4237,7 +4237,7 @@ "kind": "section" }, { - "id": "353-release-notes/0.20#upgrading", + "id": "354-release-notes/0.20#upgrading", "title": "Zero 0.20", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4247,7 +4247,7 @@ "kind": "section" }, { - "id": "354-release-notes/0.20#features", + "id": "355-release-notes/0.20#features", "title": "Zero 0.20", "searchTitle": "Features", "sectionTitle": "Features", @@ -4257,7 +4257,7 @@ "kind": "section" }, { - "id": "355-release-notes/0.20#fixes", + "id": "356-release-notes/0.20#fixes", "title": "Zero 0.20", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4267,7 +4267,7 @@ "kind": "section" }, { - "id": "356-release-notes/0.20#breaking-changes", + "id": "357-release-notes/0.20#breaking-changes", "title": "Zero 0.20", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4307,7 +4307,7 @@ "kind": "page" }, { - "id": "357-release-notes/0.21#install", + "id": "358-release-notes/0.21#install", "title": "Zero 0.21", "searchTitle": "Install", "sectionTitle": "Install", @@ -4317,7 +4317,7 @@ "kind": "section" }, { - "id": "358-release-notes/0.21#upgrading", + "id": "359-release-notes/0.21#upgrading", "title": "Zero 0.21", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4327,7 +4327,7 @@ "kind": "section" }, { - "id": "359-release-notes/0.21#features", + "id": "360-release-notes/0.21#features", "title": "Zero 0.21", "searchTitle": "Features", "sectionTitle": "Features", @@ -4337,7 +4337,7 @@ "kind": "section" }, { - "id": "360-release-notes/0.21#fixes", + "id": "361-release-notes/0.21#fixes", "title": "Zero 0.21", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4347,7 +4347,7 @@ "kind": "section" }, { - "id": "361-release-notes/0.21#breaking-changes", + "id": "362-release-notes/0.21#breaking-changes", "title": "Zero 0.21", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4399,7 +4399,7 @@ "kind": "page" }, { - "id": "362-release-notes/0.22#install", + "id": "363-release-notes/0.22#install", "title": "Zero 0.22", "searchTitle": "Install", "sectionTitle": "Install", @@ -4409,7 +4409,7 @@ "kind": "section" }, { - "id": "363-release-notes/0.22#upgrading", + "id": "364-release-notes/0.22#upgrading", "title": "Zero 0.22", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4419,7 +4419,7 @@ "kind": "section" }, { - "id": "364-release-notes/0.22#how-ttls-used-to-work", + "id": "365-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", @@ -4429,7 +4429,7 @@ "kind": "section" }, { - "id": "365-release-notes/0.22#how-ttls-work-now", + "id": "366-release-notes/0.22#how-ttls-work-now", "title": "Zero 0.22", "searchTitle": "How TTLs Work Now", "sectionTitle": "How TTLs Work Now", @@ -4439,7 +4439,7 @@ "kind": "section" }, { - "id": "366-release-notes/0.22#using-new-ttls", + "id": "367-release-notes/0.22#using-new-ttls", "title": "Zero 0.22", "searchTitle": "Using New TTLs", "sectionTitle": "Using New TTLs", @@ -4449,7 +4449,7 @@ "kind": "section" }, { - "id": "367-release-notes/0.22#features", + "id": "368-release-notes/0.22#features", "title": "Zero 0.22", "searchTitle": "Features", "sectionTitle": "Features", @@ -4459,7 +4459,7 @@ "kind": "section" }, { - "id": "368-release-notes/0.22#fixes", + "id": "369-release-notes/0.22#fixes", "title": "Zero 0.22", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4469,7 +4469,7 @@ "kind": "section" }, { - "id": "369-release-notes/0.22#breaking-changes", + "id": "370-release-notes/0.22#breaking-changes", "title": "Zero 0.22", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4513,7 +4513,7 @@ "kind": "page" }, { - "id": "370-release-notes/0.23#install", + "id": "371-release-notes/0.23#install", "title": "Zero 0.23", "searchTitle": "Install", "sectionTitle": "Install", @@ -4523,7 +4523,7 @@ "kind": "section" }, { - "id": "371-release-notes/0.23#upgrading", + "id": "372-release-notes/0.23#upgrading", "title": "Zero 0.23", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4533,7 +4533,7 @@ "kind": "section" }, { - "id": "372-release-notes/0.23#features", + "id": "373-release-notes/0.23#features", "title": "Zero 0.23", "searchTitle": "Features", "sectionTitle": "Features", @@ -4543,7 +4543,7 @@ "kind": "section" }, { - "id": "373-release-notes/0.23#fixes", + "id": "374-release-notes/0.23#fixes", "title": "Zero 0.23", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4553,7 +4553,7 @@ "kind": "section" }, { - "id": "374-release-notes/0.23#zbugs", + "id": "375-release-notes/0.23#zbugs", "title": "Zero 0.23", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -4563,7 +4563,7 @@ "kind": "section" }, { - "id": "375-release-notes/0.23#breaking-changes", + "id": "376-release-notes/0.23#breaking-changes", "title": "Zero 0.23", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4603,7 +4603,7 @@ "kind": "page" }, { - "id": "376-release-notes/0.24#installation", + "id": "377-release-notes/0.24#installation", "title": "Zero 0.24", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -4613,7 +4613,7 @@ "kind": "section" }, { - "id": "377-release-notes/0.24#features", + "id": "378-release-notes/0.24#features", "title": "Zero 0.24", "searchTitle": "Features", "sectionTitle": "Features", @@ -4623,7 +4623,7 @@ "kind": "section" }, { - "id": "378-release-notes/0.24#fixes", + "id": "379-release-notes/0.24#fixes", "title": "Zero 0.24", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4633,7 +4633,7 @@ "kind": "section" }, { - "id": "379-release-notes/0.24#breaking-changes", + "id": "380-release-notes/0.24#breaking-changes", "title": "Zero 0.24", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4643,7 +4643,7 @@ "kind": "section" }, { - "id": "380-release-notes/0.24#example-upgrades", + "id": "381-release-notes/0.24#example-upgrades", "title": "Zero 0.24", "searchTitle": "Example Upgrades", "sectionTitle": "Example Upgrades", @@ -4691,7 +4691,7 @@ "kind": "page" }, { - "id": "381-release-notes/0.25#installation", + "id": "382-release-notes/0.25#installation", "title": "Zero 0.25", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -4701,7 +4701,7 @@ "kind": "section" }, { - "id": "382-release-notes/0.25#overview", + "id": "383-release-notes/0.25#overview", "title": "Zero 0.25", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -4711,7 +4711,7 @@ "kind": "section" }, { - "id": "383-release-notes/0.25#upgrading", + "id": "384-release-notes/0.25#upgrading", "title": "Zero 0.25", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -4721,7 +4721,7 @@ "kind": "section" }, { - "id": "384-release-notes/0.25#features", + "id": "385-release-notes/0.25#features", "title": "Zero 0.25", "searchTitle": "Features", "sectionTitle": "Features", @@ -4731,7 +4731,7 @@ "kind": "section" }, { - "id": "385-release-notes/0.25#performance", + "id": "386-release-notes/0.25#performance", "title": "Zero 0.25", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -4741,7 +4741,7 @@ "kind": "section" }, { - "id": "386-release-notes/0.25#fixes", + "id": "387-release-notes/0.25#fixes", "title": "Zero 0.25", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4751,7 +4751,7 @@ "kind": "section" }, { - "id": "387-release-notes/0.25#breaking-changes", + "id": "388-release-notes/0.25#breaking-changes", "title": "Zero 0.25", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4787,7 +4787,7 @@ "kind": "page" }, { - "id": "388-release-notes/0.26#installation", + "id": "389-release-notes/0.26#installation", "title": "Zero 0.26", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -4797,7 +4797,7 @@ "kind": "section" }, { - "id": "389-release-notes/0.26#features", + "id": "390-release-notes/0.26#features", "title": "Zero 0.26", "searchTitle": "Features", "sectionTitle": "Features", @@ -4807,7 +4807,7 @@ "kind": "section" }, { - "id": "390-release-notes/0.26#fixes", + "id": "391-release-notes/0.26#fixes", "title": "Zero 0.26", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4817,7 +4817,7 @@ "kind": "section" }, { - "id": "391-release-notes/0.26#breaking-changes", + "id": "392-release-notes/0.26#breaking-changes", "title": "Zero 0.26", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -4861,7 +4861,7 @@ "kind": "page" }, { - "id": "392-release-notes/0.3#install", + "id": "393-release-notes/0.3#install", "title": "Zero 0.3", "searchTitle": "Install", "sectionTitle": "Install", @@ -4871,7 +4871,7 @@ "kind": "section" }, { - "id": "393-release-notes/0.3#breaking-changes", + "id": "394-release-notes/0.3#breaking-changes", "title": "Zero 0.3", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -4881,7 +4881,7 @@ "kind": "section" }, { - "id": "394-release-notes/0.3#features", + "id": "395-release-notes/0.3#features", "title": "Zero 0.3", "searchTitle": "Features", "sectionTitle": "Features", @@ -4891,7 +4891,7 @@ "kind": "section" }, { - "id": "395-release-notes/0.3#fixes", + "id": "396-release-notes/0.3#fixes", "title": "Zero 0.3", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4901,7 +4901,7 @@ "kind": "section" }, { - "id": "396-release-notes/0.3#docs", + "id": "397-release-notes/0.3#docs", "title": "Zero 0.3", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -4911,7 +4911,7 @@ "kind": "section" }, { - "id": "397-release-notes/0.3#zbugs", + "id": "398-release-notes/0.3#zbugs", "title": "Zero 0.3", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -4955,7 +4955,7 @@ "kind": "page" }, { - "id": "398-release-notes/0.4#install", + "id": "399-release-notes/0.4#install", "title": "Zero 0.4", "searchTitle": "Install", "sectionTitle": "Install", @@ -4965,7 +4965,7 @@ "kind": "section" }, { - "id": "399-release-notes/0.4#breaking-changes", + "id": "400-release-notes/0.4#breaking-changes", "title": "Zero 0.4", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -4975,7 +4975,7 @@ "kind": "section" }, { - "id": "400-release-notes/0.4#added-or--and--and-not-to-zql-documentation", + "id": "401-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).", @@ -4985,7 +4985,7 @@ "kind": "section" }, { - "id": "401-release-notes/0.4#fixes", + "id": "402-release-notes/0.4#fixes", "title": "Zero 0.4", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -4995,7 +4995,7 @@ "kind": "section" }, { - "id": "402-release-notes/0.4#docs", + "id": "403-release-notes/0.4#docs", "title": "Zero 0.4", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -5005,7 +5005,7 @@ "kind": "section" }, { - "id": "403-release-notes/0.4#zbugs", + "id": "404-release-notes/0.4#zbugs", "title": "Zero 0.4", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -5049,7 +5049,7 @@ "kind": "page" }, { - "id": "404-release-notes/0.5#install", + "id": "405-release-notes/0.5#install", "title": "Zero 0.5", "searchTitle": "Install", "sectionTitle": "Install", @@ -5059,7 +5059,7 @@ "kind": "section" }, { - "id": "405-release-notes/0.5#breaking-changes", + "id": "406-release-notes/0.5#breaking-changes", "title": "Zero 0.5", "searchTitle": "Breaking changes", "sectionTitle": "Breaking changes", @@ -5069,7 +5069,7 @@ "kind": "section" }, { - "id": "406-release-notes/0.5#features", + "id": "407-release-notes/0.5#features", "title": "Zero 0.5", "searchTitle": "Features", "sectionTitle": "Features", @@ -5079,7 +5079,7 @@ "kind": "section" }, { - "id": "407-release-notes/0.5#fixes", + "id": "408-release-notes/0.5#fixes", "title": "Zero 0.5", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5089,7 +5089,7 @@ "kind": "section" }, { - "id": "408-release-notes/0.5#docs", + "id": "409-release-notes/0.5#docs", "title": "Zero 0.5", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -5099,7 +5099,7 @@ "kind": "section" }, { - "id": "409-release-notes/0.5#zbugs", + "id": "410-release-notes/0.5#zbugs", "title": "Zero 0.5", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -5143,7 +5143,7 @@ "kind": "page" }, { - "id": "410-release-notes/0.6#install", + "id": "411-release-notes/0.6#install", "title": "Zero 0.6", "searchTitle": "Install", "sectionTitle": "Install", @@ -5153,7 +5153,7 @@ "kind": "section" }, { - "id": "411-release-notes/0.6#upgrade-guide", + "id": "412-release-notes/0.6#upgrade-guide", "title": "Zero 0.6", "searchTitle": "Upgrade Guide", "sectionTitle": "Upgrade Guide", @@ -5163,7 +5163,7 @@ "kind": "section" }, { - "id": "412-release-notes/0.6#breaking-changes", + "id": "413-release-notes/0.6#breaking-changes", "title": "Zero 0.6", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5173,7 +5173,7 @@ "kind": "section" }, { - "id": "413-release-notes/0.6#features", + "id": "414-release-notes/0.6#features", "title": "Zero 0.6", "searchTitle": "Features", "sectionTitle": "Features", @@ -5183,7 +5183,7 @@ "kind": "section" }, { - "id": "414-release-notes/0.6#zbugs", + "id": "415-release-notes/0.6#zbugs", "title": "Zero 0.6", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -5193,7 +5193,7 @@ "kind": "section" }, { - "id": "415-release-notes/0.6#docs", + "id": "416-release-notes/0.6#docs", "title": "Zero 0.6", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -5233,7 +5233,7 @@ "kind": "page" }, { - "id": "416-release-notes/0.7#install", + "id": "417-release-notes/0.7#install", "title": "Zero 0.7", "searchTitle": "Install", "sectionTitle": "Install", @@ -5243,7 +5243,7 @@ "kind": "section" }, { - "id": "417-release-notes/0.7#features", + "id": "418-release-notes/0.7#features", "title": "Zero 0.7", "searchTitle": "Features", "sectionTitle": "Features", @@ -5253,7 +5253,7 @@ "kind": "section" }, { - "id": "418-release-notes/0.7#breaking-changes", + "id": "419-release-notes/0.7#breaking-changes", "title": "Zero 0.7", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5263,7 +5263,7 @@ "kind": "section" }, { - "id": "419-release-notes/0.7#zbugs", + "id": "420-release-notes/0.7#zbugs", "title": "Zero 0.7", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -5273,7 +5273,7 @@ "kind": "section" }, { - "id": "420-release-notes/0.7#docs", + "id": "421-release-notes/0.7#docs", "title": "Zero 0.7", "searchTitle": "Docs", "sectionTitle": "Docs", @@ -5309,7 +5309,7 @@ "kind": "page" }, { - "id": "421-release-notes/0.8#install", + "id": "422-release-notes/0.8#install", "title": "Zero 0.8", "searchTitle": "Install", "sectionTitle": "Install", @@ -5319,7 +5319,7 @@ "kind": "section" }, { - "id": "422-release-notes/0.8#features", + "id": "423-release-notes/0.8#features", "title": "Zero 0.8", "searchTitle": "Features", "sectionTitle": "Features", @@ -5329,7 +5329,7 @@ "kind": "section" }, { - "id": "423-release-notes/0.8#fixes", + "id": "424-release-notes/0.8#fixes", "title": "Zero 0.8", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5339,7 +5339,7 @@ "kind": "section" }, { - "id": "424-release-notes/0.8#breaking-changes", + "id": "425-release-notes/0.8#breaking-changes", "title": "Zero 0.8", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5375,7 +5375,7 @@ "kind": "page" }, { - "id": "425-release-notes/0.9#install", + "id": "426-release-notes/0.9#install", "title": "Zero 0.9", "searchTitle": "Install", "sectionTitle": "Install", @@ -5385,7 +5385,7 @@ "kind": "section" }, { - "id": "426-release-notes/0.9#features", + "id": "427-release-notes/0.9#features", "title": "Zero 0.9", "searchTitle": "Features", "sectionTitle": "Features", @@ -5395,7 +5395,7 @@ "kind": "section" }, { - "id": "427-release-notes/0.9#fixes", + "id": "428-release-notes/0.9#fixes", "title": "Zero 0.9", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5405,7 +5405,7 @@ "kind": "section" }, { - "id": "428-release-notes/0.9#breaking-changes", + "id": "429-release-notes/0.9#breaking-changes", "title": "Zero 0.9", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5445,7 +5445,7 @@ "kind": "page" }, { - "id": "429-release-notes/1.0#installation", + "id": "430-release-notes/1.0#installation", "title": "Zero 1.0", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5455,7 +5455,7 @@ "kind": "section" }, { - "id": "430-release-notes/1.0#overview", + "id": "431-release-notes/1.0#overview", "title": "Zero 1.0", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -5465,7 +5465,7 @@ "kind": "section" }, { - "id": "431-release-notes/1.0#features", + "id": "432-release-notes/1.0#features", "title": "Zero 1.0", "searchTitle": "Features", "sectionTitle": "Features", @@ -5475,7 +5475,7 @@ "kind": "section" }, { - "id": "432-release-notes/1.0#fixes", + "id": "433-release-notes/1.0#fixes", "title": "Zero 1.0", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5485,7 +5485,7 @@ "kind": "section" }, { - "id": "433-release-notes/1.0#breaking-changes", + "id": "434-release-notes/1.0#breaking-changes", "title": "Zero 1.0", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5521,7 +5521,7 @@ "kind": "page" }, { - "id": "434-release-notes/1.1#installation", + "id": "435-release-notes/1.1#installation", "title": "Zero 1.1", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5531,7 +5531,7 @@ "kind": "section" }, { - "id": "435-release-notes/1.1#features", + "id": "436-release-notes/1.1#features", "title": "Zero 1.1", "searchTitle": "Features", "sectionTitle": "Features", @@ -5541,7 +5541,7 @@ "kind": "section" }, { - "id": "436-release-notes/1.1#fixes", + "id": "437-release-notes/1.1#fixes", "title": "Zero 1.1", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5551,7 +5551,7 @@ "kind": "section" }, { - "id": "437-release-notes/1.1#breaking-changes", + "id": "438-release-notes/1.1#breaking-changes", "title": "Zero 1.1", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5603,7 +5603,7 @@ "kind": "page" }, { - "id": "438-release-notes/1.10#installation", + "id": "439-release-notes/1.10#installation", "title": "Zero 1.10", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5613,7 +5613,7 @@ "kind": "section" }, { - "id": "439-release-notes/1.10#overview", + "id": "440-release-notes/1.10#overview", "title": "Zero 1.10", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -5623,7 +5623,7 @@ "kind": "section" }, { - "id": "440-release-notes/1.10#features", + "id": "441-release-notes/1.10#features", "title": "Zero 1.10", "searchTitle": "Features", "sectionTitle": "Features", @@ -5633,7 +5633,7 @@ "kind": "section" }, { - "id": "441-release-notes/1.10#fixes", + "id": "442-release-notes/1.10#fixes", "title": "Zero 1.10", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5643,7 +5643,7 @@ "kind": "section" }, { - "id": "442-release-notes/1.10#queries-and-connections", + "id": "443-release-notes/1.10#queries-and-connections", "title": "Zero 1.10", "searchTitle": "Queries and Connections", "sectionTitle": "Queries and Connections", @@ -5653,7 +5653,7 @@ "kind": "section" }, { - "id": "443-release-notes/1.10#replication-and-backfills", + "id": "444-release-notes/1.10#replication-and-backfills", "title": "Zero 1.10", "searchTitle": "Replication and Backfills", "sectionTitle": "Replication and Backfills", @@ -5663,7 +5663,7 @@ "kind": "section" }, { - "id": "444-release-notes/1.10#backups-and-startup", + "id": "445-release-notes/1.10#backups-and-startup", "title": "Zero 1.10", "searchTitle": "Backups and Startup", "sectionTitle": "Backups and Startup", @@ -5673,7 +5673,7 @@ "kind": "section" }, { - "id": "445-release-notes/1.10#breaking-changes", + "id": "446-release-notes/1.10#breaking-changes", "title": "Zero 1.10", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -5785,7 +5785,7 @@ "kind": "page" }, { - "id": "446-release-notes/1.11#installation", + "id": "447-release-notes/1.11#installation", "title": "Zero 1.11", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -5795,7 +5795,7 @@ "kind": "section" }, { - "id": "447-release-notes/1.11#1115", + "id": "448-release-notes/1.11#1115", "title": "Zero 1.11", "searchTitle": "1.11.5", "sectionTitle": "1.11.5", @@ -5805,7 +5805,7 @@ "kind": "section" }, { - "id": "448-release-notes/1.11#security", + "id": "449-release-notes/1.11#security", "title": "Zero 1.11", "searchTitle": "Security", "sectionTitle": "Security", @@ -5815,7 +5815,7 @@ "kind": "section" }, { - "id": "449-release-notes/1.11#upgrading-to-1115", + "id": "450-release-notes/1.11#upgrading-to-1115", "title": "Zero 1.11", "searchTitle": "Upgrading to 1.11.5", "sectionTitle": "Upgrading to 1.11.5", @@ -5825,7 +5825,7 @@ "kind": "section" }, { - "id": "450-release-notes/1.11#server-validated-user-ids", + "id": "451-release-notes/1.11#server-validated-user-ids", "title": "Zero 1.11", "searchTitle": "Server-Validated User IDs", "sectionTitle": "Server-Validated User IDs", @@ -5835,7 +5835,7 @@ "kind": "section" }, { - "id": "451-release-notes/1.11#multi-node-deployments", + "id": "452-release-notes/1.11#multi-node-deployments", "title": "Zero 1.11", "searchTitle": "Multi-Node Deployments", "sectionTitle": "Multi-Node Deployments", @@ -5845,7 +5845,7 @@ "kind": "section" }, { - "id": "452-release-notes/1.11#breaking-changes-in-1115", + "id": "453-release-notes/1.11#breaking-changes-in-1115", "title": "Zero 1.11", "searchTitle": "Breaking Changes in 1.11.5", "sectionTitle": "Breaking Changes in 1.11.5", @@ -5855,7 +5855,7 @@ "kind": "section" }, { - "id": "453-release-notes/1.11#fixes-in-1115", + "id": "454-release-notes/1.11#fixes-in-1115", "title": "Zero 1.11", "searchTitle": "Fixes in 1.11.5", "sectionTitle": "Fixes in 1.11.5", @@ -5865,7 +5865,7 @@ "kind": "section" }, { - "id": "454-release-notes/1.11#overview", + "id": "455-release-notes/1.11#overview", "title": "Zero 1.11", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -5875,7 +5875,7 @@ "kind": "section" }, { - "id": "455-release-notes/1.11#features", + "id": "456-release-notes/1.11#features", "title": "Zero 1.11", "searchTitle": "Features", "sectionTitle": "Features", @@ -5885,7 +5885,7 @@ "kind": "section" }, { - "id": "456-release-notes/1.11#performance", + "id": "457-release-notes/1.11#performance", "title": "Zero 1.11", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -5895,7 +5895,7 @@ "kind": "section" }, { - "id": "457-release-notes/1.11#client-cold-start", + "id": "458-release-notes/1.11#client-cold-start", "title": "Zero 1.11", "searchTitle": "Client Cold Start", "sectionTitle": "Client Cold Start", @@ -5905,7 +5905,7 @@ "kind": "section" }, { - "id": "458-release-notes/1.11#server-side-incremental-updates", + "id": "459-release-notes/1.11#server-side-incremental-updates", "title": "Zero 1.11", "searchTitle": "Server-Side Incremental Updates", "sectionTitle": "Server-Side Incremental Updates", @@ -5915,7 +5915,7 @@ "kind": "section" }, { - "id": "459-release-notes/1.11#react-native", + "id": "460-release-notes/1.11#react-native", "title": "Zero 1.11", "searchTitle": "React Native", "sectionTitle": "React Native", @@ -5925,7 +5925,7 @@ "kind": "section" }, { - "id": "460-release-notes/1.11#building-and-hashing-queries", + "id": "461-release-notes/1.11#building-and-hashing-queries", "title": "Zero 1.11", "searchTitle": "Building and Hashing Queries", "sectionTitle": "Building and Hashing Queries", @@ -5935,7 +5935,7 @@ "kind": "section" }, { - "id": "461-release-notes/1.11#client-writes-with-many-joined-queries-got-slower", + "id": "462-release-notes/1.11#client-writes-with-many-joined-queries-got-slower", "title": "Zero 1.11", "searchTitle": "Client Writes With Many Joined Queries Got Slower", "sectionTitle": "Client Writes With Many Joined Queries Got Slower", @@ -5945,7 +5945,7 @@ "kind": "section" }, { - "id": "462-release-notes/1.11#fixes", + "id": "463-release-notes/1.11#fixes", "title": "Zero 1.11", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -5955,7 +5955,7 @@ "kind": "section" }, { - "id": "463-release-notes/1.11#query-results", + "id": "464-release-notes/1.11#query-results", "title": "Zero 1.11", "searchTitle": "Query Results", "sectionTitle": "Query Results", @@ -5965,7 +5965,7 @@ "kind": "section" }, { - "id": "464-release-notes/1.11#clients-and-react-native", + "id": "465-release-notes/1.11#clients-and-react-native", "title": "Zero 1.11", "searchTitle": "Clients and React Native", "sectionTitle": "Clients and React Native", @@ -5975,7 +5975,7 @@ "kind": "section" }, { - "id": "465-release-notes/1.11#zero-cache-reliability", + "id": "466-release-notes/1.11#zero-cache-reliability", "title": "Zero 1.11", "searchTitle": "zero-cache Reliability", "sectionTitle": "zero-cache Reliability", @@ -5985,7 +5985,7 @@ "kind": "section" }, { - "id": "466-release-notes/1.11#replication-backfills-and-backups", + "id": "467-release-notes/1.11#replication-backfills-and-backups", "title": "Zero 1.11", "searchTitle": "Replication, Backfills, and Backups", "sectionTitle": "Replication, Backfills, and Backups", @@ -5995,7 +5995,7 @@ "kind": "section" }, { - "id": "467-release-notes/1.11#type-checking", + "id": "468-release-notes/1.11#type-checking", "title": "Zero 1.11", "searchTitle": "Type Checking", "sectionTitle": "Type Checking", @@ -6005,7 +6005,7 @@ "kind": "section" }, { - "id": "468-release-notes/1.11#breaking-changes", + "id": "469-release-notes/1.11#breaking-changes", "title": "Zero 1.11", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6015,7 +6015,269 @@ "kind": "section" }, { - "id": "56-release-notes/1.2", + "id": "56-release-notes/1.12", + "title": "Zero 1.12", + "searchTitle": "Zero 1.12", + "url": "/docs/release-notes/1.12", + "content": "Installation npm install @rocicorp/zero@1.12 Deploy zero-cache before updating clients. Zero 1.12 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Then set ZERO_CHANGE_STREAMER_SECRET on the view-syncers, and after that on the replication-manager. Overview Zero 1.12 syncs aggregates like count() and sum(), groups rows with groupBy() and distinct(), and can filter on values inside JSON columns. zero-cache hydrates queries and applies updates up to 1.8x faster in our benchmarks, React Native apps start about 3x faster, and each replication-manager now gets its own replication slot. This release also fixes eleven security vulnerabilities, along with a lot of other fixes, especially in replication and connection handling. Security Zero 1.12 includes the fixes for the eleven security vulnerabilities fixed in 1.11.5. See the 1.11.5 release notes for the advisories. We recommend that everyone upgrade. The fix for client-claimed user IDs is off until you turn it on. See Server-Validated User IDs. Some of these fixes change how Zero behaves, so read Breaking Changes before you upgrade, especially the entries on userID and the change-streamer secret. Upgrading Server-Validated User IDs zero-cache can now refuse a userID that your API didn't confirm. To use it: Return the validated userID from your /query and /mutate endpoints, or null for logged-out callers. handleQueryRequest and handleMutateRequest do this when you pass them userID, as recommended since 1.5. Set ZERO_AUTH_REQUIRE_SERVER_USER_ID=true. Until you do, zero-cache trusts the userID the client claims and logs a warning. Apps that verify tokens with ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL don't need this: the token already verifies the userID. (83efa9462, c1f36d79e) RMv2: Replication-Manager Slots We're reworking how replication-managers replicate from Postgres, so that several can run side by side and one can take over from another without a pause. We call this RMv2. Zero 1.12 ships the first step and turns it on by default: each replication-manager now gets its own replication slot. A new replication-manager restores its replica from the backup, catches up on its own slot while the old one keeps serving, and then takes over. Slots of replication-managers that are gone are dropped automatically. (49607b8b3) A new replication-manager also starts about 20 seconds faster, once both the old and the new one are on 1.12. The new zero.replication.server_start metric shows where the start time goes. (522d2ce57, 9aca1b0ab) This needs Litestream v5 backups, the default since 1.11. Expect one slot per running replication-manager in Postgres, each holding WAL until it is consumed. To go back to a shared slot, set ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA=false. That doesn't need a resync. Running several replication-managers at once will come in a later release. Features Queries can return a single aggregate: zql.issue.where('open', true).count(), or zql.issue.max('numVotes'). A relationship can be reduced the same way, so related('comments', q => q.count()) gives each issue its number of comments instead of the comments. Only the result is synced, and it stays up to date as rows change. Counts, sums and averages of direct relationships update optimistically when a mutator changes the related rows. sum(), avg(), min() and max() can also reduce a value inside a JSON column. (c9d1b15fc, e2ecf69ce, 642aece86, 523d3b97e, 8b716b3fe) distinct() returns the distinct combinations of some columns, and groupBy() computes an aggregate per group: zql.issue.groupBy('assigneeID').count(). Groups appear and disappear optimistically for count(), sum() and avg(). (effd2da27, 5493ba549, fe168e640) where() can compare a value inside a json() column: cmp(json('metadata', 'priority'), 'high'). Paths can go through object keys and array indices, comparisons are type-strict, and TypeScript checks the path of a typed JSON column. JSON path filters run on the client, in SQLite and in Postgres. (0a1a0f3d4) Draining behind a load balancer: a new ZERO_PRE_DRAIN_DELAY_MS makes zero-cache fail its readiness check for a while after SIGTERM while still serving, so the load balancer stops routing to it before the drain starts. With sticky sessions, set ZERO_AFFINITY_COOKIE_NAME and zero-cache expires the cookie on connections it turns away while draining. A new zero.sync.drain-time metric estimates how long a view-syncer's drain will take. (f22ec09b6, f77997349, 9a74d2d3c, 04a27634b) Clients retry a failed connection sooner: after 250–500 ms the first time, backing off to the usual 5 seconds by the fifth failure, with jitter so clients that lost the same server don't retry together. A draining zero-cache also tells new connections to retry within a second instead of after 5 seconds. (06622e6ec, a5a940321) Replication status events are republished when the replica grows by 1 GiB or 5%, so consumers that size storage from replicaSize see growth from ordinary writes. (76a0a1bd1) Multi-node deployments can authenticate view-syncers to the replication-manager with a shared ZERO_CHANGE_STREAMER_SECRET. A single-node zero-cache generates its own. (b2b8f68e4) ZERO_ENABLE_HEAPZ=false turns off the /heapz heap-snapshot endpoint while keeping the other diagnostic endpoints and the inspector. (ea3105333) Performance Server-Side Hydration When a client connects or adds a query, zero-cache has to hydrate it: run it against the replica and send the results. In Zero 1.12 this takes about 1.2x less CPU, and about 1.8x less for queries with whereExists, which are common because permissions use them. We measured 100 client groups on one sync worker, each hydrating its user's top issues with their latest comments. (6563d5751, 70c771950, c2882cba0, 5a5fb1af1, c9c34d145, f7233246c, ae794cc0e) Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.12 does this with about 1.4x to 1.7x less CPU than 1.11, which was already 2.5x better than 1.10. We measured the same workload as last release: 100 client groups on one sync worker, each syncing a query with a related subquery. Pushing one write through 300 open queries is about 1.6x faster too. (6563d5751, a35b628ea, 018a90f86, f5d11d924, 8702aea45, fd30e6437) React Native Cold Start When a React Native app starts, Zero loads the rows it has stored on the device into memory and runs the app's queries over them before anything renders. In Zero 1.12 this is about 3x faster. Hermes, the JavaScript engine React Native uses, was spending most of that time sorting rows that were already in order. We measured an app with 180,000 stored rows and 21 queries, on the Android emulator and the iOS simulator. (bfc7e8c24, e7f8a1d91, 8c21e7960, ea4560f4e, e058bfca2, 6375eeb84, 855c495b1) Query Planning Before zero-cache hydrates a query with whereExists, it plans which way to run each join. With three to five whereExists, or nested ones, planning now takes about 2x to 10x less time. (9215b24b2) Fixes Queries and Mutations Server mutators and queries read time with time zone columns as negative milliseconds when the zone offset was positive, and a ServerSchema written by hand with the short timestamptz name read a timestamp as a time of day. (9a08f1e40) Server mutators and queries failed with type \"...\" does not exist for enum and other user-defined Postgres types outside the public schema. Casts are now schema-qualified. (8166580b0) (thanks @jschaf!) A push that failed at the API server could put other tabs of the same client group into the Error state, or fail a socket the client had already replaced. A failed push now affects only the socket that sent it, and that socket's queued pushes are re-sent on the next connection. (945ce6192) (thanks @pawarren!) runAnalyzeCLI --query no longer evaluates the query text as JavaScript. It accepts query builder calls, literals and arrow functions, and rejects anything else with its position. (02b6c1bbd) Clients and React Native On React Native, client garbage collection failed every 5 minutes, and queries whose related() entries weren't in alias order threw during normalization, because Hermes lacks Array.prototype.toSorted. (b62ded385) (thanks @wikil!) A SQLite-backed store whose close() threw blocked every later Zero instance with the same name from opening, and two overlapping close() calls could close a shared SQLite connection under another store. (dee69dc9d, cc861e8f6) (thanks @wikil!) An app that refreshed its auth token while the first connection of a new client group was still being validated could be refused for good with The initConnection message for a new client group must include client schema. (874e1ec9d) (thanks @YevheniiKotyrlo!) After zero-cache was rolled back, a client group holding a query the older server could not parse was stuck in an error state until the server rolled forward. The group is now reset and the client starts a fresh one. (dbf677638) A write that put keys from both the upper Basic Multilingual Plane (U+E000 to U+FFFF) and above it (such as emoji) into the same B-tree node failed with putMany entries must be sorted and unique, and a diff could report such a key as removed and re-added instead of changed. (0e15506e8) (thanks @natew!) Client garbage collection left a store open for every database it inspected. With SQLite, closing Zero then no longer closed its database connection, and a custom StoreProvider whose drop refuses an open store failed every collection. (bd436ef0d) (thanks @wikil!) Client, client group and connection IDs now come from crypto.getRandomValues when available instead of Math.random. (e80d6305c, c2ebe3df5) zero-cache Reliability A view-syncer that reloaded a client group's state after an error kept stale row records and query pipelines, which could crash it with Query … is not in the CVR or Unexpected undefined value, or leave rows that never reached the client. (1137280b6, bb348961f, 9e0e85b62) With a statement_timeout on the CVR database, connecting a client group with many rows could fail with canceling statement due to statement timeout. (892fa553b) View-syncers on slow or throttled disks spent up to a minute inside a single commit running SQLite checkpoints, and were dropped by the replication-manager as slow subscribers. Serving replicas are now checkpointed by a separate process. (fbdb27ed5) Clients were disabled and reloaded with SchemaVersionNotSupported while an upstream migration backfilled a table or column. They now retry with backoff until the backfill is done. (8280af82f) Malformed client messages, such as a second initConnection on a socket, an invalid baseCookie or an invalid query registration, were logged as internal errors. They are now client errors logged at warn. (e1c39de03, f452e99b5, 512baff4b) A client group stays with the user it was first opened for across view-syncer restarts; a connection as a different user is refused the same way it already was while the group was live. (8d9466b13) Inspector authentication is per connection, and the 1.12 client remembers a password the server accepted, so it doesn't prompt again after reconnecting. Repeated wrong admin or operator passwords are now rate limited, on the inspector and on the HTTP diagnostic endpoints. (e186c0d59, c767ac31c, 65375a770) zero-cache logs and traces show a hash of the clientGroupID instead of the ID. (adfce3301, de9121d99) Replication A numeric, float4 or float8 value between about 2^63 and 1e21 stopped replication with a RangeError, a numeric between 2^53 and 2^63 was stored as the wrong integer, and such a value in a row key or query literal failed the client group after a reload. These values now replicate as doubles. (340215ab5) A NULL element in a timestamp[], json[] or int8[] column stopped replication, and in text, enum, boolean and numeric arrays it was stored as \"NULL\", false or null. Array columns are now parsed correctly, including arrays with explicit bounds. Rows already stored keep their old values until they change. (fe4bc732a) A schema-change or lag-report message on the replication stream that zero-cache could not parse stopped replication. Such messages, and schema-change messages that its own upstream functions didn't emit, are now skipped. (08c634549, f6010bb34) With Litestream v5 backups on Google Cloud Storage or Azure, backup files were never compacted, so old ones were never deleted and the backup kept growing. (e2f030e63) Breaking Changes analyze-query bin removed: npx analyze-query is gone. Use a script that calls runAnalyzeCLI. @rocicorp/zero now depends on oxc-parser, which installs a native binding for your platform. (a3932f51e, 02b6c1bbd) userID needs a token: when ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL is set, a connection that passes a userID without an auth token is refused with Unauthorized. Omit userID for logged-out users; the deprecated userID: 'anon' without auth is refused too. (522688120) Changed defaults: ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA is on. It needs Litestream v5 backups, the default since 1.11, and each replication-manager holds its own slot while it runs. Turning it off doesn't resync. (49607b8b3) Multi-node secret: set ZERO_CHANGE_STREAMER_SECRET on the view-syncers first, then on the replication-manager. Until you do, the replication-manager still accepts view-syncers without it, but warns at startup. If one of your nodes runs sync workers and is also the replication-manager for other view-syncers, it now makes up its own secret, so set the secret on every node. (b2b8f68e4) Legacy CRUD mutations are keyed by the primary key of the replica's table, not by the key the client sends. This only matters if your client schema declares a different primary key than the table has in Postgres. (f24fddae7) Rollback: clients keep their local database and unsent mutations when moving between 1.11 and 1.12 in either direction. One exception: a client group that registered an aggregate, groupBy, distinct or JSON path query is reset when zero-cache is rolled back to 1.11, and its unsent mutations are lost. (8b716b3fe, dbf677638)", + "headings": [ + { + "text": "Installation", + "id": "installation" + }, + { + "text": "Overview", + "id": "overview" + }, + { + "text": "Security", + "id": "security" + }, + { + "text": "Upgrading", + "id": "upgrading" + }, + { + "text": "Server-Validated User IDs", + "id": "server-validated-user-ids" + }, + { + "text": "RMv2: Replication-Manager Slots", + "id": "rmv2-replication-manager-slots" + }, + { + "text": "Features", + "id": "features" + }, + { + "text": "Performance", + "id": "performance" + }, + { + "text": "Server-Side Hydration", + "id": "server-side-hydration" + }, + { + "text": "Server-Side Incremental Updates", + "id": "server-side-incremental-updates" + }, + { + "text": "React Native Cold Start", + "id": "react-native-cold-start" + }, + { + "text": "Query Planning", + "id": "query-planning" + }, + { + "text": "Fixes", + "id": "fixes" + }, + { + "text": "Queries and Mutations", + "id": "queries-and-mutations" + }, + { + "text": "Clients and React Native", + "id": "clients-and-react-native" + }, + { + "text": "zero-cache Reliability", + "id": "zero-cache-reliability" + }, + { + "text": "Replication", + "id": "replication" + }, + { + "text": "Breaking Changes", + "id": "breaking-changes" + } + ], + "kind": "page" + }, + { + "id": "470-release-notes/1.12#installation", + "title": "Zero 1.12", + "searchTitle": "Installation", + "sectionTitle": "Installation", + "sectionId": "installation", + "url": "/docs/release-notes/1.12", + "content": "npm install @rocicorp/zero@1.12 Deploy zero-cache before updating clients. Zero 1.12 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Then set ZERO_CHANGE_STREAMER_SECRET on the view-syncers, and after that on the replication-manager.", + "kind": "section" + }, + { + "id": "471-release-notes/1.12#overview", + "title": "Zero 1.12", + "searchTitle": "Overview", + "sectionTitle": "Overview", + "sectionId": "overview", + "url": "/docs/release-notes/1.12", + "content": "Zero 1.12 syncs aggregates like count() and sum(), groups rows with groupBy() and distinct(), and can filter on values inside JSON columns. zero-cache hydrates queries and applies updates up to 1.8x faster in our benchmarks, React Native apps start about 3x faster, and each replication-manager now gets its own replication slot. This release also fixes eleven security vulnerabilities, along with a lot of other fixes, especially in replication and connection handling.", + "kind": "section" + }, + { + "id": "472-release-notes/1.12#security", + "title": "Zero 1.12", + "searchTitle": "Security", + "sectionTitle": "Security", + "sectionId": "security", + "url": "/docs/release-notes/1.12", + "content": "Zero 1.12 includes the fixes for the eleven security vulnerabilities fixed in 1.11.5. See the 1.11.5 release notes for the advisories. We recommend that everyone upgrade. The fix for client-claimed user IDs is off until you turn it on. See Server-Validated User IDs. Some of these fixes change how Zero behaves, so read Breaking Changes before you upgrade, especially the entries on userID and the change-streamer secret.", + "kind": "section" + }, + { + "id": "473-release-notes/1.12#upgrading", + "title": "Zero 1.12", + "searchTitle": "Upgrading", + "sectionTitle": "Upgrading", + "sectionId": "upgrading", + "url": "/docs/release-notes/1.12", + "content": "Server-Validated User IDs zero-cache can now refuse a userID that your API didn't confirm. To use it: Return the validated userID from your /query and /mutate endpoints, or null for logged-out callers. handleQueryRequest and handleMutateRequest do this when you pass them userID, as recommended since 1.5. Set ZERO_AUTH_REQUIRE_SERVER_USER_ID=true. Until you do, zero-cache trusts the userID the client claims and logs a warning. Apps that verify tokens with ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL don't need this: the token already verifies the userID. (83efa9462, c1f36d79e)", + "kind": "section" + }, + { + "id": "474-release-notes/1.12#server-validated-user-ids", + "title": "Zero 1.12", + "searchTitle": "Server-Validated User IDs", + "sectionTitle": "Server-Validated User IDs", + "sectionId": "server-validated-user-ids", + "url": "/docs/release-notes/1.12", + "content": "zero-cache can now refuse a userID that your API didn't confirm. To use it: Return the validated userID from your /query and /mutate endpoints, or null for logged-out callers. handleQueryRequest and handleMutateRequest do this when you pass them userID, as recommended since 1.5. Set ZERO_AUTH_REQUIRE_SERVER_USER_ID=true. Until you do, zero-cache trusts the userID the client claims and logs a warning. Apps that verify tokens with ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL don't need this: the token already verifies the userID. (83efa9462, c1f36d79e)", + "kind": "section" + }, + { + "id": "475-release-notes/1.12#rmv2-replication-manager-slots", + "title": "Zero 1.12", + "searchTitle": "RMv2: Replication-Manager Slots", + "sectionTitle": "RMv2: Replication-Manager Slots", + "sectionId": "rmv2-replication-manager-slots", + "url": "/docs/release-notes/1.12", + "content": "We're reworking how replication-managers replicate from Postgres, so that several can run side by side and one can take over from another without a pause. We call this RMv2. Zero 1.12 ships the first step and turns it on by default: each replication-manager now gets its own replication slot. A new replication-manager restores its replica from the backup, catches up on its own slot while the old one keeps serving, and then takes over. Slots of replication-managers that are gone are dropped automatically. (49607b8b3) A new replication-manager also starts about 20 seconds faster, once both the old and the new one are on 1.12. The new zero.replication.server_start metric shows where the start time goes. (522d2ce57, 9aca1b0ab) This needs Litestream v5 backups, the default since 1.11. Expect one slot per running replication-manager in Postgres, each holding WAL until it is consumed. To go back to a shared slot, set ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA=false. That doesn't need a resync. Running several replication-managers at once will come in a later release.", + "kind": "section" + }, + { + "id": "476-release-notes/1.12#features", + "title": "Zero 1.12", + "searchTitle": "Features", + "sectionTitle": "Features", + "sectionId": "features", + "url": "/docs/release-notes/1.12", + "content": "Queries can return a single aggregate: zql.issue.where('open', true).count(), or zql.issue.max('numVotes'). A relationship can be reduced the same way, so related('comments', q => q.count()) gives each issue its number of comments instead of the comments. Only the result is synced, and it stays up to date as rows change. Counts, sums and averages of direct relationships update optimistically when a mutator changes the related rows. sum(), avg(), min() and max() can also reduce a value inside a JSON column. (c9d1b15fc, e2ecf69ce, 642aece86, 523d3b97e, 8b716b3fe) distinct() returns the distinct combinations of some columns, and groupBy() computes an aggregate per group: zql.issue.groupBy('assigneeID').count(). Groups appear and disappear optimistically for count(), sum() and avg(). (effd2da27, 5493ba549, fe168e640) where() can compare a value inside a json() column: cmp(json('metadata', 'priority'), 'high'). Paths can go through object keys and array indices, comparisons are type-strict, and TypeScript checks the path of a typed JSON column. JSON path filters run on the client, in SQLite and in Postgres. (0a1a0f3d4) Draining behind a load balancer: a new ZERO_PRE_DRAIN_DELAY_MS makes zero-cache fail its readiness check for a while after SIGTERM while still serving, so the load balancer stops routing to it before the drain starts. With sticky sessions, set ZERO_AFFINITY_COOKIE_NAME and zero-cache expires the cookie on connections it turns away while draining. A new zero.sync.drain-time metric estimates how long a view-syncer's drain will take. (f22ec09b6, f77997349, 9a74d2d3c, 04a27634b) Clients retry a failed connection sooner: after 250–500 ms the first time, backing off to the usual 5 seconds by the fifth failure, with jitter so clients that lost the same server don't retry together. A draining zero-cache also tells new connections to retry within a second instead of after 5 seconds. (06622e6ec, a5a940321) Replication status events are republished when the replica grows by 1 GiB or 5%, so consumers that size storage from replicaSize see growth from ordinary writes. (76a0a1bd1) Multi-node deployments can authenticate view-syncers to the replication-manager with a shared ZERO_CHANGE_STREAMER_SECRET. A single-node zero-cache generates its own. (b2b8f68e4) ZERO_ENABLE_HEAPZ=false turns off the /heapz heap-snapshot endpoint while keeping the other diagnostic endpoints and the inspector. (ea3105333)", + "kind": "section" + }, + { + "id": "477-release-notes/1.12#performance", + "title": "Zero 1.12", + "searchTitle": "Performance", + "sectionTitle": "Performance", + "sectionId": "performance", + "url": "/docs/release-notes/1.12", + "content": "Server-Side Hydration When a client connects or adds a query, zero-cache has to hydrate it: run it against the replica and send the results. In Zero 1.12 this takes about 1.2x less CPU, and about 1.8x less for queries with whereExists, which are common because permissions use them. We measured 100 client groups on one sync worker, each hydrating its user's top issues with their latest comments. (6563d5751, 70c771950, c2882cba0, 5a5fb1af1, c9c34d145, f7233246c, ae794cc0e) Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.12 does this with about 1.4x to 1.7x less CPU than 1.11, which was already 2.5x better than 1.10. We measured the same workload as last release: 100 client groups on one sync worker, each syncing a query with a related subquery. Pushing one write through 300 open queries is about 1.6x faster too. (6563d5751, a35b628ea, 018a90f86, f5d11d924, 8702aea45, fd30e6437) React Native Cold Start When a React Native app starts, Zero loads the rows it has stored on the device into memory and runs the app's queries over them before anything renders. In Zero 1.12 this is about 3x faster. Hermes, the JavaScript engine React Native uses, was spending most of that time sorting rows that were already in order. We measured an app with 180,000 stored rows and 21 queries, on the Android emulator and the iOS simulator. (bfc7e8c24, e7f8a1d91, 8c21e7960, ea4560f4e, e058bfca2, 6375eeb84, 855c495b1) Query Planning Before zero-cache hydrates a query with whereExists, it plans which way to run each join. With three to five whereExists, or nested ones, planning now takes about 2x to 10x less time. (9215b24b2)", + "kind": "section" + }, + { + "id": "478-release-notes/1.12#server-side-hydration", + "title": "Zero 1.12", + "searchTitle": "Server-Side Hydration", + "sectionTitle": "Server-Side Hydration", + "sectionId": "server-side-hydration", + "url": "/docs/release-notes/1.12", + "content": "When a client connects or adds a query, zero-cache has to hydrate it: run it against the replica and send the results. In Zero 1.12 this takes about 1.2x less CPU, and about 1.8x less for queries with whereExists, which are common because permissions use them. We measured 100 client groups on one sync worker, each hydrating its user's top issues with their latest comments. (6563d5751, 70c771950, c2882cba0, 5a5fb1af1, c9c34d145, f7233246c, ae794cc0e)", + "kind": "section" + }, + { + "id": "479-release-notes/1.12#server-side-incremental-updates", + "title": "Zero 1.12", + "searchTitle": "Server-Side Incremental Updates", + "sectionTitle": "Server-Side Incremental Updates", + "sectionId": "server-side-incremental-updates", + "url": "/docs/release-notes/1.12", + "content": "When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.12 does this with about 1.4x to 1.7x less CPU than 1.11, which was already 2.5x better than 1.10. We measured the same workload as last release: 100 client groups on one sync worker, each syncing a query with a related subquery. Pushing one write through 300 open queries is about 1.6x faster too. (6563d5751, a35b628ea, 018a90f86, f5d11d924, 8702aea45, fd30e6437)", + "kind": "section" + }, + { + "id": "480-release-notes/1.12#react-native-cold-start", + "title": "Zero 1.12", + "searchTitle": "React Native Cold Start", + "sectionTitle": "React Native Cold Start", + "sectionId": "react-native-cold-start", + "url": "/docs/release-notes/1.12", + "content": "When a React Native app starts, Zero loads the rows it has stored on the device into memory and runs the app's queries over them before anything renders. In Zero 1.12 this is about 3x faster. Hermes, the JavaScript engine React Native uses, was spending most of that time sorting rows that were already in order. We measured an app with 180,000 stored rows and 21 queries, on the Android emulator and the iOS simulator. (bfc7e8c24, e7f8a1d91, 8c21e7960, ea4560f4e, e058bfca2, 6375eeb84, 855c495b1)", + "kind": "section" + }, + { + "id": "481-release-notes/1.12#query-planning", + "title": "Zero 1.12", + "searchTitle": "Query Planning", + "sectionTitle": "Query Planning", + "sectionId": "query-planning", + "url": "/docs/release-notes/1.12", + "content": "Before zero-cache hydrates a query with whereExists, it plans which way to run each join. With three to five whereExists, or nested ones, planning now takes about 2x to 10x less time. (9215b24b2)", + "kind": "section" + }, + { + "id": "482-release-notes/1.12#fixes", + "title": "Zero 1.12", + "searchTitle": "Fixes", + "sectionTitle": "Fixes", + "sectionId": "fixes", + "url": "/docs/release-notes/1.12", + "content": "Queries and Mutations Server mutators and queries read time with time zone columns as negative milliseconds when the zone offset was positive, and a ServerSchema written by hand with the short timestamptz name read a timestamp as a time of day. (9a08f1e40) Server mutators and queries failed with type \"...\" does not exist for enum and other user-defined Postgres types outside the public schema. Casts are now schema-qualified. (8166580b0) (thanks @jschaf!) A push that failed at the API server could put other tabs of the same client group into the Error state, or fail a socket the client had already replaced. A failed push now affects only the socket that sent it, and that socket's queued pushes are re-sent on the next connection. (945ce6192) (thanks @pawarren!) runAnalyzeCLI --query no longer evaluates the query text as JavaScript. It accepts query builder calls, literals and arrow functions, and rejects anything else with its position. (02b6c1bbd) Clients and React Native On React Native, client garbage collection failed every 5 minutes, and queries whose related() entries weren't in alias order threw during normalization, because Hermes lacks Array.prototype.toSorted. (b62ded385) (thanks @wikil!) A SQLite-backed store whose close() threw blocked every later Zero instance with the same name from opening, and two overlapping close() calls could close a shared SQLite connection under another store. (dee69dc9d, cc861e8f6) (thanks @wikil!) An app that refreshed its auth token while the first connection of a new client group was still being validated could be refused for good with The initConnection message for a new client group must include client schema. (874e1ec9d) (thanks @YevheniiKotyrlo!) After zero-cache was rolled back, a client group holding a query the older server could not parse was stuck in an error state until the server rolled forward. The group is now reset and the client starts a fresh one. (dbf677638) A write that put keys from both the upper Basic Multilingual Plane (U+E000 to U+FFFF) and above it (such as emoji) into the same B-tree node failed with putMany entries must be sorted and unique, and a diff could report such a key as removed and re-added instead of changed. (0e15506e8) (thanks @natew!) Client garbage collection left a store open for every database it inspected. With SQLite, closing Zero then no longer closed its database connection, and a custom StoreProvider whose drop refuses an open store failed every collection. (bd436ef0d) (thanks @wikil!) Client, client group and connection IDs now come from crypto.getRandomValues when available instead of Math.random. (e80d6305c, c2ebe3df5) zero-cache Reliability A view-syncer that reloaded a client group's state after an error kept stale row records and query pipelines, which could crash it with Query … is not in the CVR or Unexpected undefined value, or leave rows that never reached the client. (1137280b6, bb348961f, 9e0e85b62) With a statement_timeout on the CVR database, connecting a client group with many rows could fail with canceling statement due to statement timeout. (892fa553b) View-syncers on slow or throttled disks spent up to a minute inside a single commit running SQLite checkpoints, and were dropped by the replication-manager as slow subscribers. Serving replicas are now checkpointed by a separate process. (fbdb27ed5) Clients were disabled and reloaded with SchemaVersionNotSupported while an upstream migration backfilled a table or column. They now retry with backoff until the backfill is done. (8280af82f) Malformed client messages, such as a second initConnection on a socket, an invalid baseCookie or an invalid query registration, were logged as internal errors. They are now client errors logged at warn. (e1c39de03, f452e99b5, 512baff4b) A client group stays with the user it was first opened for across view-syncer restarts; a connection as a different user is refused the same way it already was while the group was live. (8d9466b13) Inspector authentication is per connection, and the 1.12 client remembers a password the server accepted, so it doesn't prompt again after reconnecting. Repeated wrong admin or operator passwords are now rate limited, on the inspector and on the HTTP diagnostic endpoints. (e186c0d59, c767ac31c, 65375a770) zero-cache logs and traces show a hash of the clientGroupID instead of the ID. (adfce3301, de9121d99) Replication A numeric, float4 or float8 value between about 2^63 and 1e21 stopped replication with a RangeError, a numeric between 2^53 and 2^63 was stored as the wrong integer, and such a value in a row key or query literal failed the client group after a reload. These values now replicate as doubles. (340215ab5) A NULL element in a timestamp[], json[] or int8[] column stopped replication, and in text, enum, boolean and numeric arrays it was stored as \"NULL\", false or null. Array columns are now parsed correctly, including arrays with explicit bounds. Rows already stored keep their old values until they change. (fe4bc732a) A schema-change or lag-report message on the replication stream that zero-cache could not parse stopped replication. Such messages, and schema-change messages that its own upstream functions didn't emit, are now skipped. (08c634549, f6010bb34) With Litestream v5 backups on Google Cloud Storage or Azure, backup files were never compacted, so old ones were never deleted and the backup kept growing. (e2f030e63)", + "kind": "section" + }, + { + "id": "483-release-notes/1.12#queries-and-mutations", + "title": "Zero 1.12", + "searchTitle": "Queries and Mutations", + "sectionTitle": "Queries and Mutations", + "sectionId": "queries-and-mutations", + "url": "/docs/release-notes/1.12", + "content": "Server mutators and queries read time with time zone columns as negative milliseconds when the zone offset was positive, and a ServerSchema written by hand with the short timestamptz name read a timestamp as a time of day. (9a08f1e40) Server mutators and queries failed with type \"...\" does not exist for enum and other user-defined Postgres types outside the public schema. Casts are now schema-qualified. (8166580b0) (thanks @jschaf!) A push that failed at the API server could put other tabs of the same client group into the Error state, or fail a socket the client had already replaced. A failed push now affects only the socket that sent it, and that socket's queued pushes are re-sent on the next connection. (945ce6192) (thanks @pawarren!) runAnalyzeCLI --query no longer evaluates the query text as JavaScript. It accepts query builder calls, literals and arrow functions, and rejects anything else with its position. (02b6c1bbd)", + "kind": "section" + }, + { + "id": "484-release-notes/1.12#clients-and-react-native", + "title": "Zero 1.12", + "searchTitle": "Clients and React Native", + "sectionTitle": "Clients and React Native", + "sectionId": "clients-and-react-native", + "url": "/docs/release-notes/1.12", + "content": "On React Native, client garbage collection failed every 5 minutes, and queries whose related() entries weren't in alias order threw during normalization, because Hermes lacks Array.prototype.toSorted. (b62ded385) (thanks @wikil!) A SQLite-backed store whose close() threw blocked every later Zero instance with the same name from opening, and two overlapping close() calls could close a shared SQLite connection under another store. (dee69dc9d, cc861e8f6) (thanks @wikil!) An app that refreshed its auth token while the first connection of a new client group was still being validated could be refused for good with The initConnection message for a new client group must include client schema. (874e1ec9d) (thanks @YevheniiKotyrlo!) After zero-cache was rolled back, a client group holding a query the older server could not parse was stuck in an error state until the server rolled forward. The group is now reset and the client starts a fresh one. (dbf677638) A write that put keys from both the upper Basic Multilingual Plane (U+E000 to U+FFFF) and above it (such as emoji) into the same B-tree node failed with putMany entries must be sorted and unique, and a diff could report such a key as removed and re-added instead of changed. (0e15506e8) (thanks @natew!) Client garbage collection left a store open for every database it inspected. With SQLite, closing Zero then no longer closed its database connection, and a custom StoreProvider whose drop refuses an open store failed every collection. (bd436ef0d) (thanks @wikil!) Client, client group and connection IDs now come from crypto.getRandomValues when available instead of Math.random. (e80d6305c, c2ebe3df5)", + "kind": "section" + }, + { + "id": "485-release-notes/1.12#zero-cache-reliability", + "title": "Zero 1.12", + "searchTitle": "zero-cache Reliability", + "sectionTitle": "zero-cache Reliability", + "sectionId": "zero-cache-reliability", + "url": "/docs/release-notes/1.12", + "content": "A view-syncer that reloaded a client group's state after an error kept stale row records and query pipelines, which could crash it with Query … is not in the CVR or Unexpected undefined value, or leave rows that never reached the client. (1137280b6, bb348961f, 9e0e85b62) With a statement_timeout on the CVR database, connecting a client group with many rows could fail with canceling statement due to statement timeout. (892fa553b) View-syncers on slow or throttled disks spent up to a minute inside a single commit running SQLite checkpoints, and were dropped by the replication-manager as slow subscribers. Serving replicas are now checkpointed by a separate process. (fbdb27ed5) Clients were disabled and reloaded with SchemaVersionNotSupported while an upstream migration backfilled a table or column. They now retry with backoff until the backfill is done. (8280af82f) Malformed client messages, such as a second initConnection on a socket, an invalid baseCookie or an invalid query registration, were logged as internal errors. They are now client errors logged at warn. (e1c39de03, f452e99b5, 512baff4b) A client group stays with the user it was first opened for across view-syncer restarts; a connection as a different user is refused the same way it already was while the group was live. (8d9466b13) Inspector authentication is per connection, and the 1.12 client remembers a password the server accepted, so it doesn't prompt again after reconnecting. Repeated wrong admin or operator passwords are now rate limited, on the inspector and on the HTTP diagnostic endpoints. (e186c0d59, c767ac31c, 65375a770) zero-cache logs and traces show a hash of the clientGroupID instead of the ID. (adfce3301, de9121d99)", + "kind": "section" + }, + { + "id": "486-release-notes/1.12#replication", + "title": "Zero 1.12", + "searchTitle": "Replication", + "sectionTitle": "Replication", + "sectionId": "replication", + "url": "/docs/release-notes/1.12", + "content": "A numeric, float4 or float8 value between about 2^63 and 1e21 stopped replication with a RangeError, a numeric between 2^53 and 2^63 was stored as the wrong integer, and such a value in a row key or query literal failed the client group after a reload. These values now replicate as doubles. (340215ab5) A NULL element in a timestamp[], json[] or int8[] column stopped replication, and in text, enum, boolean and numeric arrays it was stored as \"NULL\", false or null. Array columns are now parsed correctly, including arrays with explicit bounds. Rows already stored keep their old values until they change. (fe4bc732a) A schema-change or lag-report message on the replication stream that zero-cache could not parse stopped replication. Such messages, and schema-change messages that its own upstream functions didn't emit, are now skipped. (08c634549, f6010bb34) With Litestream v5 backups on Google Cloud Storage or Azure, backup files were never compacted, so old ones were never deleted and the backup kept growing. (e2f030e63)", + "kind": "section" + }, + { + "id": "487-release-notes/1.12#breaking-changes", + "title": "Zero 1.12", + "searchTitle": "Breaking Changes", + "sectionTitle": "Breaking Changes", + "sectionId": "breaking-changes", + "url": "/docs/release-notes/1.12", + "content": "analyze-query bin removed: npx analyze-query is gone. Use a script that calls runAnalyzeCLI. @rocicorp/zero now depends on oxc-parser, which installs a native binding for your platform. (a3932f51e, 02b6c1bbd) userID needs a token: when ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL is set, a connection that passes a userID without an auth token is refused with Unauthorized. Omit userID for logged-out users; the deprecated userID: 'anon' without auth is refused too. (522688120) Changed defaults: ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA is on. It needs Litestream v5 backups, the default since 1.11, and each replication-manager holds its own slot while it runs. Turning it off doesn't resync. (49607b8b3) Multi-node secret: set ZERO_CHANGE_STREAMER_SECRET on the view-syncers first, then on the replication-manager. Until you do, the replication-manager still accepts view-syncers without it, but warns at startup. If one of your nodes runs sync workers and is also the replication-manager for other view-syncers, it now makes up its own secret, so set the secret on every node. (b2b8f68e4) Legacy CRUD mutations are keyed by the primary key of the replica's table, not by the key the client sends. This only matters if your client schema declares a different primary key than the table has in Postgres. (f24fddae7) Rollback: clients keep their local database and unsent mutations when moving between 1.11 and 1.12 in either direction. One exception: a client group that registered an aggregate, groupBy, distinct or JSON path query is reset when zero-cache is rolled back to 1.11, and its unsent mutations are lost. (8b716b3fe, dbf677638)", + "kind": "section" + }, + { + "id": "57-release-notes/1.2", "title": "Zero 1.2", "searchTitle": "Zero 1.2", "url": "/docs/release-notes/1.2", @@ -6045,7 +6307,7 @@ "kind": "page" }, { - "id": "469-release-notes/1.2#installation", + "id": "488-release-notes/1.2#installation", "title": "Zero 1.2", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6055,7 +6317,7 @@ "kind": "section" }, { - "id": "470-release-notes/1.2#features", + "id": "489-release-notes/1.2#features", "title": "Zero 1.2", "searchTitle": "Features", "sectionTitle": "Features", @@ -6065,7 +6327,7 @@ "kind": "section" }, { - "id": "471-release-notes/1.2#performance", + "id": "490-release-notes/1.2#performance", "title": "Zero 1.2", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6075,7 +6337,7 @@ "kind": "section" }, { - "id": "472-release-notes/1.2#fixes", + "id": "491-release-notes/1.2#fixes", "title": "Zero 1.2", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6085,7 +6347,7 @@ "kind": "section" }, { - "id": "473-release-notes/1.2#breaking-changes", + "id": "492-release-notes/1.2#breaking-changes", "title": "Zero 1.2", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6095,7 +6357,7 @@ "kind": "section" }, { - "id": "57-release-notes/1.3", + "id": "58-release-notes/1.3", "title": "Zero 1.3", "searchTitle": "Zero 1.3", "url": "/docs/release-notes/1.3", @@ -6125,7 +6387,7 @@ "kind": "page" }, { - "id": "474-release-notes/1.3#installation", + "id": "493-release-notes/1.3#installation", "title": "Zero 1.3", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6135,7 +6397,7 @@ "kind": "section" }, { - "id": "475-release-notes/1.3#features", + "id": "494-release-notes/1.3#features", "title": "Zero 1.3", "searchTitle": "Features", "sectionTitle": "Features", @@ -6145,7 +6407,7 @@ "kind": "section" }, { - "id": "476-release-notes/1.3#performance", + "id": "495-release-notes/1.3#performance", "title": "Zero 1.3", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6155,7 +6417,7 @@ "kind": "section" }, { - "id": "477-release-notes/1.3#fixes", + "id": "496-release-notes/1.3#fixes", "title": "Zero 1.3", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6165,7 +6427,7 @@ "kind": "section" }, { - "id": "478-release-notes/1.3#breaking-changes", + "id": "497-release-notes/1.3#breaking-changes", "title": "Zero 1.3", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6175,7 +6437,7 @@ "kind": "section" }, { - "id": "58-release-notes/1.4", + "id": "59-release-notes/1.4", "title": "Zero 1.4", "searchTitle": "Zero 1.4", "url": "/docs/release-notes/1.4", @@ -6213,7 +6475,7 @@ "kind": "page" }, { - "id": "479-release-notes/1.4#installation", + "id": "498-release-notes/1.4#installation", "title": "Zero 1.4", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6223,7 +6485,7 @@ "kind": "section" }, { - "id": "480-release-notes/1.4#upgrading", + "id": "499-release-notes/1.4#upgrading", "title": "Zero 1.4", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -6233,7 +6495,7 @@ "kind": "section" }, { - "id": "481-release-notes/1.4#userid-anon", + "id": "500-release-notes/1.4#userid-anon", "title": "Zero 1.4", "searchTitle": "userID: \"anon\"", "sectionTitle": "userID: \"anon\"", @@ -6243,7 +6505,7 @@ "kind": "section" }, { - "id": "482-release-notes/1.4#features", + "id": "501-release-notes/1.4#features", "title": "Zero 1.4", "searchTitle": "Features", "sectionTitle": "Features", @@ -6253,7 +6515,7 @@ "kind": "section" }, { - "id": "483-release-notes/1.4#performance", + "id": "502-release-notes/1.4#performance", "title": "Zero 1.4", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6263,7 +6525,7 @@ "kind": "section" }, { - "id": "484-release-notes/1.4#fixes", + "id": "503-release-notes/1.4#fixes", "title": "Zero 1.4", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6273,7 +6535,7 @@ "kind": "section" }, { - "id": "485-release-notes/1.4#breaking-changes", + "id": "504-release-notes/1.4#breaking-changes", "title": "Zero 1.4", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6283,7 +6545,7 @@ "kind": "section" }, { - "id": "59-release-notes/1.5", + "id": "60-release-notes/1.5", "title": "Zero 1.5", "searchTitle": "Zero 1.5", "url": "/docs/release-notes/1.5", @@ -6325,7 +6587,7 @@ "kind": "page" }, { - "id": "486-release-notes/1.5#installation", + "id": "505-release-notes/1.5#installation", "title": "Zero 1.5", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6335,7 +6597,7 @@ "kind": "section" }, { - "id": "487-release-notes/1.5#upgrading", + "id": "506-release-notes/1.5#upgrading", "title": "Zero 1.5", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -6345,7 +6607,7 @@ "kind": "section" }, { - "id": "488-release-notes/1.5#authenticated-client-groups", + "id": "507-release-notes/1.5#authenticated-client-groups", "title": "Zero 1.5", "searchTitle": "Authenticated Client Groups", "sectionTitle": "Authenticated Client Groups", @@ -6355,7 +6617,7 @@ "kind": "section" }, { - "id": "489-release-notes/1.5#deploy-order", + "id": "508-release-notes/1.5#deploy-order", "title": "Zero 1.5", "searchTitle": "Deploy Order", "sectionTitle": "Deploy Order", @@ -6365,7 +6627,7 @@ "kind": "section" }, { - "id": "490-release-notes/1.5#features", + "id": "509-release-notes/1.5#features", "title": "Zero 1.5", "searchTitle": "Features", "sectionTitle": "Features", @@ -6375,7 +6637,7 @@ "kind": "section" }, { - "id": "491-release-notes/1.5#performance", + "id": "510-release-notes/1.5#performance", "title": "Zero 1.5", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6385,7 +6647,7 @@ "kind": "section" }, { - "id": "492-release-notes/1.5#fixes", + "id": "511-release-notes/1.5#fixes", "title": "Zero 1.5", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6395,7 +6657,7 @@ "kind": "section" }, { - "id": "493-release-notes/1.5#breaking-changes", + "id": "512-release-notes/1.5#breaking-changes", "title": "Zero 1.5", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6405,7 +6667,7 @@ "kind": "section" }, { - "id": "60-release-notes/1.6", + "id": "61-release-notes/1.6", "title": "Zero 1.6", "searchTitle": "Zero 1.6", "url": "/docs/release-notes/1.6", @@ -6443,7 +6705,7 @@ "kind": "page" }, { - "id": "494-release-notes/1.6#installation", + "id": "513-release-notes/1.6#installation", "title": "Zero 1.6", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6453,7 +6715,7 @@ "kind": "section" }, { - "id": "495-release-notes/1.6#upgrading", + "id": "514-release-notes/1.6#upgrading", "title": "Zero 1.6", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -6463,7 +6725,7 @@ "kind": "section" }, { - "id": "496-release-notes/1.6#planetscale-failover", + "id": "515-release-notes/1.6#planetscale-failover", "title": "Zero 1.6", "searchTitle": "PlanetScale Failover", "sectionTitle": "PlanetScale Failover", @@ -6473,7 +6735,7 @@ "kind": "section" }, { - "id": "497-release-notes/1.6#features", + "id": "516-release-notes/1.6#features", "title": "Zero 1.6", "searchTitle": "Features", "sectionTitle": "Features", @@ -6483,7 +6745,7 @@ "kind": "section" }, { - "id": "498-release-notes/1.6#performance", + "id": "517-release-notes/1.6#performance", "title": "Zero 1.6", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6493,7 +6755,7 @@ "kind": "section" }, { - "id": "499-release-notes/1.6#fixes", + "id": "518-release-notes/1.6#fixes", "title": "Zero 1.6", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6503,7 +6765,7 @@ "kind": "section" }, { - "id": "500-release-notes/1.6#breaking-changes", + "id": "519-release-notes/1.6#breaking-changes", "title": "Zero 1.6", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6513,7 +6775,7 @@ "kind": "section" }, { - "id": "61-release-notes/1.7", + "id": "62-release-notes/1.7", "title": "Zero 1.7", "searchTitle": "Zero 1.7", "url": "/docs/release-notes/1.7", @@ -6555,7 +6817,7 @@ "kind": "page" }, { - "id": "501-release-notes/1.7#installation", + "id": "520-release-notes/1.7#installation", "title": "Zero 1.7", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6565,7 +6827,7 @@ "kind": "section" }, { - "id": "502-release-notes/1.7#overview", + "id": "521-release-notes/1.7#overview", "title": "Zero 1.7", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6575,7 +6837,7 @@ "kind": "section" }, { - "id": "503-release-notes/1.7#features", + "id": "522-release-notes/1.7#features", "title": "Zero 1.7", "searchTitle": "Features", "sectionTitle": "Features", @@ -6585,7 +6847,7 @@ "kind": "section" }, { - "id": "504-release-notes/1.7#performance", + "id": "523-release-notes/1.7#performance", "title": "Zero 1.7", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6595,7 +6857,7 @@ "kind": "section" }, { - "id": "505-release-notes/1.7#replication", + "id": "524-release-notes/1.7#replication", "title": "Zero 1.7", "searchTitle": "Replication", "sectionTitle": "Replication", @@ -6605,7 +6867,7 @@ "kind": "section" }, { - "id": "506-release-notes/1.7#flipped-exists-queries", + "id": "525-release-notes/1.7#flipped-exists-queries", "title": "Zero 1.7", "searchTitle": "Flipped Exists Queries", "sectionTitle": "Flipped Exists Queries", @@ -6615,7 +6877,7 @@ "kind": "section" }, { - "id": "507-release-notes/1.7#fixes", + "id": "526-release-notes/1.7#fixes", "title": "Zero 1.7", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6625,7 +6887,7 @@ "kind": "section" }, { - "id": "508-release-notes/1.7#breaking-changes", + "id": "527-release-notes/1.7#breaking-changes", "title": "Zero 1.7", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6635,7 +6897,7 @@ "kind": "section" }, { - "id": "62-release-notes/1.8", + "id": "63-release-notes/1.8", "title": "Zero 1.8", "searchTitle": "Zero 1.8", "url": "/docs/release-notes/1.8", @@ -6681,7 +6943,7 @@ "kind": "page" }, { - "id": "509-release-notes/1.8#installation", + "id": "528-release-notes/1.8#installation", "title": "Zero 1.8", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6691,7 +6953,7 @@ "kind": "section" }, { - "id": "510-release-notes/1.8#overview", + "id": "529-release-notes/1.8#overview", "title": "Zero 1.8", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6701,7 +6963,7 @@ "kind": "section" }, { - "id": "511-release-notes/1.8#features", + "id": "530-release-notes/1.8#features", "title": "Zero 1.8", "searchTitle": "Features", "sectionTitle": "Features", @@ -6711,7 +6973,7 @@ "kind": "section" }, { - "id": "512-release-notes/1.8#performance", + "id": "531-release-notes/1.8#performance", "title": "Zero 1.8", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6721,7 +6983,7 @@ "kind": "section" }, { - "id": "513-release-notes/1.8#replicating-large-transactions", + "id": "532-release-notes/1.8#replicating-large-transactions", "title": "Zero 1.8", "searchTitle": "Replicating Large Transactions", "sectionTitle": "Replicating Large Transactions", @@ -6731,7 +6993,7 @@ "kind": "section" }, { - "id": "514-release-notes/1.8#maintaining-limit-queries", + "id": "533-release-notes/1.8#maintaining-limit-queries", "title": "Zero 1.8", "searchTitle": "Maintaining limit() Queries", "sectionTitle": "Maintaining limit() Queries", @@ -6741,7 +7003,7 @@ "kind": "section" }, { - "id": "515-release-notes/1.8#client-side-hydration", + "id": "534-release-notes/1.8#client-side-hydration", "title": "Zero 1.8", "searchTitle": "Client-Side Hydration", "sectionTitle": "Client-Side Hydration", @@ -6751,7 +7013,7 @@ "kind": "section" }, { - "id": "516-release-notes/1.8#fixes", + "id": "535-release-notes/1.8#fixes", "title": "Zero 1.8", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6761,7 +7023,7 @@ "kind": "section" }, { - "id": "517-release-notes/1.8#breaking-changes", + "id": "536-release-notes/1.8#breaking-changes", "title": "Zero 1.8", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6771,7 +7033,7 @@ "kind": "section" }, { - "id": "63-release-notes/1.9", + "id": "64-release-notes/1.9", "title": "Zero 1.9", "searchTitle": "Zero 1.9", "url": "/docs/release-notes/1.9", @@ -6805,7 +7067,7 @@ "kind": "page" }, { - "id": "518-release-notes/1.9#installation", + "id": "537-release-notes/1.9#installation", "title": "Zero 1.9", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6815,7 +7077,7 @@ "kind": "section" }, { - "id": "519-release-notes/1.9#overview", + "id": "538-release-notes/1.9#overview", "title": "Zero 1.9", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6825,7 +7087,7 @@ "kind": "section" }, { - "id": "520-release-notes/1.9#features", + "id": "539-release-notes/1.9#features", "title": "Zero 1.9", "searchTitle": "Features", "sectionTitle": "Features", @@ -6835,7 +7097,7 @@ "kind": "section" }, { - "id": "521-release-notes/1.9#performance", + "id": "540-release-notes/1.9#performance", "title": "Zero 1.9", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6845,7 +7107,7 @@ "kind": "section" }, { - "id": "522-release-notes/1.9#cold-mutation-latency", + "id": "541-release-notes/1.9#cold-mutation-latency", "title": "Zero 1.9", "searchTitle": "Cold Mutation Latency", "sectionTitle": "Cold Mutation Latency", @@ -6855,7 +7117,7 @@ "kind": "section" }, { - "id": "523-release-notes/1.9#fixes", + "id": "542-release-notes/1.9#fixes", "title": "Zero 1.9", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6865,16 +7127,16 @@ "kind": "section" }, { - "id": "64-release-notes", + "id": "65-release-notes", "title": "Release Notes", "searchTitle": "Release Notes", "url": "/docs/release-notes", - "content": "Zero 1.11: Cached Results and Sturdier Client Storage Zero 1.10: Unique Keys and More Reliable Replication Zero 1.9: Stability and Query Correctness Zero 1.8: Observability and Reliability Zero 1.7: Query Correctness and Performance Zero 1.6: PlanetScale Failover Support Zero 1.5: Schema Change Improvements and Client Group Auth Zero 1.4: Performance and Reliability Improvements Zero 1.3: Faster Initial Sync and Other Perf Improvements Zero 1.2: IVM Performance and Bug Fixes Zero 1.1: Replication Monitoring Zero 1.0: First Stable Release Zero 0.26: Schema Backfill and Scalar Subqueries Zero 0.25: DX Overhaul, Query Planning Zero 0.24: Join Flipping, Cookie Auth, Inspector Updates Zero 0.23: Synced Queries and React Native Support Zero 0.22: Simplified TTLs Zero 0.21: PG arrays, TanStack starter, and more Zero 0.20: Full Supabase support, performance improvements Zero 0.19: Many, many bugfixes and cleanups Zero 0.18: Custom Mutators Zero 0.17: Background Queries Zero 0.16: Lambda-Based Permission Deployment Zero 0.15: Live Permission Updates Zero 0.14: Name Mapping and Multischema Zero 0.13: Multinode and SST Zero 0.12: Circular Relationships Zero 0.11: Windows Zero 0.10: Remove Top-Level Await Zero 0.9: JWK Support Zero 0.8: Schema Autobuild, Result Types, and Enums Zero 0.7: Read Perms and Docker Zero 0.6: Relationship Filters Zero 0.5: JSON Columns Zero 0.4: Compound Filters Zero 0.3: Schema Migrations and Write Perms Zero 0.2: Skip Mode and Computed PKs Zero 0.1: First Release", + "content": "Zero 1.12: Synced Aggregates and JSON Path Filters Zero 1.11: Cached Results and Sturdier Client Storage Zero 1.10: Unique Keys and More Reliable Replication Zero 1.9: Stability and Query Correctness Zero 1.8: Observability and Reliability Zero 1.7: Query Correctness and Performance Zero 1.6: PlanetScale Failover Support Zero 1.5: Schema Change Improvements and Client Group Auth Zero 1.4: Performance and Reliability Improvements Zero 1.3: Faster Initial Sync and Other Perf Improvements Zero 1.2: IVM Performance and Bug Fixes Zero 1.1: Replication Monitoring Zero 1.0: First Stable Release Zero 0.26: Schema Backfill and Scalar Subqueries Zero 0.25: DX Overhaul, Query Planning Zero 0.24: Join Flipping, Cookie Auth, Inspector Updates Zero 0.23: Synced Queries and React Native Support Zero 0.22: Simplified TTLs Zero 0.21: PG arrays, TanStack starter, and more Zero 0.20: Full Supabase support, performance improvements Zero 0.19: Many, many bugfixes and cleanups Zero 0.18: Custom Mutators Zero 0.17: Background Queries Zero 0.16: Lambda-Based Permission Deployment Zero 0.15: Live Permission Updates Zero 0.14: Name Mapping and Multischema Zero 0.13: Multinode and SST Zero 0.12: Circular Relationships Zero 0.11: Windows Zero 0.10: Remove Top-Level Await Zero 0.9: JWK Support Zero 0.8: Schema Autobuild, Result Types, and Enums Zero 0.7: Read Perms and Docker Zero 0.6: Relationship Filters Zero 0.5: JSON Columns Zero 0.4: Compound Filters Zero 0.3: Schema Migrations and Write Perms Zero 0.2: Skip Mode and Computed PKs Zero 0.1: First Release", "headings": [], "kind": "page" }, { - "id": "65-reporting-bugs", + "id": "66-reporting-bugs", "title": "Reporting Bugs", "searchTitle": "Reporting Bugs", "url": "/docs/reporting-bugs", @@ -6892,7 +7154,7 @@ "kind": "page" }, { - "id": "524-reporting-bugs#zbugs", + "id": "543-reporting-bugs#zbugs", "title": "Reporting Bugs", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -6902,7 +7164,7 @@ "kind": "section" }, { - "id": "525-reporting-bugs#discord", + "id": "544-reporting-bugs#discord", "title": "Reporting Bugs", "searchTitle": "Discord", "sectionTitle": "Discord", @@ -6912,7 +7174,7 @@ "kind": "section" }, { - "id": "66-rest", + "id": "67-rest", "title": "REST", "searchTitle": "REST", "url": "/docs/rest", @@ -6938,7 +7200,7 @@ "kind": "page" }, { - "id": "526-rest#pattern", + "id": "545-rest#pattern", "title": "REST", "searchTitle": "Pattern", "sectionTitle": "Pattern", @@ -6948,7 +7210,7 @@ "kind": "section" }, { - "id": "527-rest#tanstack-start-example", + "id": "546-rest#tanstack-start-example", "title": "REST", "searchTitle": "TanStack Start Example", "sectionTitle": "TanStack Start Example", @@ -6958,7 +7220,7 @@ "kind": "section" }, { - "id": "528-rest#openapi-generation", + "id": "547-rest#openapi-generation", "title": "REST", "searchTitle": "OpenAPI Generation", "sectionTitle": "OpenAPI Generation", @@ -6968,7 +7230,7 @@ "kind": "section" }, { - "id": "529-rest#full-working-example", + "id": "548-rest#full-working-example", "title": "REST", "searchTitle": "Full Working Example", "sectionTitle": "Full Working Example", @@ -6978,7 +7240,7 @@ "kind": "section" }, { - "id": "67-roadmap", + "id": "68-roadmap", "title": "Roadmap", "searchTitle": "Roadmap", "url": "/docs/roadmap", @@ -6996,7 +7258,7 @@ "kind": "page" }, { - "id": "530-roadmap#q4-2025", + "id": "549-roadmap#q4-2025", "title": "Roadmap", "searchTitle": "Q4 2025", "sectionTitle": "Q4 2025", @@ -7006,7 +7268,7 @@ "kind": "section" }, { - "id": "531-roadmap#beyond", + "id": "550-roadmap#beyond", "title": "Roadmap", "searchTitle": "Beyond", "sectionTitle": "Beyond", @@ -7016,7 +7278,7 @@ "kind": "section" }, { - "id": "68-samples", + "id": "69-samples", "title": "Samples", "searchTitle": "Samples", "url": "/docs/samples", @@ -7042,7 +7304,7 @@ "kind": "page" }, { - "id": "532-samples#gigabugs", + "id": "551-samples#gigabugs", "title": "Samples", "searchTitle": "Gigabugs", "sectionTitle": "Gigabugs", @@ -7052,7 +7314,7 @@ "kind": "section" }, { - "id": "533-samples#ztunes", + "id": "552-samples#ztunes", "title": "Samples", "searchTitle": "ztunes", "sectionTitle": "ztunes", @@ -7062,7 +7324,7 @@ "kind": "section" }, { - "id": "534-samples#zslack", + "id": "553-samples#zslack", "title": "Samples", "searchTitle": "zslack", "sectionTitle": "zslack", @@ -7072,7 +7334,7 @@ "kind": "section" }, { - "id": "535-samples#zero-music", + "id": "554-samples#zero-music", "title": "Samples", "searchTitle": "zero-music", "sectionTitle": "zero-music", @@ -7082,7 +7344,7 @@ "kind": "section" }, { - "id": "69-schema", + "id": "70-schema", "title": "Zero Schema", "searchTitle": "Zero Schema", "url": "/docs/schema", @@ -7212,7 +7474,7 @@ "kind": "page" }, { - "id": "536-schema#generating-from-database", + "id": "555-schema#generating-from-database", "title": "Zero Schema", "searchTitle": "Generating from Database", "sectionTitle": "Generating from Database", @@ -7222,7 +7484,7 @@ "kind": "section" }, { - "id": "537-schema#writing-by-hand", + "id": "556-schema#writing-by-hand", "title": "Zero Schema", "searchTitle": "Writing by Hand", "sectionTitle": "Writing by Hand", @@ -7232,7 +7494,7 @@ "kind": "section" }, { - "id": "538-schema#table-schemas", + "id": "557-schema#table-schemas", "title": "Zero Schema", "searchTitle": "Table Schemas", "sectionTitle": "Table Schemas", @@ -7242,7 +7504,7 @@ "kind": "section" }, { - "id": "539-schema#name-mapping", + "id": "558-schema#name-mapping", "title": "Zero Schema", "searchTitle": "Name Mapping", "sectionTitle": "Name Mapping", @@ -7252,7 +7514,7 @@ "kind": "section" }, { - "id": "540-schema#multiple-schemas", + "id": "559-schema#multiple-schemas", "title": "Zero Schema", "searchTitle": "Multiple Schemas", "sectionTitle": "Multiple Schemas", @@ -7262,7 +7524,7 @@ "kind": "section" }, { - "id": "541-schema#optional-columns", + "id": "560-schema#optional-columns", "title": "Zero Schema", "searchTitle": "Optional Columns", "sectionTitle": "Optional Columns", @@ -7272,7 +7534,7 @@ "kind": "section" }, { - "id": "542-schema#enumerations", + "id": "561-schema#enumerations", "title": "Zero Schema", "searchTitle": "Enumerations", "sectionTitle": "Enumerations", @@ -7282,7 +7544,7 @@ "kind": "section" }, { - "id": "543-schema#custom-json-types", + "id": "562-schema#custom-json-types", "title": "Zero Schema", "searchTitle": "Custom JSON Types", "sectionTitle": "Custom JSON Types", @@ -7292,7 +7554,7 @@ "kind": "section" }, { - "id": "544-schema#compound-primary-keys", + "id": "563-schema#compound-primary-keys", "title": "Zero Schema", "searchTitle": "Compound Primary Keys", "sectionTitle": "Compound Primary Keys", @@ -7302,7 +7564,7 @@ "kind": "section" }, { - "id": "545-schema#unique-keys", + "id": "564-schema#unique-keys", "title": "Zero Schema", "searchTitle": "Unique Keys", "sectionTitle": "Unique Keys", @@ -7312,7 +7574,7 @@ "kind": "section" }, { - "id": "546-schema#relationships", + "id": "565-schema#relationships", "title": "Zero Schema", "searchTitle": "Relationships", "sectionTitle": "Relationships", @@ -7322,7 +7584,7 @@ "kind": "section" }, { - "id": "547-schema#many-to-many-relationships", + "id": "566-schema#many-to-many-relationships", "title": "Zero Schema", "searchTitle": "Many-to-Many Relationships", "sectionTitle": "Many-to-Many Relationships", @@ -7332,7 +7594,7 @@ "kind": "section" }, { - "id": "548-schema#compound-keys-relationships", + "id": "567-schema#compound-keys-relationships", "title": "Zero Schema", "searchTitle": "Compound Keys Relationships", "sectionTitle": "Compound Keys Relationships", @@ -7342,7 +7604,7 @@ "kind": "section" }, { - "id": "549-schema#circular-relationships", + "id": "568-schema#circular-relationships", "title": "Zero Schema", "searchTitle": "Circular Relationships", "sectionTitle": "Circular Relationships", @@ -7352,7 +7614,7 @@ "kind": "section" }, { - "id": "550-schema#database-schemas", + "id": "569-schema#database-schemas", "title": "Zero Schema", "searchTitle": "Database Schemas", "sectionTitle": "Database Schemas", @@ -7362,7 +7624,7 @@ "kind": "section" }, { - "id": "551-schema#register-schema-type", + "id": "570-schema#register-schema-type", "title": "Zero Schema", "searchTitle": "Register Schema Type", "sectionTitle": "Register Schema Type", @@ -7372,7 +7634,7 @@ "kind": "section" }, { - "id": "552-schema#schema-changes", + "id": "571-schema#schema-changes", "title": "Zero Schema", "searchTitle": "Schema Changes", "sectionTitle": "Schema Changes", @@ -7382,7 +7644,7 @@ "kind": "section" }, { - "id": "553-schema#development", + "id": "572-schema#development", "title": "Zero Schema", "searchTitle": "Development", "sectionTitle": "Development", @@ -7392,7 +7654,7 @@ "kind": "section" }, { - "id": "554-schema#production", + "id": "573-schema#production", "title": "Zero Schema", "searchTitle": "Production", "sectionTitle": "Production", @@ -7402,7 +7664,7 @@ "kind": "section" }, { - "id": "555-schema#expand-changes", + "id": "574-schema#expand-changes", "title": "Zero Schema", "searchTitle": "Expand Changes", "sectionTitle": "Expand Changes", @@ -7412,7 +7674,7 @@ "kind": "section" }, { - "id": "556-schema#contract-changes", + "id": "575-schema#contract-changes", "title": "Zero Schema", "searchTitle": "Contract Changes", "sectionTitle": "Contract Changes", @@ -7422,7 +7684,7 @@ "kind": "section" }, { - "id": "557-schema#compound-changes", + "id": "576-schema#compound-changes", "title": "Zero Schema", "searchTitle": "Compound Changes", "sectionTitle": "Compound Changes", @@ -7432,7 +7694,7 @@ "kind": "section" }, { - "id": "558-schema#examples", + "id": "577-schema#examples", "title": "Zero Schema", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -7442,7 +7704,7 @@ "kind": "section" }, { - "id": "559-schema#adding-a-column", + "id": "578-schema#adding-a-column", "title": "Zero Schema", "searchTitle": "Adding a Column", "sectionTitle": "Adding a Column", @@ -7452,7 +7714,7 @@ "kind": "section" }, { - "id": "560-schema#removing-a-column", + "id": "579-schema#removing-a-column", "title": "Zero Schema", "searchTitle": "Removing a Column", "sectionTitle": "Removing a Column", @@ -7462,7 +7724,7 @@ "kind": "section" }, { - "id": "561-schema#renaming-a-column", + "id": "580-schema#renaming-a-column", "title": "Zero Schema", "searchTitle": "Renaming a Column", "sectionTitle": "Renaming a Column", @@ -7472,7 +7734,7 @@ "kind": "section" }, { - "id": "562-schema#making-a-column-optional", + "id": "581-schema#making-a-column-optional", "title": "Zero Schema", "searchTitle": "Making a Column Optional", "sectionTitle": "Making a Column Optional", @@ -7482,7 +7744,7 @@ "kind": "section" }, { - "id": "563-schema#quick-reference", + "id": "582-schema#quick-reference", "title": "Zero Schema", "searchTitle": "Quick Reference", "sectionTitle": "Quick Reference", @@ -7492,7 +7754,7 @@ "kind": "section" }, { - "id": "564-schema#backfill", + "id": "583-schema#backfill", "title": "Zero Schema", "searchTitle": "Backfill", "sectionTitle": "Backfill", @@ -7502,7 +7764,7 @@ "kind": "section" }, { - "id": "565-schema#monitoring-backfill-progress", + "id": "584-schema#monitoring-backfill-progress", "title": "Zero Schema", "searchTitle": "Monitoring Backfill Progress", "sectionTitle": "Monitoring Backfill Progress", @@ -7512,7 +7774,7 @@ "kind": "section" }, { - "id": "70-self-host", + "id": "71-self-host", "title": "Self-Hosting Zero", "searchTitle": "Self-Hosting Zero", "url": "/docs/self-host", @@ -7574,7 +7836,7 @@ "kind": "page" }, { - "id": "566-self-host#docker-images", + "id": "585-self-host#docker-images", "title": "Self-Hosting Zero", "searchTitle": "Docker Images", "sectionTitle": "Docker Images", @@ -7584,7 +7846,7 @@ "kind": "section" }, { - "id": "567-self-host#minimum-viable-strategy", + "id": "586-self-host#minimum-viable-strategy", "title": "Self-Hosting Zero", "searchTitle": "Minimum Viable Strategy", "sectionTitle": "Minimum Viable Strategy", @@ -7594,7 +7856,7 @@ "kind": "section" }, { - "id": "568-self-host#maximal-strategy", + "id": "587-self-host#maximal-strategy", "title": "Self-Hosting Zero", "searchTitle": "Maximal Strategy", "sectionTitle": "Maximal Strategy", @@ -7604,7 +7866,7 @@ "kind": "section" }, { - "id": "569-self-host#replica-lifecycle", + "id": "588-self-host#replica-lifecycle", "title": "Self-Hosting Zero", "searchTitle": "Replica Lifecycle", "sectionTitle": "Replica Lifecycle", @@ -7614,7 +7876,7 @@ "kind": "section" }, { - "id": "570-self-host#performance", + "id": "589-self-host#performance", "title": "Self-Hosting Zero", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -7624,7 +7886,7 @@ "kind": "section" }, { - "id": "571-self-host#hydration", + "id": "590-self-host#hydration", "title": "Self-Hosting Zero", "searchTitle": "Hydration", "sectionTitle": "Hydration", @@ -7634,7 +7896,7 @@ "kind": "section" }, { - "id": "572-self-host#ivm-advancement", + "id": "591-self-host#ivm-advancement", "title": "Self-Hosting Zero", "searchTitle": "IVM advancement", "sectionTitle": "IVM advancement", @@ -7644,7 +7906,7 @@ "kind": "section" }, { - "id": "573-self-host#system-level", + "id": "592-self-host#system-level", "title": "Self-Hosting Zero", "searchTitle": "System-level", "sectionTitle": "System-level", @@ -7654,7 +7916,7 @@ "kind": "section" }, { - "id": "574-self-host#networking", + "id": "593-self-host#networking", "title": "Self-Hosting Zero", "searchTitle": "Networking", "sectionTitle": "Networking", @@ -7664,7 +7926,7 @@ "kind": "section" }, { - "id": "575-self-host#sticky-sessions", + "id": "594-self-host#sticky-sessions", "title": "Self-Hosting Zero", "searchTitle": "Sticky Sessions", "sectionTitle": "Sticky Sessions", @@ -7674,7 +7936,7 @@ "kind": "section" }, { - "id": "576-self-host#rolling-updates", + "id": "595-self-host#rolling-updates", "title": "Self-Hosting Zero", "searchTitle": "Rolling Updates", "sectionTitle": "Rolling Updates", @@ -7684,7 +7946,7 @@ "kind": "section" }, { - "id": "577-self-host#clientserver-version-compatibility", + "id": "596-self-host#clientserver-version-compatibility", "title": "Self-Hosting Zero", "searchTitle": "Client/Server Version Compatibility", "sectionTitle": "Client/Server Version Compatibility", @@ -7694,7 +7956,7 @@ "kind": "section" }, { - "id": "578-self-host#configuration", + "id": "597-self-host#configuration", "title": "Self-Hosting Zero", "searchTitle": "Configuration", "sectionTitle": "Configuration", @@ -7704,7 +7966,7 @@ "kind": "section" }, { - "id": "71-server-zql", + "id": "72-server-zql", "title": "ZQL on the Server", "searchTitle": "ZQL on the Server", "url": "/docs/server-zql", @@ -7734,7 +7996,7 @@ "kind": "page" }, { - "id": "579-server-zql#creating-a-database", + "id": "598-server-zql#creating-a-database", "title": "ZQL on the Server", "searchTitle": "Creating a Database", "sectionTitle": "Creating a Database", @@ -7744,7 +8006,7 @@ "kind": "section" }, { - "id": "580-server-zql#isolation-level", + "id": "599-server-zql#isolation-level", "title": "ZQL on the Server", "searchTitle": "Isolation Level", "sectionTitle": "Isolation Level", @@ -7754,7 +8016,7 @@ "kind": "section" }, { - "id": "581-server-zql#custom-database", + "id": "600-server-zql#custom-database", "title": "ZQL on the Server", "searchTitle": "Custom Database", "sectionTitle": "Custom Database", @@ -7764,7 +8026,7 @@ "kind": "section" }, { - "id": "582-server-zql#running-zql", + "id": "601-server-zql#running-zql", "title": "ZQL on the Server", "searchTitle": "Running ZQL", "sectionTitle": "Running ZQL", @@ -7774,7 +8036,7 @@ "kind": "section" }, { - "id": "583-server-zql#ssr", + "id": "602-server-zql#ssr", "title": "ZQL on the Server", "searchTitle": "SSR", "sectionTitle": "SSR", @@ -7784,7 +8046,7 @@ "kind": "section" }, { - "id": "72-solidjs", + "id": "73-solidjs", "title": "SolidJS", "searchTitle": "SolidJS", "url": "/docs/solidjs", @@ -7806,7 +8068,7 @@ "kind": "page" }, { - "id": "584-solidjs#setup", + "id": "603-solidjs#setup", "title": "SolidJS", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -7816,7 +8078,7 @@ "kind": "section" }, { - "id": "585-solidjs#usage", + "id": "604-solidjs#usage", "title": "SolidJS", "searchTitle": "Usage", "sectionTitle": "Usage", @@ -7826,7 +8088,7 @@ "kind": "section" }, { - "id": "586-solidjs#examples", + "id": "605-solidjs#examples", "title": "SolidJS", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -7836,7 +8098,7 @@ "kind": "section" }, { - "id": "73-status", + "id": "74-status", "title": "Project Status", "searchTitle": "Project Status", "url": "/docs/status", @@ -7862,7 +8124,7 @@ "kind": "page" }, { - "id": "587-status#breaking-changes", + "id": "606-status#breaking-changes", "title": "Project Status", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -7872,7 +8134,7 @@ "kind": "section" }, { - "id": "588-status#roadmap", + "id": "607-status#roadmap", "title": "Project Status", "searchTitle": "Roadmap", "sectionTitle": "Roadmap", @@ -7882,7 +8144,7 @@ "kind": "section" }, { - "id": "589-status#2026", + "id": "608-status#2026", "title": "Project Status", "searchTitle": "2026", "sectionTitle": "2026", @@ -7892,7 +8154,7 @@ "kind": "section" }, { - "id": "590-status#soon", + "id": "609-status#soon", "title": "Project Status", "searchTitle": "Soon", "sectionTitle": "Soon", @@ -7902,7 +8164,7 @@ "kind": "section" }, { - "id": "74-sync", + "id": "75-sync", "title": "What is Sync?", "searchTitle": "What is Sync?", "url": "/docs/sync", @@ -7924,7 +8186,7 @@ "kind": "page" }, { - "id": "591-sync#problem", + "id": "610-sync#problem", "title": "What is Sync?", "searchTitle": "Problem", "sectionTitle": "Problem", @@ -7934,7 +8196,7 @@ "kind": "section" }, { - "id": "592-sync#solution", + "id": "611-sync#solution", "title": "What is Sync?", "searchTitle": "Solution", "sectionTitle": "Solution", @@ -7944,7 +8206,7 @@ "kind": "section" }, { - "id": "593-sync#history-of-sync", + "id": "612-sync#history-of-sync", "title": "What is Sync?", "searchTitle": "History of Sync", "sectionTitle": "History of Sync", @@ -7954,7 +8216,7 @@ "kind": "section" }, { - "id": "75-tutorial", + "id": "76-tutorial", "title": "Tutorial", "searchTitle": "Tutorial", "url": "/docs/tutorial", @@ -8028,7 +8290,7 @@ "kind": "page" }, { - "id": "594-tutorial#setup", + "id": "613-tutorial#setup", "title": "Tutorial", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -8038,7 +8300,7 @@ "kind": "section" }, { - "id": "595-tutorial#create-a-project", + "id": "614-tutorial#create-a-project", "title": "Tutorial", "searchTitle": "Create a Project", "sectionTitle": "Create a Project", @@ -8048,7 +8310,7 @@ "kind": "section" }, { - "id": "596-tutorial#set-up-your-database", + "id": "615-tutorial#set-up-your-database", "title": "Tutorial", "searchTitle": "Set Up Your Database", "sectionTitle": "Set Up Your Database", @@ -8058,7 +8320,7 @@ "kind": "section" }, { - "id": "597-tutorial#install-and-run-zero-cache", + "id": "616-tutorial#install-and-run-zero-cache", "title": "Tutorial", "searchTitle": "Install and Run Zero-Cache", "sectionTitle": "Install and Run Zero-Cache", @@ -8068,7 +8330,7 @@ "kind": "section" }, { - "id": "598-tutorial#integrate-zero", + "id": "617-tutorial#integrate-zero", "title": "Tutorial", "searchTitle": "Integrate Zero", "sectionTitle": "Integrate Zero", @@ -8078,7 +8340,7 @@ "kind": "section" }, { - "id": "599-tutorial#set-up-your-zero-schema", + "id": "618-tutorial#set-up-your-zero-schema", "title": "Tutorial", "searchTitle": "Set Up Your Zero Schema", "sectionTitle": "Set Up Your Zero Schema", @@ -8088,7 +8350,7 @@ "kind": "section" }, { - "id": "600-tutorial#set-up-the-zero-client", + "id": "619-tutorial#set-up-the-zero-client", "title": "Tutorial", "searchTitle": "Set Up the Zero Client", "sectionTitle": "Set Up the Zero Client", @@ -8098,7 +8360,7 @@ "kind": "section" }, { - "id": "601-tutorial#sync-data", + "id": "620-tutorial#sync-data", "title": "Tutorial", "searchTitle": "Sync Data", "sectionTitle": "Sync Data", @@ -8108,7 +8370,7 @@ "kind": "section" }, { - "id": "602-tutorial#define-query", + "id": "621-tutorial#define-query", "title": "Tutorial", "searchTitle": "Define Query", "sectionTitle": "Define Query", @@ -8118,7 +8380,7 @@ "kind": "section" }, { - "id": "603-tutorial#add-query-endpoint", + "id": "622-tutorial#add-query-endpoint", "title": "Tutorial", "searchTitle": "Add Query Endpoint", "sectionTitle": "Add Query Endpoint", @@ -8128,7 +8390,7 @@ "kind": "section" }, { - "id": "604-tutorial#invoke-query", + "id": "623-tutorial#invoke-query", "title": "Tutorial", "searchTitle": "Invoke Query", "sectionTitle": "Invoke Query", @@ -8138,7 +8400,7 @@ "kind": "section" }, { - "id": "605-tutorial#mutate-data", + "id": "624-tutorial#mutate-data", "title": "Tutorial", "searchTitle": "Mutate Data", "sectionTitle": "Mutate Data", @@ -8148,7 +8410,7 @@ "kind": "section" }, { - "id": "606-tutorial#define-mutators", + "id": "625-tutorial#define-mutators", "title": "Tutorial", "searchTitle": "Define Mutators", "sectionTitle": "Define Mutators", @@ -8158,7 +8420,7 @@ "kind": "section" }, { - "id": "607-tutorial#add-mutate-endpoint", + "id": "626-tutorial#add-mutate-endpoint", "title": "Tutorial", "searchTitle": "Add Mutate Endpoint", "sectionTitle": "Add Mutate Endpoint", @@ -8168,7 +8430,7 @@ "kind": "section" }, { - "id": "608-tutorial#invoke-mutators", + "id": "627-tutorial#invoke-mutators", "title": "Tutorial", "searchTitle": "Invoke Mutators", "sectionTitle": "Invoke Mutators", @@ -8178,7 +8440,7 @@ "kind": "section" }, { - "id": "609-tutorial#next-steps", + "id": "628-tutorial#next-steps", "title": "Tutorial", "searchTitle": "Next Steps", "sectionTitle": "Next Steps", @@ -8188,7 +8450,7 @@ "kind": "section" }, { - "id": "76-when-to-use", + "id": "77-when-to-use", "title": "When To Use Zero", "searchTitle": "When To Use Zero", "url": "/docs/when-to-use", @@ -8254,7 +8516,7 @@ "kind": "page" }, { - "id": "610-when-to-use#zero-might-be-a-good-fit", + "id": "629-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", @@ -8264,7 +8526,7 @@ "kind": "section" }, { - "id": "611-when-to-use#you-want-to-sync-only-a-small-subset-of-data-to-client", + "id": "630-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", @@ -8274,7 +8536,7 @@ "kind": "section" }, { - "id": "612-when-to-use#you-need-fine-grained-read-or-write-permissions", + "id": "631-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", @@ -8284,7 +8546,7 @@ "kind": "section" }, { - "id": "613-when-to-use#you-are-building-a-traditional-client-server-web-app", + "id": "632-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", @@ -8294,7 +8556,7 @@ "kind": "section" }, { - "id": "614-when-to-use#you-use-postgresql", + "id": "633-when-to-use#you-use-postgresql", "title": "When To Use Zero", "searchTitle": "You use PostgreSQL", "sectionTitle": "You use PostgreSQL", @@ -8304,7 +8566,7 @@ "kind": "section" }, { - "id": "615-when-to-use#your-app-is-broadly-like-linear", + "id": "634-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\"", @@ -8314,7 +8576,7 @@ "kind": "section" }, { - "id": "616-when-to-use#interaction-performance-is-very-important-to-you", + "id": "635-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", @@ -8324,7 +8586,7 @@ "kind": "section" }, { - "id": "617-when-to-use#zero-might-not-be-a-good-fit", + "id": "636-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", @@ -8334,7 +8596,7 @@ "kind": "section" }, { - "id": "618-when-to-use#you-need-the-privacy-or-data-ownership-benefits-of-local-first", + "id": "637-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", @@ -8344,7 +8606,7 @@ "kind": "section" }, { - "id": "619-when-to-use#you-need-to-support-offline-writes-or-long-periods-offline", + "id": "638-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", @@ -8354,7 +8616,7 @@ "kind": "section" }, { - "id": "620-when-to-use#you-are-building-a-native-mobile-app", + "id": "639-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", @@ -8364,7 +8626,7 @@ "kind": "section" }, { - "id": "621-when-to-use#the-total-backend-dataset-is--100gb", + "id": "640-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", @@ -8374,7 +8636,7 @@ "kind": "section" }, { - "id": "622-when-to-use#zero-might-not-be-a-good-fit-yet", + "id": "641-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", @@ -8384,7 +8646,7 @@ "kind": "section" }, { - "id": "623-when-to-use#alternatives", + "id": "642-when-to-use#alternatives", "title": "When To Use Zero", "searchTitle": "Alternatives", "sectionTitle": "Alternatives", @@ -8394,11 +8656,11 @@ "kind": "section" }, { - "id": "77-zero-cache-config", + "id": "78-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. The admin password also grants access to everything that the operator password does. flag: --admin-password env: ZERO_ADMIN_PASSWORD required: in production (when NODE_ENV=production) Optional Flags Allow Legacy Queries Allows clients to send query ASTs directly to zero-cache. Ad-hoc queries (also called legacy queries) work this way. When this is false, zero-cache rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use queries. flag: --allow-legacy-queries env: ZERO_ALLOW_LEGACY_QUERIES default: false App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Require Server User ID Refuse a connection that claims a userID unless your /query or /mutate endpoint confirmed it. Your endpoints confirm the user by returning the validated userID in their response, or null for logged-out callers. handleQueryRequest and handleMutateRequest do this when you pass userID to their object form. A userID verified by ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL is accepted either way. With this off, zero-cache trusts the userID the client claims when your endpoint doesn't return one, and logs a warning. Turn it on once your endpoints return the userID. flag: --auth-require-server-user-id env: ZERO_AUTH_REQUIRE_SERVER_USER_ID default: false Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: 300 Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: 300 Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 4 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Secret A shared secret that authenticates view-syncers to the change-streamer. View-syncers present it when connecting to the replication-manager, which refuses replication connections that do not present it. Set it to the same value on the replication-manager and on every view-syncer. In a single-node configuration zero-cache generates a secret for its own workers, so no value is needed. In a multi-node configuration it should always be set: while it is unset, the replication-manager accepts replication connections from anything that can reach its change-streamer port, and logs a warning on startup. To add the secret to an existing multi-node deployment, set it on the view-syncers before setting it on the replication-manager. A replication-manager without a secret accepts connections both with and without one, so the rollout needs no downtime. flag: --change-streamer-secret env: ZERO_CHANGE_STREAMER_SECRET Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 168 (one week) CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Heapz Serve heap snapshots of the zero-cache process at /heapz, protected by the admin password. A heap snapshot contains everything the process holds in memory, including the auth tokens, cookies and API keys of connected clients. If you don't use heap snapshots, set this to false. That turns off /heapz and keeps the other diagnostic endpoints and the inspector. flag: --enable-heapz env: ZERO_ENABLE_HEAPZ default: true Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. With Litestream v5 backups (the default), configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 or ZERO_LITESTREAM_BACKUP_USING_V5 is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build 0.5.18-zero.16. Restores fork the source backup with litestream restore -fork-to-url, so a custom executable must support that flag. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and ZERO_LITESTREAM_BACKUP_USING_V5 to false. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. Set this to false to write legacy WAL backups with Litestream v0.3.x instead. This requires ZERO_LITESTREAM_RESTORE_USING_V5, which is enabled by default. Zero requests an incremental backup every ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS, and acknowledges upstream changes only after a backup containing them has completed. Each replication-manager backs up to its own subfolder of the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: true Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. Leave this unset to use 48, lowered as needed so that the parts fit in a quarter of the memory available to the container. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Operator Password A password for the diagnostic endpoints that can't expose application data, so the people who run zero-cache can debug it without being able to read the data it serves. It grants access to: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Initial Readiness Threshold (ms) The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. When lag reporting is disabled, readiness does not wait for replication lag. flag: --replication-lag-initial-readiness-threshold-ms env: ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS default: 1000 Replication Lag Max Readiness Delay (ms) The maximum time that a server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. flag: --replication-lag-max-readiness-delay-ms env: ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS default: 600000 (10 minutes) Replication Lag Report Interval (ms) The approximate interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. Lag reports are also used at startup: a new server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready. This interval sets how long the server waits for the next report. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 10000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS View Syncer Hydration Budget (ms) A soft time limit for hydrating inactive (cached) queries when a view-syncer hydrates a client group's queries, for example after a client reconnects. Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your query endpoint does not count toward the budget. A value of 0 disables the budget. flag: --view-syncer-hydration-budget-ms env: ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS default: 0 View Syncer Query Hydration Timeout (ms) The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see Handling Errors). For the next five minutes, the query is rejected without being run again. After that, it can be retried. A value of 0 disables the limit. flag: --view-syncer-query-hydration-timeout-ms env: ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS default: 0 Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10 Deprecated Flags Change Max Connections ZERO_CHANGE_MAX_CONNS is deprecated and no longer controls change database connection usage. Size the database or pooler as described under Change DB. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS Change Streamer Mode ZERO_CHANGE_STREAMER_MODE is deprecated. Set ZERO_CHANGE_STREAMER_URI on view-syncers instead. The mode for running or connecting to the change-streamer: dedicated: runs the change-streamer and shuts down when another change-streamer takes over the replication slot. This is appropriate in a single-node configuration, or for the replication-manager in a multi-node configuration. discover: connects to the change-streamer as internally advertised in the change-db. This is appropriate for the view-syncers in a multi-node setup. This may not work in all networking configurations (e.g., some private networking or port forwarding setups). This option is ignored if ZERO_CHANGE_STREAMER_URI is set. flag: --change-streamer-mode env: ZERO_CHANGE_STREAMER_MODE default: dedicated Litestream Minimum Checkpoint Page Count ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT is deprecated. Use ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB instead. The WAL page count at which SQLite attempts a PASSIVE checkpoint. If unset, it is derived from the checkpoint threshold as checkpointThresholdMB * 256 (4 KiB pages). flag: --litestream-min-checkpoint-page-count env: ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT default: checkpointThresholdMB * 256 Auth JWK A public key in JWK format used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwk env: ZERO_AUTH_JWK Auth JWKS URL A URL that returns a JWK set used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwks-url env: ZERO_AUTH_JWKS_URL Auth Secret A symmetric key used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-secret env: ZERO_AUTH_SECRET", + "content": "zero-cache is configured either via CLI flag or environment variable. There is no separate zero.config file. You can also see all available flags by running zero-cache --help. Required Flags Upstream DB The \"upstream\" authoritative postgres database. In the future we will support other types of upstream besides PG. flag: --upstream-db env: ZERO_UPSTREAM_DB required: true Admin Password A password used to administer zero-cache server, for example to access the /statz endpoint and the inspector. This is required in production (when NODE_ENV=production) because we want all Zero servers to be debuggable using admin tools by default, without needing a restart. But we also don't want to expose sensitive data using them. The admin password also grants access to everything that the operator password does. flag: --admin-password env: ZERO_ADMIN_PASSWORD required: in production (when NODE_ENV=production) Optional Flags Affinity Cookie Name The name of your load balancer's session affinity (sticky session) cookie. When a draining zero-cache turns away a new connection, it expires this cookie in the WebSocket upgrade response, so the client's next attempt can be routed to a different instance. Without it, a draining zero-cache still tells the client to reconnect, but leaves its cookies alone, and a load balancer with sticky sessions can keep sending the client back to the instance that is shutting down. The name, path and domain must match the cookie your load balancer sets. The cookie is expired with Secure and SameSite=None, so that it also works for clients on another site over HTTPS. flag: --affinity-cookie-name env: ZERO_AFFINITY_COOKIE_NAME Affinity Cookie Path The path of the affinity cookie to expire while draining. flag: --affinity-cookie-path env: ZERO_AFFINITY_COOKIE_PATH default: / Affinity Cookie Domain The domain of the affinity cookie to expire while draining. Leave it unset for a host-only cookie. flag: --affinity-cookie-domain env: ZERO_AFFINITY_COOKIE_DOMAIN Allow Legacy Queries Allows clients to send query ASTs directly to zero-cache. Ad-hoc queries (also called legacy queries) work this way. When this is false, zero-cache rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use queries. flag: --allow-legacy-queries env: ZERO_ALLOW_LEGACY_QUERIES default: false App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Require Server User ID Refuse a connection that claims a userID unless your /query or /mutate endpoint confirmed it. Your endpoints confirm the user by returning the validated userID in their response, or null for logged-out callers. handleQueryRequest and handleMutateRequest do this when you pass userID to their object form. A userID verified by ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL is accepted either way. With this off, zero-cache trusts the userID the client claims when your endpoint doesn't return one, and logs a warning. Turn it on once your endpoints return the userID. flag: --auth-require-server-user-id env: ZERO_AUTH_REQUIRE_SERVER_USER_ID default: false Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: 300 Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: 300 Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 4 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Secret A shared secret that authenticates view-syncers to the change-streamer. View-syncers present it when connecting to the replication-manager, which refuses replication connections that do not present it. Set it to the same value on the replication-manager and on every view-syncer. In a single-node configuration zero-cache generates a secret for its own workers, so no value is needed. In a multi-node configuration it should always be set: while it is unset, the replication-manager accepts replication connections from anything that can reach its change-streamer port, and logs a warning on startup. To add the secret to an existing multi-node deployment, set it on the view-syncers before setting it on the replication-manager. A replication-manager without a secret accepts connections both with and without one, so the rollout needs no downtime. flag: --change-streamer-secret env: ZERO_CHANGE_STREAMER_SECRET Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 168 (one week) CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Heapz Serve heap snapshots of the zero-cache process at /heapz, protected by the admin password. A heap snapshot contains everything the process holds in memory, including the auth tokens, cookies and API keys of connected clients. If you don't use heap snapshots, set this to false. That turns off /heapz and keeps the other diagnostic endpoints and the inspector. flag: --enable-heapz env: ZERO_ENABLE_HEAPZ default: true Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. With Litestream v5 backups (the default), configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 or ZERO_LITESTREAM_BACKUP_USING_V5 is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build 0.5.18-zero.16. Restores fork the source backup with litestream restore -fork-to-url, so a custom executable must support that flag. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and ZERO_LITESTREAM_BACKUP_USING_V5 to false. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. Set this to false to write legacy WAL backups with Litestream v0.3.x instead. This requires ZERO_LITESTREAM_RESTORE_USING_V5, which is enabled by default. Zero requests an incremental backup every ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS, and acknowledges upstream changes only after a backup containing them has completed. Each replication-manager backs up to its own subfolder of the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: true Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. Leave this unset to use 48, lowered as needed so that the parts fit in a quarter of the memory available to the container. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Operator Password A password for the diagnostic endpoints that can't expose application data, so the people who run zero-cache can debug it without being able to read the data it serves. It grants access to: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Pre-Drain Delay (ms) The time between receiving a SIGTERM (or SIGINT) and starting to drain. During this time zero-cache fails its readiness check at /, so load balancers stop routing to it, while it keeps accepting and serving connections as usual. The liveness check at /keepalive keeps succeeding. This takes the place of a pre-stop delay. Set it longer than the time your load balancer needs to notice the failed readiness check, and keep it within your container runner's termination grace period. With the default of 0, draining starts immediately. flag: --pre-drain-delay-ms env: ZERO_PRE_DRAIN_DELAY_MS default: 0 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Initial Readiness Threshold (ms) The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. When lag reporting is disabled, readiness does not wait for replication lag. flag: --replication-lag-initial-readiness-threshold-ms env: ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS default: 1000 Replication Lag Max Readiness Delay (ms) The maximum time that a server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. flag: --replication-lag-max-readiness-delay-ms env: ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS default: 600000 (10 minutes) Replication Lag Report Interval (ms) The approximate interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. Lag reports are also used at startup: a new server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready. This interval sets how long the server waits for the next report. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 10000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Replication Slot Per Replica Create a dedicated replication slot for each replication-manager, instead of having a new replication-manager take over the slot of the one it replaces. A new replication-manager restores its replica from the backup, creates its own slot, and catches up while the previous replication-manager keeps serving. It takes over once it has caught up. Expect one slot per running replication-manager in Postgres, each holding WAL until it is consumed. This requires Litestream v5 backups. The backup and the change DB stay compatible with a shared slot, so setting this to false does not cause a resync. This is the first step towards a high-availability mode in which several replication-managers run at the same time. Running more than one replication-manager is not supported yet. flag: --upstream-pg-replication-slot-per-replica env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA default: true Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS View Syncer Hydration Budget (ms) A soft time limit for hydrating inactive (cached) queries when a view-syncer hydrates a client group's queries, for example after a client reconnects. Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your query endpoint does not count toward the budget. A value of 0 disables the budget. flag: --view-syncer-hydration-budget-ms env: ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS default: 0 View Syncer Query Hydration Timeout (ms) The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see Handling Errors). For the next five minutes, the query is rejected without being run again. After that, it can be retried. A value of 0 disables the limit. flag: --view-syncer-query-hydration-timeout-ms env: ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS default: 0 Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10 Deprecated Flags Change Max Connections ZERO_CHANGE_MAX_CONNS is deprecated and no longer controls change database connection usage. Size the database or pooler as described under Change DB. flag: --change-max-conns env: ZERO_CHANGE_MAX_CONNS Change Streamer Mode ZERO_CHANGE_STREAMER_MODE is deprecated. Set ZERO_CHANGE_STREAMER_URI on view-syncers instead. The mode for running or connecting to the change-streamer: dedicated: runs the change-streamer and shuts down when another change-streamer takes over the replication slot. This is appropriate in a single-node configuration, or for the replication-manager in a multi-node configuration. discover: connects to the change-streamer as internally advertised in the change-db. This is appropriate for the view-syncers in a multi-node setup. This may not work in all networking configurations (e.g., some private networking or port forwarding setups). This option is ignored if ZERO_CHANGE_STREAMER_URI is set. flag: --change-streamer-mode env: ZERO_CHANGE_STREAMER_MODE default: dedicated Litestream Minimum Checkpoint Page Count ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT is deprecated. Use ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB instead. The WAL page count at which SQLite attempts a PASSIVE checkpoint. If unset, it is derived from the checkpoint threshold as checkpointThresholdMB * 256 (4 KiB pages). flag: --litestream-min-checkpoint-page-count env: ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT default: checkpointThresholdMB * 256 Auth JWK A public key in JWK format used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwk env: ZERO_AUTH_JWK Auth JWKS URL A URL that returns a JWK set used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-jwks-url env: ZERO_AUTH_JWKS_URL Auth Secret A symmetric key used to verify JWTs. Only one of jwk, jwksUrl and secret may be set. flag: --auth-secret env: ZERO_AUTH_SECRET", "headings": [ { "text": "Required Flags", @@ -8416,6 +8678,18 @@ "text": "Optional Flags", "id": "optional-flags" }, + { + "text": "Affinity Cookie Name", + "id": "affinity-cookie-name" + }, + { + "text": "Affinity Cookie Path", + "id": "affinity-cookie-path" + }, + { + "text": "Affinity Cookie Domain", + "id": "affinity-cookie-domain" + }, { "text": "Allow Legacy Queries", "id": "allow-legacy-queries" @@ -8660,6 +8934,10 @@ "text": "Port", "id": "port" }, + { + "text": "Pre-Drain Delay (ms)", + "id": "pre-drain-delay-ms" + }, { "text": "Query API Key", "id": "query-api-key" @@ -8740,6 +9018,10 @@ "text": "Upstream PG Replication Slot Failover", "id": "upstream-pg-replication-slot-failover" }, + { + "text": "Upstream PG Replication Slot Per Replica", + "id": "upstream-pg-replication-slot-per-replica" + }, { "text": "Upstream PG Stream Inbound Timeout", "id": "upstream-pg-stream-inbound-timeout" @@ -8800,7 +9082,7 @@ "kind": "page" }, { - "id": "624-zero-cache-config#required-flags", + "id": "643-zero-cache-config#required-flags", "title": "zero-cache Config", "searchTitle": "Required Flags", "sectionTitle": "Required Flags", @@ -8810,7 +9092,7 @@ "kind": "section" }, { - "id": "625-zero-cache-config#upstream-db", + "id": "644-zero-cache-config#upstream-db", "title": "zero-cache Config", "searchTitle": "Upstream DB", "sectionTitle": "Upstream DB", @@ -8820,7 +9102,7 @@ "kind": "section" }, { - "id": "626-zero-cache-config#admin-password", + "id": "645-zero-cache-config#admin-password", "title": "zero-cache Config", "searchTitle": "Admin Password", "sectionTitle": "Admin Password", @@ -8830,17 +9112,47 @@ "kind": "section" }, { - "id": "627-zero-cache-config#optional-flags", + "id": "646-zero-cache-config#optional-flags", "title": "zero-cache Config", "searchTitle": "Optional Flags", "sectionTitle": "Optional Flags", "sectionId": "optional-flags", "url": "/docs/zero-cache-config", - "content": "Allow Legacy Queries Allows clients to send query ASTs directly to zero-cache. Ad-hoc queries (also called legacy queries) work this way. When this is false, zero-cache rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use queries. flag: --allow-legacy-queries env: ZERO_ALLOW_LEGACY_QUERIES default: false App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Require Server User ID Refuse a connection that claims a userID unless your /query or /mutate endpoint confirmed it. Your endpoints confirm the user by returning the validated userID in their response, or null for logged-out callers. handleQueryRequest and handleMutateRequest do this when you pass userID to their object form. A userID verified by ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL is accepted either way. With this off, zero-cache trusts the userID the client claims when your endpoint doesn't return one, and logs a warning. Turn it on once your endpoints return the userID. flag: --auth-require-server-user-id env: ZERO_AUTH_REQUIRE_SERVER_USER_ID default: false Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: 300 Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: 300 Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 4 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Secret A shared secret that authenticates view-syncers to the change-streamer. View-syncers present it when connecting to the replication-manager, which refuses replication connections that do not present it. Set it to the same value on the replication-manager and on every view-syncer. In a single-node configuration zero-cache generates a secret for its own workers, so no value is needed. In a multi-node configuration it should always be set: while it is unset, the replication-manager accepts replication connections from anything that can reach its change-streamer port, and logs a warning on startup. To add the secret to an existing multi-node deployment, set it on the view-syncers before setting it on the replication-manager. A replication-manager without a secret accepts connections both with and without one, so the rollout needs no downtime. flag: --change-streamer-secret env: ZERO_CHANGE_STREAMER_SECRET Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 168 (one week) CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Heapz Serve heap snapshots of the zero-cache process at /heapz, protected by the admin password. A heap snapshot contains everything the process holds in memory, including the auth tokens, cookies and API keys of connected clients. If you don't use heap snapshots, set this to false. That turns off /heapz and keeps the other diagnostic endpoints and the inspector. flag: --enable-heapz env: ZERO_ENABLE_HEAPZ default: true Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. With Litestream v5 backups (the default), configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 or ZERO_LITESTREAM_BACKUP_USING_V5 is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build 0.5.18-zero.16. Restores fork the source backup with litestream restore -fork-to-url, so a custom executable must support that flag. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and ZERO_LITESTREAM_BACKUP_USING_V5 to false. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. Set this to false to write legacy WAL backups with Litestream v0.3.x instead. This requires ZERO_LITESTREAM_RESTORE_USING_V5, which is enabled by default. Zero requests an incremental backup every ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS, and acknowledges upstream changes only after a backup containing them has completed. Each replication-manager backs up to its own subfolder of the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: true Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. Leave this unset to use 48, lowered as needed so that the parts fit in a quarter of the memory available to the container. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Operator Password A password for the diagnostic endpoints that can't expose application data, so the people who run zero-cache can debug it without being able to read the data it serves. It grants access to: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Initial Readiness Threshold (ms) The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. When lag reporting is disabled, readiness does not wait for replication lag. flag: --replication-lag-initial-readiness-threshold-ms env: ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS default: 1000 Replication Lag Max Readiness Delay (ms) The maximum time that a server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. flag: --replication-lag-max-readiness-delay-ms env: ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS default: 600000 (10 minutes) Replication Lag Report Interval (ms) The approximate interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. Lag reports are also used at startup: a new server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready. This interval sets how long the server waits for the next report. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 10000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS View Syncer Hydration Budget (ms) A soft time limit for hydrating inactive (cached) queries when a view-syncer hydrates a client group's queries, for example after a client reconnects. Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your query endpoint does not count toward the budget. A value of 0 disables the budget. flag: --view-syncer-hydration-budget-ms env: ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS default: 0 View Syncer Query Hydration Timeout (ms) The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see Handling Errors). For the next five minutes, the query is rejected without being run again. After that, it can be retried. A value of 0 disables the limit. flag: --view-syncer-query-hydration-timeout-ms env: ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS default: 0 Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10", + "content": "Affinity Cookie Name The name of your load balancer's session affinity (sticky session) cookie. When a draining zero-cache turns away a new connection, it expires this cookie in the WebSocket upgrade response, so the client's next attempt can be routed to a different instance. Without it, a draining zero-cache still tells the client to reconnect, but leaves its cookies alone, and a load balancer with sticky sessions can keep sending the client back to the instance that is shutting down. The name, path and domain must match the cookie your load balancer sets. The cookie is expired with Secure and SameSite=None, so that it also works for clients on another site over HTTPS. flag: --affinity-cookie-name env: ZERO_AFFINITY_COOKIE_NAME Affinity Cookie Path The path of the affinity cookie to expire while draining. flag: --affinity-cookie-path env: ZERO_AFFINITY_COOKIE_PATH default: / Affinity Cookie Domain The domain of the affinity cookie to expire while draining. Leave it unset for a host-only cookie. flag: --affinity-cookie-domain env: ZERO_AFFINITY_COOKIE_DOMAIN Allow Legacy Queries Allows clients to send query ASTs directly to zero-cache. Ad-hoc queries (also called legacy queries) work this way. When this is false, zero-cache rejects client-sent ASTs without parsing them and closes the connection with an error. Keep it disabled when you use queries. flag: --allow-legacy-queries env: ZERO_ALLOW_LEGACY_QUERIES default: false App ID Unique identifier for the app. Multiple zero-cache apps can run on a single upstream database, each of which is isolated from the others, with its own permissions, sharding (future feature), and change/cvr databases. The metadata of an app is stored in an upstream schema with the same name, e.g. zero, and the metadata for each app shard, e.g. client and mutation ids, is stored in the {app-id}_{#} schema. (Currently there is only a single \"0\" shard, but this will change with sharding). The CVR and Change data are managed in schemas named {app-id}_{shard-num}/cvr and {app-id}_{shard-num}/cdc, respectively, allowing multiple apps and shards to share the same database instance (e.g. a Postgres \"cluster\") for CVR and Change management. Due to constraints on replication slot names, an App ID may only consist of lower-case letters, numbers, and the underscore character. Note that this option is used by both zero-cache and zero-deploy-permissions. flag: --app-id env: ZERO_APP_ID default: zero App Publications Postgres PUBLICATIONs that define the tables and columns to replicate. Publication names may not begin with an underscore, as zero reserves that prefix for internal use. If unspecified, zero-cache will create and use an internal publication that publishes all tables in the public schema, i.e.: CREATE PUBLICATION _{app-id}_public_0 FOR TABLES IN SCHEMA public; Note that changing the set of publications will result in resyncing the replica, which may involve downtime (replication lag) while the new replica is initializing. To change the set of publications without disrupting an existing app, a new app should be created. To use a custom publication, you can create one with: CREATE PUBLICATION zero_data FOR TABLES IN SCHEMA public; -- or, more selectively: CREATE PUBLICATION zero_data FOR TABLE users, orders; Then set the flag to that publication name, e.g.: ZERO_APP_PUBLICATIONS=zero_data. To specify multiple publications, separate them with commas, e.g.: ZERO_APP_PUBLICATIONS=zero_data1,zero_data2. flag: --app-publications env: ZERO_APP_PUBLICATIONS default: _{app-id}_public_0 Auth Require Server User ID Refuse a connection that claims a userID unless your /query or /mutate endpoint confirmed it. Your endpoints confirm the user by returning the validated userID in their response, or null for logged-out callers. handleQueryRequest and handleMutateRequest do this when you pass userID to their object form. A userID verified by ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL is accepted either way. With this off, zero-cache trusts the userID the client claims when your endpoint doesn't return one, and logs a warning. Turn it on once your endpoints return the userID. flag: --auth-require-server-user-id env: ZERO_AUTH_REQUIRE_SERVER_USER_ID default: false Auth Revalidate Interval Seconds How often zero-cache re-checks that each live connection is still authorized to use your /query endpoint. On each interval, zero-cache sends a lightweight validation request using that connection's current auth context, such as forwarded cookies or an opaque auth token. If your query endpoint rejects that auth with a 401/403, the connection is disconnected. Use this to bound how long already-open connections can continue after logout, session expiry, token revocation, or other server-side auth changes that happen without a reconnect. Lower values enforce auth changes faster, but send more validation requests to /query. flag: --auth-revalidate-interval-seconds env: ZERO_AUTH_REVALIDATE_INTERVAL_SECONDS default: 300 Auth Retransform Interval Seconds How often zero-cache refreshes a client group's synced or named query transformations using one validated connection from that group. This re-runs auth-sensitive query expansion even when the query set itself has not changed. It is useful when your query endpoint generates different ZQL based on current auth or server-side session state, such as roles, organization membership, feature flags, or other permissions-derived context. Use this to bound how long a client group can keep using stale auth-derived query shapes after backend auth state changes. Lower values pick up those changes faster, but do more /query transform work. If clients already call updateAuth whenever auth changes, this mainly serves as a background safety net for out-of-band auth changes. flag: --auth-retransform-interval-seconds env: ZERO_AUTH_RETRANSFORM_INTERVAL_SECONDS default: 300 Auto Reset Automatically wipe and resync the replica when replication is halted. This situation can occur for configurations in which the upstream database provider prohibits event trigger creation, preventing the zero-cache from being able to correctly replicate schema changes. For such configurations, an upstream schema change will instead result in halting replication with an error indicating that the replica needs to be reset. When auto-reset is enabled, zero-cache will respond to such situations by shutting down, and when restarted, resetting the replica and all synced clients. This is a heavy-weight operation and can result in user-visible slowness or downtime if compute resources are scarce. flag: --auto-reset env: ZERO_AUTO_RESET default: true Change DB The Postgres database used to store recent replication log entries, in order to sync multiple view-syncers without requiring multiple replication slots on the upstream database. If unspecified, the upstream-db will be used. flag: --change-db env: ZERO_CHANGE_DB Change Streamer Back Pressure Limit Heap Proportion The percentage of --max-old-space-size to use as a buffer for absorbing replication stream spikes. When the estimated amount of queued data exceeds this threshold, back pressure is applied to the replication stream, delaying downstream sync as a result. The threshold was determined empirically with load testing. Higher thresholds have resulted in OOMs. Note also that the byte-counting logic in the queue is strictly an underestimate of actual memory usage (but importantly, proportionally correct), so the queue is actually using more than what this proportion suggests. This parameter is exported as an emergency knob to reduce the size of the buffer in the event that the server OOMs from back pressure. Resist the urge to increase this proportion, as it is mainly useful for absorbing periodic spikes and does not meaningfully affect steady-state replication throughput; the latter is determined by other factors such as object serialization and PG throughput. In other words, the back pressure limit does not constrain replication throughput; rather, it protects the system when the upstream throughput exceeds the downstream throughput. flag: --change-streamer-back-pressure-limit-heap-proportion env: ZERO_CHANGE_STREAMER_BACK_PRESSURE_LIMIT_HEAP_PROPORTION default: 0.04 Change Streamer Flow Control Consensus Timeout Proportion During periodic flow-control checks, this is the amount of additional time to wait after the majority of serving replicas have acknowledged a checkpoint, expressed as a proportion of the time the majority took. Replication continues after that timeout even if some serving replicas have not acknowledged the checkpoint. A backup replica that is still catching up does not delay caught-up serving replicas; once caught up, the backup must acknowledge the checkpoint before replication continues. For example, if the majority responds in 2.5 ms, a proportion of 2 waits up to another 5 ms. The default lets a subscriber take roughly five times as long as the majority before early release. This bounds the effect of a lagging or unresponsive subscriber while allowing for normal processing-time variance. This value is a ratio, not a duration in seconds. Set it to a negative number to disable early releases and wait for all subscribers. flag: --change-streamer-flow-control-consensus-timeout-proportion env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_CONSENSUS_TIMEOUT_PROPORTION default: 4 Change Streamer Flow Control Slow Subscriber Grace Period Seconds The time a serving subscriber may continuously miss the consensus timeout while processing changes more slowly than healthy subscribers before Zero disconnects it and instructs it to restart. This protects healthy subscribers from zombie or chronically slow tasks while allowing temporary catch-up work to complete. Only serving replicas are reset; the replication-manager's backup replica is not disconnected by this setting. Set this to 0 or a negative number to disable slow-subscriber resets. flag: --change-streamer-flow-control-slow-subscriber-grace-period-seconds env: ZERO_CHANGE_STREAMER_FLOW_CONTROL_SLOW_SUBSCRIBER_GRACE_PERIOD_SECONDS default: 30 Change Streamer Port The port on which the change-streamer runs. This is an internal protocol between the replication-manager and view-syncers, which runs in the same process tree in local development or a single-node configuration. If unspecified, defaults to --port + 1. flag: --change-streamer-port env: ZERO_CHANGE_STREAMER_PORT default: --port + 1 Change Streamer Secret A shared secret that authenticates view-syncers to the change-streamer. View-syncers present it when connecting to the replication-manager, which refuses replication connections that do not present it. Set it to the same value on the replication-manager and on every view-syncer. In a single-node configuration zero-cache generates a secret for its own workers, so no value is needed. In a multi-node configuration it should always be set: while it is unset, the replication-manager accepts replication connections from anything that can reach its change-streamer port, and logs a warning on startup. To add the secret to an existing multi-node deployment, set it on the view-syncers before setting it on the replication-manager. A replication-manager without a secret accepts connections both with and without one, so the rollout needs no downtime. flag: --change-streamer-secret env: ZERO_CHANGE_STREAMER_SECRET Change Streamer Startup Delay (ms) The delay to wait before the change-streamer takes over the replication stream (i.e. the handoff during replication-manager updates), to allow load balancers to register the task as healthy based on healthcheck parameters. If a change stream request is received during this interval, the delay will be canceled and the takeover will happen immediately, since the incoming request indicates that the task is registered as a target. flag: --change-streamer-startup-delay-ms env: ZERO_CHANGE_STREAMER_STARTUP_DELAY_MS default: 15000 Change Streamer URI When set, connects to the change-streamer at the given URI. In a multi-node setup, this should be specified in view-syncer options, pointing to the replication-manager URI, which runs a change-streamer on port 4849. flag: --change-streamer-uri env: ZERO_CHANGE_STREAMER_URI CVR DB The Postgres database used to store CVRs. CVRs (client view records) keep track of the data synced to clients in order to determine the diff to send on reconnect. If unspecified, the upstream-db will be used. flag: --cvr-db env: ZERO_CVR_DB CVR Garbage Collection Inactivity Threshold Hours The duration after which an inactive CVR is eligible for garbage collection. Garbage collection is incremental and periodic, so eligible CVRs are not necessarily purged immediately. Purging a CVR forces the next connection from that client group to sync from scratch. Set this well above how long a typical user goes between sessions. flag: --cvr-garbage-collection-inactivity-threshold-hours env: ZERO_CVR_GARBAGE_COLLECTION_INACTIVITY_THRESHOLD_HOURS default: 168 (one week) CVR Garbage Collection Initial Batch Size The initial number of CVRs to purge per garbage collection interval. This number is increased linearly if the rate of new CVRs exceeds the rate of purged CVRs, in order to reach a steady state. Setting this to 0 effectively disables CVR garbage collection. flag: --cvr-garbage-collection-initial-batch-size env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_BATCH_SIZE default: 25 CVR Garbage Collection Initial Interval Seconds The initial interval at which to check and garbage collect inactive CVRs. This interval is increased exponentially (up to 16 minutes) when there is nothing to purge. flag: --cvr-garbage-collection-initial-interval-seconds env: ZERO_CVR_GARBAGE_COLLECTION_INITIAL_INTERVAL_SECONDS default: 60 CVR Max Connections The maximum number of connections to open to the CVR database. This is divided evenly amongst sync workers. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --cvr-max-conns env: ZERO_CVR_MAX_CONNS default: 30 Enable Heapz Serve heap snapshots of the zero-cache process at /heapz, protected by the admin password. A heap snapshot contains everything the process holds in memory, including the auth tokens, cookies and API keys of connected clients. If you don't use heap snapshots, set this to false. That turns off /heapz and keeps the other diagnostic endpoints and the inspector. flag: --enable-heapz env: ZERO_ENABLE_HEAPZ default: true Enable Query Planner Enable the query planner for optimizing ZQL queries. The query planner analyzes and optimizes query execution by determining the most efficient join strategies. You can disable the planner if it is picking bad strategies. flag: --enable-query-planner env: ZERO_ENABLE_QUERY_PLANNER default: true Enable CRUD Mutations Enables support for legacy CRUD mutations. When this is false, view-syncers do not connect to the upstream database for CRUD writes, and push messages with CRUD mutations return an error response. flag: --enable-crud-mutations env: ZERO_ENABLE_CRUD_MUTATIONS default: true Enable Telemetry Zero collects anonymous telemetry data to help us understand usage. We collect: Zero version Uptime General machine information, like the number of CPUs, OS, CI/CD environment, etc. Information about usage, such as number of queries or mutations processed per hour. This is completely optional and can be disabled at any time. You can also opt-out by setting DO_NOT_TRACK=1. flag: --enable-telemetry env: ZERO_ENABLE_TELEMETRY default: true Initial Sync Table Copy Workers The number of parallel workers used to copy tables during initial sync. Each worker uses a database connection, copies a single table at a time, and buffers up to (approximately) 10 MB of table data in memory during initial sync. Increasing the number of workers may improve initial sync speed; however, local disk throughput (IOPS), upstream CPU, and network bandwidth may also be bottlenecks. flag: --initial-sync-table-copy-workers env: ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS default: 5 Lazy Startup Delay starting the majority of zero-cache until first request. This is mainly intended to avoid connecting to Postgres replication stream until the first request is received, which can be useful i.e., for preview instances. Currently only supported in single-node mode. flag: --lazy-startup env: ZERO_LAZY_STARTUP default: false Litestream Backup URL The location of the litestream backup, usually an s3:// URL. This is only consulted by the replication-manager. view-syncers receive this information from the replication-manager. In multi-node deployments, this is required on the replication-manager so view-syncers can reserve snapshots; in single-node deployments it is optional. With Litestream v5 backups (the default), configure this as a folder URL ending in /, for example s3://acme-zero-backups/v1/. Zero resolves each replica-specific backup path relative to this URL; without the trailing slash, URL resolution replaces the final path segment instead of nesting the replica backup under it. flag: --litestream-backup-url env: ZERO_LITESTREAM_BACKUP_URL Litestream Endpoint The S3-compatible endpoint URL to use for the litestream backup. This is only required for non-AWS services. The replication-manager and view-syncers must have the same endpoint. For example, to use Cloudflare R2: https://.r2.cloudflarestorage.com. flag: --litestream-endpoint env: ZERO_LITESTREAM_ENDPOINT Litestream Checkpoint Threshold MB The size of the WAL file at which Litestream performs background (PASSIVE) SQLite checkpoints to apply the writes in the WAL to the main database file. Each checkpoint creates new files (WAL segments with Litestream v3, LTX files with v5) that are then backed up. PASSIVE checkpoints are skipped while a write is in progress, so high write rates can make the WAL grow. flag: --litestream-checkpoint-threshold-mb env: ZERO_LITESTREAM_CHECKPOINT_THRESHOLD_MB default: 40 Litestream Config Path Path to the litestream yaml config file. zero-cache will run this with its environment variables, which can be referenced in the file via ${ENV} substitution, for example: ZERO_REPLICA_FILE for the db Path ZERO_LITESTREAM_BACKUP_LOCATION for the db replica url ZERO_LITESTREAM_LOG_LEVEL for the log Level ZERO_LOG_FORMAT for the log type flag: --litestream-config-path env: ZERO_LITESTREAM_CONFIG_PATH default: ./src/services/litestream/config.yml Litestream Config Path V5 Path to the Litestream v5 YAML config file. zero-cache runs this file with the same environment-variable substitution described for ZERO_LITESTREAM_CONFIG_PATH. flag: --litestream-config-path-v5 env: ZERO_LITESTREAM_CONFIG_PATH_V5 default: ./src/services/litestream/config-v5.yml Litestream Executable Path to the litestream executable. This must be built from the rocicorp/litestream fork. This option has no effect if litestream-backup-url is unspecified. flag: --litestream-executable env: ZERO_LITESTREAM_EXECUTABLE Litestream V5 Executable Path to the official Litestream v0.5.x executable used for v5 restores and backups. Litestream v0.5.8 and later can restore both legacy WAL backups and LTX backups, choosing the format with the latest data. When a backup URL is configured and ZERO_LITESTREAM_RESTORE_USING_V5 or ZERO_LITESTREAM_BACKUP_USING_V5 is enabled (both are by default), this executable is required. The official Zero Docker image includes Rocicorp's Litestream build 0.5.18-zero.16. Restores fork the source backup with litestream restore -fork-to-url, so a custom executable must support that flag. flag: --litestream-executable-v5 env: ZERO_LITESTREAM_EXECUTABLE_V5 Litestream Restore Using V5 Use ZERO_LITESTREAM_EXECUTABLE_V5 for restores. Set this to false to force legacy restore behavior. Because v5 backups require v5 restore, running without a v5 executable when a backup URL is configured requires setting both this option and ZERO_LITESTREAM_BACKUP_USING_V5 to false. Litestream v0.5 cannot restore legacy backups encrypted with Age. Keep legacy restore enabled for those backups or migrate them before enabling v5 restore. flag: --litestream-restore-using-v5 env: ZERO_LITESTREAM_RESTORE_USING_V5 default: true Litestream Backup Using V5 Write LTX backups with Litestream v0.5.x. Set this to false to write legacy WAL backups with Litestream v0.3.x instead. This requires ZERO_LITESTREAM_RESTORE_USING_V5, which is enabled by default. Zero requests an incremental backup every ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS, and acknowledges upstream changes only after a backup containing them has completed. Each replication-manager backs up to its own subfolder of the folder URL configured by ZERO_LITESTREAM_BACKUP_URL. Zero does not delete subfolders left by previous replication-managers. The simplest way to remove them is an object-store lifecycle expiration rule. Every file a restore needs is at most about ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS old, so an expiration of at least twice that interval is safe. Rolling back to a server that does not understand replica-specific paths can require a full resync. flag: --litestream-backup-using-v5 env: ZERO_LITESTREAM_BACKUP_USING_V5 default: true Litestream Incremental Backup Interval Minutes The interval between incremental Litestream v3 backups of the replica. Shorter intervals reduce the amount of change history that needs to be replayed when catching up a new view-syncer, at the expense of increasing the number of files needed to download for the initial restore. For v5 backups, use ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS. flag: --litestream-incremental-backup-interval-minutes env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES default: 15 Litestream Incremental Backup Interval Seconds The interval between incremental Litestream v5 backups. Zero does not acknowledge upstream changes until the corresponding replica changes are confirmed in backup storage. Shorter intervals increase storage requests; longer intervals retain more upstream WAL while confirmation is pending. flag: --litestream-incremental-backup-interval-seconds env: ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS default: 15 Litestream Maximum Checkpoint Page Count The WAL page count at which SQLite performs a RESTART checkpoint, which blocks writers until complete. Defaults to minCheckpointPageCount * 10. Set to 0 to disable RESTART checkpoints entirely. This setting is ignored when backing up with Litestream v5. flag: --litestream-max-checkpoint-page-count env: ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT default: minCheckpointPageCount * 10 Litestream Multipart Concurrency The number of parts (of size --litestream-multipart-size bytes) to upload or download in parallel when backing up or restoring the snapshot. Leave this unset to use 48, lowered as needed so that the parts fit in a quarter of the memory available to the container. flag: --litestream-multipart-concurrency env: ZERO_LITESTREAM_MULTIPART_CONCURRENCY Litestream Multipart Size The size of each part when uploading or downloading the snapshot with --litestream-multipart-concurrency. Note that up to concurrency * size bytes of memory are used when backing up or restoring the snapshot. flag: --litestream-multipart-size env: ZERO_LITESTREAM_MULTIPART_SIZE default: 16777216 (16 MiB) Litestream Log Level flag: --litestream-log-level env: ZERO_LITESTREAM_LOG_LEVEL default: warn values: debug, info, warn, error Litestream Port Port on which litestream exports metrics, used to determine the replication watermark up to which it is safe to purge change log records. flag: --litestream-port env: ZERO_LITESTREAM_PORT default: --port + 2 Litestream Region The AWS region for the litestream backup bucket. Required for non-standard AWS partitions (e.g. GovCloud us-gov-west-1) where Litestream cannot auto-detect the region. The replication-manager and view-syncers must have the same region. flag: --litestream-region env: ZERO_LITESTREAM_REGION Litestream Restore Parallelism The number of WAL files to download in parallel when performing the initial restore of the replica from the backup. flag: --litestream-restore-parallelism env: ZERO_LITESTREAM_RESTORE_PARALLELISM default: 48 Litestream Snapshot Backup Interval Hours The interval between snapshot backups of the replica. Snapshot backups make a full copy of the database to a new Litestream generation. Zero retains the previous generation for six additional hours so an active restore can finish before its snapshot and WAL files are removed. This improves restore time and safety at the expense of bandwidth and temporary backup storage. The setting applies to v3 and v5 backups, although v5 generally does not need frequent snapshots for restore speed. flag: --litestream-snapshot-backup-interval-hours env: ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS default: 12 Log Format Use text for developer-friendly console logging and json for consumption by structured-logging services. flag: --log-format env: ZERO_LOG_FORMAT default: \"text\" values: text, json Log IVM Sampling How often to collect IVM metrics. 1 out of N requests will be sampled where N is this value. flag: --log-ivm-sampling env: ZERO_LOG_IVM_SAMPLING default: 5000 Log Level Sets the logging level for the application. flag: --log-level env: ZERO_LOG_LEVEL default: \"info\" values: debug, info, warn, error Log Slow Hydrate Threshold The number of milliseconds a query hydration must take to print a slow warning. flag: --log-slow-hydrate-threshold env: ZERO_LOG_SLOW_HYDRATE_THRESHOLD default: 100 Log Slow Row Threshold The number of ms a row must take to fetch from table-source before it is considered slow. flag: --log-slow-row-threshold env: ZERO_LOG_SLOW_ROW_THRESHOLD default: 2 Mutate API Key An optional secret used to authorize zero-cache to call the API server handling writes. This is sent from zero-cache to your mutate endpoint in an X-Api-Key header. flag: --mutate-api-key env: ZERO_MUTATE_API_KEY Mutate Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your mutate endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --mutate-allowed-client-headers env: ZERO_MUTATE_ALLOWED_CLIENT_HEADERS default: none Mutate Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your mutate endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike mutate allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --mutate-allowed-request-headers env: ZERO_MUTATE_ALLOWED_REQUEST_HEADERS default: none Mutate Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your mutate endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --mutate-forward-cookies env: ZERO_MUTATE_FORWARD_COOKIES default: false Mutate URL The URL of the API server to which zero-cache will push mutations. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/mutate\" Any subdomain using wildcard: \"https://*.example.com/mutate\" Multiple subdomain levels: \"https://*.*.example.com/mutate\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/mutate\" Matches https://api.example.com/v1/mutate, https://api.example.com/v2/mutate, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/mutate\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/mutate,https://api2.example.com/mutate Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --mutate-url env: ZERO_MUTATE_URL Number of Sync Workers The number of processes to use for view syncing. Leave this unset to use max(1, availableParallelism() - 1), reserving one core for the replicator. If set to 0, the server runs without sync workers, which is the configuration for running the replication-manager in multi-node deployments. flag: --num-sync-workers env: ZERO_NUM_SYNC_WORKERS Operator Password A password for the diagnostic endpoints that can't expose application data, so the people who run zero-cache can debug it without being able to read the data it serves. It grants access to: /profz and /profrmz: CPU profiles. Profiles served for the operator password have the source of every regular expression redacted, since those can contain LIKE patterns from client queries. /statz: counts and sizes. /plannerz: the replica's schema and planner statistics. The other protected endpoints, the inspector and /heapz, still require the admin password. For /profrmz in a multi-node deployment, the replication-manager must be configured with the same operator password. If the operator password is the same as the admin password, it grants admin access. flag: --operator-password env: ZERO_OPERATOR_PASSWORD Per User Mutation Limit Max The maximum mutations per user within the specified windowMs. flag: --per-user-mutation-limit-max env: ZERO_PER_USER_MUTATION_LIMIT_MAX Per User Mutation Limit Window (ms) The sliding window over which the perUserMutationLimitMax is enforced. flag: --per-user-mutation-limit-window-ms env: ZERO_PER_USER_MUTATION_LIMIT_WINDOW_MS default: 60000 PG Replication Slot Failover For upstream Postgres 17+, creates replication slots with the failover flag enabled so they can be synchronized to a standby and survive a failover. This requires additional Postgres-side configuration on your provider; see High Availability. Has no effect on Postgres versions before 17. flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Port The port for sync connections. flag: --port env: ZERO_PORT default: 4848 Pre-Drain Delay (ms) The time between receiving a SIGTERM (or SIGINT) and starting to drain. During this time zero-cache fails its readiness check at /, so load balancers stop routing to it, while it keeps accepting and serving connections as usual. The liveness check at /keepalive keeps succeeding. This takes the place of a pre-stop delay. Set it longer than the time your load balancer needs to notice the failed readiness check, and keep it within your container runner's termination grace period. With the default of 0, draining starts immediately. flag: --pre-drain-delay-ms env: ZERO_PRE_DRAIN_DELAY_MS default: 0 Query API Key An optional secret used to authorize zero-cache to call the API server handling queries. This is sent from zero-cache to your query endpoint in an X-Api-Key header. flag: --query-api-key env: ZERO_QUERY_API_KEY Query Allowed Client Headers Comma-separated allowlist of client-provided custom headers to forward to your query endpoint. Header names are matched case-insensitively. By default, no client-provided custom headers are forwarded. flag: --query-allowed-client-headers env: ZERO_QUERY_ALLOWED_CLIENT_HEADERS default: none Query Allowed Request Headers Comma-separated allowlist of HTTP headers from the request that opened the WebSocket to forward to your query endpoint. Use this for proxy- or load-balancer-injected headers such as x-forwarded-for or cf-ray. Unlike query allowed client headers, these values come from the request that established the connection. Header names are matched case-insensitively. Values are retained for the WebSocket's lifetime, so clients must reconnect to receive changes. The allowlist does not verify the header source - only allow headers that a trusted proxy overwrites or removes from untrusted requests. No request headers are forwarded by default. flag: --query-allowed-request-headers env: ZERO_QUERY_ALLOWED_REQUEST_HEADERS default: none Query Forward Cookies If true, zero-cache will forward cookies from the request to zero-cache to your query endpoint. This is useful for passing authentication cookies to the API server. If false, cookies are not forwarded. flag: --query-forward-cookies env: ZERO_QUERY_FORWARD_COOKIES default: false Query Hydration Stats Track and log the number of rows considered by query hydrations which take longer than log-slow-hydrate-threshold milliseconds. This is useful for debugging and performance tuning. flag: --query-hydration-stats env: ZERO_QUERY_HYDRATION_STATS Query URL The URL of the API server to which zero-cache will send synced queries. URLs are matched using URLPattern, a standard Web API. Pattern syntax (similar to Express routes): Exact URL match: \"https://api.example.com/query\" Any subdomain using wildcard: \"https://*.example.com/query\" Multiple subdomain levels: \"https://*.*.example.com/query\" Any path under a domain: \"https://api.example.com/*\" Named path parameters: \"https://api.example.com/:version/query\" Matches https://api.example.com/v1/query, https://api.example.com/v2/query, etc. Advanced patterns: Optional path segments: \"https://api.example.com/:path?\" Regex in segments (for specific patterns): \"https://api.example.com/:version(v\\\\d+)/query\" matches only v followed by digits. Multiple patterns can be specified, for example: https://api1.example.com/query,https://api2.example.com/query Query parameters and URL fragments (#) are ignored during matching. See URLPattern for full syntax. flag: --query-url env: ZERO_QUERY_URL Replica File File path to the SQLite replica that zero-cache maintains. This can be lost, but if it is, zero-cache will have to re-replicate next time it starts up. flag: --replica-file env: ZERO_REPLICA_FILE default: \"zero.db\" Replica Vacuum Interval Hours Performs a VACUUM at server startup if the specified number of hours has elapsed since the last VACUUM (or initial-sync). The VACUUM operation is heavyweight and requires double the size of the db in disk space. If unspecified, VACUUM operations are not performed. flag: --replica-vacuum-interval-hours env: ZERO_REPLICA_VACUUM_INTERVAL_HOURS Replication Lag Initial Readiness Threshold (ms) The maximum replication lag that a server can have before it reports that it is ready to serve clients. This keeps a server that is starting up from serving stale data. This only applies at startup. Once a server is ready, it keeps serving even if replication lag later goes above this threshold. When lag reporting is disabled, readiness does not wait for replication lag. flag: --replication-lag-initial-readiness-threshold-ms env: ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS default: 1000 Replication Lag Max Readiness Delay (ms) The maximum time that a server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready anyway. This keeps a server from waiting forever, for example when the upstream database is under heavy load or there is a network partition. flag: --replication-lag-max-readiness-delay-ms env: ZERO_REPLICATION_LAG_MAX_READINESS_DELAY_MS default: 600000 (10 minutes) Replication Lag Report Interval (ms) The approximate interval at which replication lag reports are written upstream and reported via the zero.replication.total_lag OpenTelemetry metric. If an expected report is not received before the next interval, Zero emits a new report and increments zero.replication.lag_report_retries. Lag reports are also used at startup: a new server waits for replication lag to fall below ZERO_REPLICATION_LAG_INITIAL_READINESS_THRESHOLD_MS before it reports ready. This interval sets how long the server waits for the next report. This feature requires write access to upstream Postgres (uses pg_logical_emit_message()). For PostgreSQL 17+, lag measurements accurately reflect committed write latency (single-digit milliseconds). For PostgreSQL 16 and earlier, measurements may appear 50-100ms longer due to flush behavior. A negative or 0 value disables lag reporting and the readiness wait. Even if otel is not enabled, info and warn-level logs are emitted for large lag values. flag: --replication-lag-report-interval-ms env: ZERO_REPLICATION_LAG_REPORT_INTERVAL_MS default: 10000 Server Version The version string outputted to logs when the server starts up. flag: --server-version env: ZERO_SERVER_VERSION Shadow Sync Enabled Periodically exercises the initial-sync code path against a sample of rows from every published table, writing to a throwaway SQLite database. This acts as a canary: if the real initial-sync path breaks because of schema drift, Postgres version quirks, or another full-resync issue, the shadow run fails before a customer actually needs a full reset. flag: --shadow-sync-enabled env: ZERO_SHADOW_SYNC_ENABLED default: false Shadow Sync Interval Hours The interval between shadow initial-sync runs, in hours. The first run fires within [2/3, 1) of this interval after startup, so the canary completes at least once per task lifetime while still jittering fleet restarts. flag: --shadow-sync-interval-hours env: ZERO_SHADOW_SYNC_INTERVAL_HOURS default: 12 Shadow Sync Sample Rate The Bernoulli sampling rate for each table, where 0 < rate <= 1. A value of 1 disables sampling and copies all rows, still subject to --shadow-sync-max-rows-per-table. flag: --shadow-sync-sample-rate env: ZERO_SHADOW_SYNC_SAMPLE_RATE default: 0.1 Shadow Sync Max Rows Per Table The hard upper bound on rows copied per table per shadow run. This guards against unexpectedly large tables consuming too much disk or upstream bandwidth. flag: --shadow-sync-max-rows-per-table env: ZERO_SHADOW_SYNC_MAX_ROWS_PER_TABLE default: 10000 Storage DB Temp Dir Temporary directory for IVM operator storage. Leave unset to use os.tmpdir(). flag: --storage-db-tmp-dir env: ZERO_STORAGE_DB_TMP_DIR Task ID Globally unique identifier for the zero-cache instance. Setting this to a platform specific task identifier can be useful for debugging. If unspecified, zero-cache will attempt to extract the TaskARN if run from within an AWS ECS container, and otherwise use a random string. flag: --task-id env: ZERO_TASK_ID Upstream Max Connections The maximum number of connections to open to the upstream database for committing mutations. This is divided evenly amongst sync workers. In addition to this number, zero-cache uses one connection for the replication stream. Note that this number must allow for at least one connection per sync worker, or zero-cache will fail to start. See num-sync-workers. flag: --upstream-max-conns env: ZERO_UPSTREAM_MAX_CONNS default: 20 Upstream PG Replication Slot Failover For upstream PostgreSQL 17 and later, create replication slots with the failover parameter set to true to enable slot synchronization and failover. Additional Postgres-level configuration is required when enabling this option. This option has no effect for PostgreSQL versions before 17. See the PostgreSQL docs for details: https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION flag: --upstream-pg-replication-slot-failover env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER default: false Upstream PG Replication Slot Per Replica Create a dedicated replication slot for each replication-manager, instead of having a new replication-manager take over the slot of the one it replaces. A new replication-manager restores its replica from the backup, creates its own slot, and catches up while the previous replication-manager keeps serving. It takes over once it has caught up. Expect one slot per running replication-manager in Postgres, each holding WAL until it is consumed. This requires Litestream v5 backups. The backup and the change DB stay compatible with a shared slot, so setting this to false does not cause a resync. This is the first step towards a high-availability mode in which several replication-managers run at the same time. Running more than one replication-manager is not supported yet. flag: --upstream-pg-replication-slot-per-replica env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA default: true Upstream PG Stream Inbound Timeout The time, in milliseconds, without an inbound message from the upstream WAL sender after which zero-cache tears down the replication stream to force a reconnect. By default, the threshold is twice the server's wal_sender_timeout. Increase this value when a healthy WAL sender can remain silent while decoding unpublished WAL or assembling a large transaction. This changes only Zero's inbound timeout; keepalive timing remains derived from wal_sender_timeout. The option has no effect when wal_sender_timeout is 0, which disables inbound liveness detection. See WAL Sender Timeout. flag: --upstream-pg-stream-inbound-timeout-ms env: ZERO_UPSTREAM_PG_STREAM_INBOUND_TIMEOUT_MS View Syncer Hydration Budget (ms) A soft time limit for hydrating inactive (cached) queries when a view-syncer hydrates a client group's queries, for example after a client reconnects. Active queries always finish. Inactive queries are hydrated after them, starting with the ones whose TTL expires last. Once the budget is spent, the inactive queries that were not reached are removed, along with the remaining TTL that would have kept them cached. The budget is only checked between queries, so a query that has started is not cut short by it. Time spent waiting on your query endpoint does not count toward the budget. A value of 0 disables the budget. flag: --view-syncer-hydration-budget-ms env: ZERO_VIEW_SYNCER_HYDRATION_BUDGET_MS default: 0 View Syncer Query Hydration Timeout (ms) The maximum processing time that a view-syncer spends hydrating a single query. Time spent yielding to other work is not counted. If hydrating a query takes longer than this, the hydration is aborted and the query is removed from the client's view. Clients that use the query receive an error for it (see Handling Errors). For the next five minutes, the query is rejected without being run again. After that, it can be retried. A value of 0 disables the limit. flag: --view-syncer-query-hydration-timeout-ms env: ZERO_VIEW_SYNCER_QUERY_HYDRATION_TIMEOUT_MS default: 0 Websocket Compression Enable WebSocket per-message deflate compression. Compression can reduce bandwidth usage for sync traffic but increases CPU usage on both client and server. Disabled by default. See: https://github.com/websockets/ws#websocket-compression flag: --websocket-compression env: ZERO_WEBSOCKET_COMPRESSION default: false Websocket Compression Options JSON string containing WebSocket compression options. Only used if websocket-compression is enabled. Example: {\"zlibDeflateOptions\":{\"level\":3},\"threshold\":1024}. See https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback for available options. flag: --websocket-compression-options env: ZERO_WEBSOCKET_COMPRESSION_OPTIONS Websocket Max Payload Bytes Maximum size of incoming WebSocket messages in bytes. Messages exceeding this limit are rejected before parsing. flag: --websocket-max-payload-bytes env: ZERO_WEBSOCKET_MAX_PAYLOAD_BYTES default: 10485760 (10 MiB) Yield Threshold (ms) The maximum amount of time in milliseconds that a sync worker will spend in IVM (processing query hydration and advancement) before yielding to the event loop. Lower values increase responsiveness and fairness at the cost of reduced throughput. flag: --yield-threshold-ms env: ZERO_YIELD_THRESHOLD_MS default: 10", + "kind": "section" + }, + { + "id": "647-zero-cache-config#affinity-cookie-name", + "title": "zero-cache Config", + "searchTitle": "Affinity Cookie Name", + "sectionTitle": "Affinity Cookie Name", + "sectionId": "affinity-cookie-name", + "url": "/docs/zero-cache-config", + "content": "The name of your load balancer's session affinity (sticky session) cookie. When a draining zero-cache turns away a new connection, it expires this cookie in the WebSocket upgrade response, so the client's next attempt can be routed to a different instance. Without it, a draining zero-cache still tells the client to reconnect, but leaves its cookies alone, and a load balancer with sticky sessions can keep sending the client back to the instance that is shutting down. The name, path and domain must match the cookie your load balancer sets. The cookie is expired with Secure and SameSite=None, so that it also works for clients on another site over HTTPS. flag: --affinity-cookie-name env: ZERO_AFFINITY_COOKIE_NAME", + "kind": "section" + }, + { + "id": "648-zero-cache-config#affinity-cookie-path", + "title": "zero-cache Config", + "searchTitle": "Affinity Cookie Path", + "sectionTitle": "Affinity Cookie Path", + "sectionId": "affinity-cookie-path", + "url": "/docs/zero-cache-config", + "content": "The path of the affinity cookie to expire while draining. flag: --affinity-cookie-path env: ZERO_AFFINITY_COOKIE_PATH default: /", + "kind": "section" + }, + { + "id": "649-zero-cache-config#affinity-cookie-domain", + "title": "zero-cache Config", + "searchTitle": "Affinity Cookie Domain", + "sectionTitle": "Affinity Cookie Domain", + "sectionId": "affinity-cookie-domain", + "url": "/docs/zero-cache-config", + "content": "The domain of the affinity cookie to expire while draining. Leave it unset for a host-only cookie. flag: --affinity-cookie-domain env: ZERO_AFFINITY_COOKIE_DOMAIN", "kind": "section" }, { - "id": "628-zero-cache-config#allow-legacy-queries", + "id": "650-zero-cache-config#allow-legacy-queries", "title": "zero-cache Config", "searchTitle": "Allow Legacy Queries", "sectionTitle": "Allow Legacy Queries", @@ -8850,7 +9162,7 @@ "kind": "section" }, { - "id": "629-zero-cache-config#app-id", + "id": "651-zero-cache-config#app-id", "title": "zero-cache Config", "searchTitle": "App ID", "sectionTitle": "App ID", @@ -8860,7 +9172,7 @@ "kind": "section" }, { - "id": "630-zero-cache-config#app-publications", + "id": "652-zero-cache-config#app-publications", "title": "zero-cache Config", "searchTitle": "App Publications", "sectionTitle": "App Publications", @@ -8870,7 +9182,7 @@ "kind": "section" }, { - "id": "631-zero-cache-config#auth-require-server-user-id", + "id": "653-zero-cache-config#auth-require-server-user-id", "title": "zero-cache Config", "searchTitle": "Auth Require Server User ID", "sectionTitle": "Auth Require Server User ID", @@ -8880,7 +9192,7 @@ "kind": "section" }, { - "id": "632-zero-cache-config#auth-revalidate-interval-seconds", + "id": "654-zero-cache-config#auth-revalidate-interval-seconds", "title": "zero-cache Config", "searchTitle": "Auth Revalidate Interval Seconds", "sectionTitle": "Auth Revalidate Interval Seconds", @@ -8890,7 +9202,7 @@ "kind": "section" }, { - "id": "633-zero-cache-config#auth-retransform-interval-seconds", + "id": "655-zero-cache-config#auth-retransform-interval-seconds", "title": "zero-cache Config", "searchTitle": "Auth Retransform Interval Seconds", "sectionTitle": "Auth Retransform Interval Seconds", @@ -8900,7 +9212,7 @@ "kind": "section" }, { - "id": "634-zero-cache-config#auto-reset", + "id": "656-zero-cache-config#auto-reset", "title": "zero-cache Config", "searchTitle": "Auto Reset", "sectionTitle": "Auto Reset", @@ -8910,7 +9222,7 @@ "kind": "section" }, { - "id": "635-zero-cache-config#change-db", + "id": "657-zero-cache-config#change-db", "title": "zero-cache Config", "searchTitle": "Change DB", "sectionTitle": "Change DB", @@ -8920,7 +9232,7 @@ "kind": "section" }, { - "id": "636-zero-cache-config#change-streamer-back-pressure-limit-heap-proportion", + "id": "658-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", @@ -8930,7 +9242,7 @@ "kind": "section" }, { - "id": "637-zero-cache-config#change-streamer-flow-control-consensus-timeout-proportion", + "id": "659-zero-cache-config#change-streamer-flow-control-consensus-timeout-proportion", "title": "zero-cache Config", "searchTitle": "Change Streamer Flow Control Consensus Timeout Proportion", "sectionTitle": "Change Streamer Flow Control Consensus Timeout Proportion", @@ -8940,7 +9252,7 @@ "kind": "section" }, { - "id": "638-zero-cache-config#change-streamer-flow-control-slow-subscriber-grace-period-seconds", + "id": "660-zero-cache-config#change-streamer-flow-control-slow-subscriber-grace-period-seconds", "title": "zero-cache Config", "searchTitle": "Change Streamer Flow Control Slow Subscriber Grace Period Seconds", "sectionTitle": "Change Streamer Flow Control Slow Subscriber Grace Period Seconds", @@ -8950,7 +9262,7 @@ "kind": "section" }, { - "id": "639-zero-cache-config#change-streamer-port", + "id": "661-zero-cache-config#change-streamer-port", "title": "zero-cache Config", "searchTitle": "Change Streamer Port", "sectionTitle": "Change Streamer Port", @@ -8960,7 +9272,7 @@ "kind": "section" }, { - "id": "640-zero-cache-config#change-streamer-secret", + "id": "662-zero-cache-config#change-streamer-secret", "title": "zero-cache Config", "searchTitle": "Change Streamer Secret", "sectionTitle": "Change Streamer Secret", @@ -8970,7 +9282,7 @@ "kind": "section" }, { - "id": "641-zero-cache-config#change-streamer-startup-delay-ms", + "id": "663-zero-cache-config#change-streamer-startup-delay-ms", "title": "zero-cache Config", "searchTitle": "Change Streamer Startup Delay (ms)", "sectionTitle": "Change Streamer Startup Delay (ms)", @@ -8980,7 +9292,7 @@ "kind": "section" }, { - "id": "642-zero-cache-config#change-streamer-uri", + "id": "664-zero-cache-config#change-streamer-uri", "title": "zero-cache Config", "searchTitle": "Change Streamer URI", "sectionTitle": "Change Streamer URI", @@ -8990,7 +9302,7 @@ "kind": "section" }, { - "id": "643-zero-cache-config#cvr-db", + "id": "665-zero-cache-config#cvr-db", "title": "zero-cache Config", "searchTitle": "CVR DB", "sectionTitle": "CVR DB", @@ -9000,7 +9312,7 @@ "kind": "section" }, { - "id": "644-zero-cache-config#cvr-garbage-collection-inactivity-threshold-hours", + "id": "666-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", @@ -9010,7 +9322,7 @@ "kind": "section" }, { - "id": "645-zero-cache-config#cvr-garbage-collection-initial-batch-size", + "id": "667-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", @@ -9020,7 +9332,7 @@ "kind": "section" }, { - "id": "646-zero-cache-config#cvr-garbage-collection-initial-interval-seconds", + "id": "668-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", @@ -9030,7 +9342,7 @@ "kind": "section" }, { - "id": "647-zero-cache-config#cvr-max-connections", + "id": "669-zero-cache-config#cvr-max-connections", "title": "zero-cache Config", "searchTitle": "CVR Max Connections", "sectionTitle": "CVR Max Connections", @@ -9040,7 +9352,7 @@ "kind": "section" }, { - "id": "648-zero-cache-config#enable-heapz", + "id": "670-zero-cache-config#enable-heapz", "title": "zero-cache Config", "searchTitle": "Enable Heapz", "sectionTitle": "Enable Heapz", @@ -9050,7 +9362,7 @@ "kind": "section" }, { - "id": "649-zero-cache-config#enable-query-planner", + "id": "671-zero-cache-config#enable-query-planner", "title": "zero-cache Config", "searchTitle": "Enable Query Planner", "sectionTitle": "Enable Query Planner", @@ -9060,7 +9372,7 @@ "kind": "section" }, { - "id": "650-zero-cache-config#enable-crud-mutations", + "id": "672-zero-cache-config#enable-crud-mutations", "title": "zero-cache Config", "searchTitle": "Enable CRUD Mutations", "sectionTitle": "Enable CRUD Mutations", @@ -9070,7 +9382,7 @@ "kind": "section" }, { - "id": "651-zero-cache-config#enable-telemetry", + "id": "673-zero-cache-config#enable-telemetry", "title": "zero-cache Config", "searchTitle": "Enable Telemetry", "sectionTitle": "Enable Telemetry", @@ -9080,7 +9392,7 @@ "kind": "section" }, { - "id": "652-zero-cache-config#initial-sync-table-copy-workers", + "id": "674-zero-cache-config#initial-sync-table-copy-workers", "title": "zero-cache Config", "searchTitle": "Initial Sync Table Copy Workers", "sectionTitle": "Initial Sync Table Copy Workers", @@ -9090,7 +9402,7 @@ "kind": "section" }, { - "id": "653-zero-cache-config#lazy-startup", + "id": "675-zero-cache-config#lazy-startup", "title": "zero-cache Config", "searchTitle": "Lazy Startup", "sectionTitle": "Lazy Startup", @@ -9100,7 +9412,7 @@ "kind": "section" }, { - "id": "654-zero-cache-config#litestream-backup-url", + "id": "676-zero-cache-config#litestream-backup-url", "title": "zero-cache Config", "searchTitle": "Litestream Backup URL", "sectionTitle": "Litestream Backup URL", @@ -9110,7 +9422,7 @@ "kind": "section" }, { - "id": "655-zero-cache-config#litestream-endpoint", + "id": "677-zero-cache-config#litestream-endpoint", "title": "zero-cache Config", "searchTitle": "Litestream Endpoint", "sectionTitle": "Litestream Endpoint", @@ -9120,7 +9432,7 @@ "kind": "section" }, { - "id": "656-zero-cache-config#litestream-checkpoint-threshold-mb", + "id": "678-zero-cache-config#litestream-checkpoint-threshold-mb", "title": "zero-cache Config", "searchTitle": "Litestream Checkpoint Threshold MB", "sectionTitle": "Litestream Checkpoint Threshold MB", @@ -9130,7 +9442,7 @@ "kind": "section" }, { - "id": "657-zero-cache-config#litestream-config-path", + "id": "679-zero-cache-config#litestream-config-path", "title": "zero-cache Config", "searchTitle": "Litestream Config Path", "sectionTitle": "Litestream Config Path", @@ -9140,7 +9452,7 @@ "kind": "section" }, { - "id": "658-zero-cache-config#litestream-config-path-v5", + "id": "680-zero-cache-config#litestream-config-path-v5", "title": "zero-cache Config", "searchTitle": "Litestream Config Path V5", "sectionTitle": "Litestream Config Path V5", @@ -9150,7 +9462,7 @@ "kind": "section" }, { - "id": "659-zero-cache-config#litestream-executable", + "id": "681-zero-cache-config#litestream-executable", "title": "zero-cache Config", "searchTitle": "Litestream Executable", "sectionTitle": "Litestream Executable", @@ -9160,7 +9472,7 @@ "kind": "section" }, { - "id": "660-zero-cache-config#litestream-v5-executable", + "id": "682-zero-cache-config#litestream-v5-executable", "title": "zero-cache Config", "searchTitle": "Litestream V5 Executable", "sectionTitle": "Litestream V5 Executable", @@ -9170,7 +9482,7 @@ "kind": "section" }, { - "id": "661-zero-cache-config#litestream-restore-using-v5", + "id": "683-zero-cache-config#litestream-restore-using-v5", "title": "zero-cache Config", "searchTitle": "Litestream Restore Using V5", "sectionTitle": "Litestream Restore Using V5", @@ -9180,7 +9492,7 @@ "kind": "section" }, { - "id": "662-zero-cache-config#litestream-backup-using-v5", + "id": "684-zero-cache-config#litestream-backup-using-v5", "title": "zero-cache Config", "searchTitle": "Litestream Backup Using V5", "sectionTitle": "Litestream Backup Using V5", @@ -9190,7 +9502,7 @@ "kind": "section" }, { - "id": "663-zero-cache-config#litestream-incremental-backup-interval-minutes", + "id": "685-zero-cache-config#litestream-incremental-backup-interval-minutes", "title": "zero-cache Config", "searchTitle": "Litestream Incremental Backup Interval Minutes", "sectionTitle": "Litestream Incremental Backup Interval Minutes", @@ -9200,7 +9512,7 @@ "kind": "section" }, { - "id": "664-zero-cache-config#litestream-incremental-backup-interval-seconds", + "id": "686-zero-cache-config#litestream-incremental-backup-interval-seconds", "title": "zero-cache Config", "searchTitle": "Litestream Incremental Backup Interval Seconds", "sectionTitle": "Litestream Incremental Backup Interval Seconds", @@ -9210,7 +9522,7 @@ "kind": "section" }, { - "id": "665-zero-cache-config#litestream-maximum-checkpoint-page-count", + "id": "687-zero-cache-config#litestream-maximum-checkpoint-page-count", "title": "zero-cache Config", "searchTitle": "Litestream Maximum Checkpoint Page Count", "sectionTitle": "Litestream Maximum Checkpoint Page Count", @@ -9220,7 +9532,7 @@ "kind": "section" }, { - "id": "666-zero-cache-config#litestream-multipart-concurrency", + "id": "688-zero-cache-config#litestream-multipart-concurrency", "title": "zero-cache Config", "searchTitle": "Litestream Multipart Concurrency", "sectionTitle": "Litestream Multipart Concurrency", @@ -9230,7 +9542,7 @@ "kind": "section" }, { - "id": "667-zero-cache-config#litestream-multipart-size", + "id": "689-zero-cache-config#litestream-multipart-size", "title": "zero-cache Config", "searchTitle": "Litestream Multipart Size", "sectionTitle": "Litestream Multipart Size", @@ -9240,7 +9552,7 @@ "kind": "section" }, { - "id": "668-zero-cache-config#litestream-log-level", + "id": "690-zero-cache-config#litestream-log-level", "title": "zero-cache Config", "searchTitle": "Litestream Log Level", "sectionTitle": "Litestream Log Level", @@ -9250,7 +9562,7 @@ "kind": "section" }, { - "id": "669-zero-cache-config#litestream-port", + "id": "691-zero-cache-config#litestream-port", "title": "zero-cache Config", "searchTitle": "Litestream Port", "sectionTitle": "Litestream Port", @@ -9260,7 +9572,7 @@ "kind": "section" }, { - "id": "670-zero-cache-config#litestream-region", + "id": "692-zero-cache-config#litestream-region", "title": "zero-cache Config", "searchTitle": "Litestream Region", "sectionTitle": "Litestream Region", @@ -9270,7 +9582,7 @@ "kind": "section" }, { - "id": "671-zero-cache-config#litestream-restore-parallelism", + "id": "693-zero-cache-config#litestream-restore-parallelism", "title": "zero-cache Config", "searchTitle": "Litestream Restore Parallelism", "sectionTitle": "Litestream Restore Parallelism", @@ -9280,7 +9592,7 @@ "kind": "section" }, { - "id": "672-zero-cache-config#litestream-snapshot-backup-interval-hours", + "id": "694-zero-cache-config#litestream-snapshot-backup-interval-hours", "title": "zero-cache Config", "searchTitle": "Litestream Snapshot Backup Interval Hours", "sectionTitle": "Litestream Snapshot Backup Interval Hours", @@ -9290,7 +9602,7 @@ "kind": "section" }, { - "id": "673-zero-cache-config#log-format", + "id": "695-zero-cache-config#log-format", "title": "zero-cache Config", "searchTitle": "Log Format", "sectionTitle": "Log Format", @@ -9300,7 +9612,7 @@ "kind": "section" }, { - "id": "674-zero-cache-config#log-ivm-sampling", + "id": "696-zero-cache-config#log-ivm-sampling", "title": "zero-cache Config", "searchTitle": "Log IVM Sampling", "sectionTitle": "Log IVM Sampling", @@ -9310,7 +9622,7 @@ "kind": "section" }, { - "id": "675-zero-cache-config#log-level", + "id": "697-zero-cache-config#log-level", "title": "zero-cache Config", "searchTitle": "Log Level", "sectionTitle": "Log Level", @@ -9320,7 +9632,7 @@ "kind": "section" }, { - "id": "676-zero-cache-config#log-slow-hydrate-threshold", + "id": "698-zero-cache-config#log-slow-hydrate-threshold", "title": "zero-cache Config", "searchTitle": "Log Slow Hydrate Threshold", "sectionTitle": "Log Slow Hydrate Threshold", @@ -9330,7 +9642,7 @@ "kind": "section" }, { - "id": "677-zero-cache-config#log-slow-row-threshold", + "id": "699-zero-cache-config#log-slow-row-threshold", "title": "zero-cache Config", "searchTitle": "Log Slow Row Threshold", "sectionTitle": "Log Slow Row Threshold", @@ -9340,7 +9652,7 @@ "kind": "section" }, { - "id": "678-zero-cache-config#mutate-api-key", + "id": "700-zero-cache-config#mutate-api-key", "title": "zero-cache Config", "searchTitle": "Mutate API Key", "sectionTitle": "Mutate API Key", @@ -9350,7 +9662,7 @@ "kind": "section" }, { - "id": "679-zero-cache-config#mutate-allowed-client-headers", + "id": "701-zero-cache-config#mutate-allowed-client-headers", "title": "zero-cache Config", "searchTitle": "Mutate Allowed Client Headers", "sectionTitle": "Mutate Allowed Client Headers", @@ -9360,7 +9672,7 @@ "kind": "section" }, { - "id": "680-zero-cache-config#mutate-allowed-request-headers", + "id": "702-zero-cache-config#mutate-allowed-request-headers", "title": "zero-cache Config", "searchTitle": "Mutate Allowed Request Headers", "sectionTitle": "Mutate Allowed Request Headers", @@ -9370,7 +9682,7 @@ "kind": "section" }, { - "id": "681-zero-cache-config#mutate-forward-cookies", + "id": "703-zero-cache-config#mutate-forward-cookies", "title": "zero-cache Config", "searchTitle": "Mutate Forward Cookies", "sectionTitle": "Mutate Forward Cookies", @@ -9380,7 +9692,7 @@ "kind": "section" }, { - "id": "682-zero-cache-config#mutate-url", + "id": "704-zero-cache-config#mutate-url", "title": "zero-cache Config", "searchTitle": "Mutate URL", "sectionTitle": "Mutate URL", @@ -9390,7 +9702,7 @@ "kind": "section" }, { - "id": "683-zero-cache-config#number-of-sync-workers", + "id": "705-zero-cache-config#number-of-sync-workers", "title": "zero-cache Config", "searchTitle": "Number of Sync Workers", "sectionTitle": "Number of Sync Workers", @@ -9400,7 +9712,7 @@ "kind": "section" }, { - "id": "684-zero-cache-config#operator-password", + "id": "706-zero-cache-config#operator-password", "title": "zero-cache Config", "searchTitle": "Operator Password", "sectionTitle": "Operator Password", @@ -9410,7 +9722,7 @@ "kind": "section" }, { - "id": "685-zero-cache-config#per-user-mutation-limit-max", + "id": "707-zero-cache-config#per-user-mutation-limit-max", "title": "zero-cache Config", "searchTitle": "Per User Mutation Limit Max", "sectionTitle": "Per User Mutation Limit Max", @@ -9420,7 +9732,7 @@ "kind": "section" }, { - "id": "686-zero-cache-config#per-user-mutation-limit-window-ms", + "id": "708-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)", @@ -9430,7 +9742,7 @@ "kind": "section" }, { - "id": "687-zero-cache-config#pg-replication-slot-failover", + "id": "709-zero-cache-config#pg-replication-slot-failover", "title": "zero-cache Config", "searchTitle": "PG Replication Slot Failover", "sectionTitle": "PG Replication Slot Failover", @@ -9440,7 +9752,7 @@ "kind": "section" }, { - "id": "688-zero-cache-config#port", + "id": "710-zero-cache-config#port", "title": "zero-cache Config", "searchTitle": "Port", "sectionTitle": "Port", @@ -9450,7 +9762,17 @@ "kind": "section" }, { - "id": "689-zero-cache-config#query-api-key", + "id": "711-zero-cache-config#pre-drain-delay-ms", + "title": "zero-cache Config", + "searchTitle": "Pre-Drain Delay (ms)", + "sectionTitle": "Pre-Drain Delay (ms)", + "sectionId": "pre-drain-delay-ms", + "url": "/docs/zero-cache-config", + "content": "The time between receiving a SIGTERM (or SIGINT) and starting to drain. During this time zero-cache fails its readiness check at /, so load balancers stop routing to it, while it keeps accepting and serving connections as usual. The liveness check at /keepalive keeps succeeding. This takes the place of a pre-stop delay. Set it longer than the time your load balancer needs to notice the failed readiness check, and keep it within your container runner's termination grace period. With the default of 0, draining starts immediately. flag: --pre-drain-delay-ms env: ZERO_PRE_DRAIN_DELAY_MS default: 0", + "kind": "section" + }, + { + "id": "712-zero-cache-config#query-api-key", "title": "zero-cache Config", "searchTitle": "Query API Key", "sectionTitle": "Query API Key", @@ -9460,7 +9782,7 @@ "kind": "section" }, { - "id": "690-zero-cache-config#query-allowed-client-headers", + "id": "713-zero-cache-config#query-allowed-client-headers", "title": "zero-cache Config", "searchTitle": "Query Allowed Client Headers", "sectionTitle": "Query Allowed Client Headers", @@ -9470,7 +9792,7 @@ "kind": "section" }, { - "id": "691-zero-cache-config#query-allowed-request-headers", + "id": "714-zero-cache-config#query-allowed-request-headers", "title": "zero-cache Config", "searchTitle": "Query Allowed Request Headers", "sectionTitle": "Query Allowed Request Headers", @@ -9480,7 +9802,7 @@ "kind": "section" }, { - "id": "692-zero-cache-config#query-forward-cookies", + "id": "715-zero-cache-config#query-forward-cookies", "title": "zero-cache Config", "searchTitle": "Query Forward Cookies", "sectionTitle": "Query Forward Cookies", @@ -9490,7 +9812,7 @@ "kind": "section" }, { - "id": "693-zero-cache-config#query-hydration-stats", + "id": "716-zero-cache-config#query-hydration-stats", "title": "zero-cache Config", "searchTitle": "Query Hydration Stats", "sectionTitle": "Query Hydration Stats", @@ -9500,7 +9822,7 @@ "kind": "section" }, { - "id": "694-zero-cache-config#query-url", + "id": "717-zero-cache-config#query-url", "title": "zero-cache Config", "searchTitle": "Query URL", "sectionTitle": "Query URL", @@ -9510,7 +9832,7 @@ "kind": "section" }, { - "id": "695-zero-cache-config#replica-file", + "id": "718-zero-cache-config#replica-file", "title": "zero-cache Config", "searchTitle": "Replica File", "sectionTitle": "Replica File", @@ -9520,7 +9842,7 @@ "kind": "section" }, { - "id": "696-zero-cache-config#replica-vacuum-interval-hours", + "id": "719-zero-cache-config#replica-vacuum-interval-hours", "title": "zero-cache Config", "searchTitle": "Replica Vacuum Interval Hours", "sectionTitle": "Replica Vacuum Interval Hours", @@ -9530,7 +9852,7 @@ "kind": "section" }, { - "id": "697-zero-cache-config#replication-lag-initial-readiness-threshold-ms", + "id": "720-zero-cache-config#replication-lag-initial-readiness-threshold-ms", "title": "zero-cache Config", "searchTitle": "Replication Lag Initial Readiness Threshold (ms)", "sectionTitle": "Replication Lag Initial Readiness Threshold (ms)", @@ -9540,7 +9862,7 @@ "kind": "section" }, { - "id": "698-zero-cache-config#replication-lag-max-readiness-delay-ms", + "id": "721-zero-cache-config#replication-lag-max-readiness-delay-ms", "title": "zero-cache Config", "searchTitle": "Replication Lag Max Readiness Delay (ms)", "sectionTitle": "Replication Lag Max Readiness Delay (ms)", @@ -9550,7 +9872,7 @@ "kind": "section" }, { - "id": "699-zero-cache-config#replication-lag-report-interval-ms", + "id": "722-zero-cache-config#replication-lag-report-interval-ms", "title": "zero-cache Config", "searchTitle": "Replication Lag Report Interval (ms)", "sectionTitle": "Replication Lag Report Interval (ms)", @@ -9560,7 +9882,7 @@ "kind": "section" }, { - "id": "700-zero-cache-config#server-version", + "id": "723-zero-cache-config#server-version", "title": "zero-cache Config", "searchTitle": "Server Version", "sectionTitle": "Server Version", @@ -9570,7 +9892,7 @@ "kind": "section" }, { - "id": "701-zero-cache-config#shadow-sync-enabled", + "id": "724-zero-cache-config#shadow-sync-enabled", "title": "zero-cache Config", "searchTitle": "Shadow Sync Enabled", "sectionTitle": "Shadow Sync Enabled", @@ -9580,7 +9902,7 @@ "kind": "section" }, { - "id": "702-zero-cache-config#shadow-sync-interval-hours", + "id": "725-zero-cache-config#shadow-sync-interval-hours", "title": "zero-cache Config", "searchTitle": "Shadow Sync Interval Hours", "sectionTitle": "Shadow Sync Interval Hours", @@ -9590,7 +9912,7 @@ "kind": "section" }, { - "id": "703-zero-cache-config#shadow-sync-sample-rate", + "id": "726-zero-cache-config#shadow-sync-sample-rate", "title": "zero-cache Config", "searchTitle": "Shadow Sync Sample Rate", "sectionTitle": "Shadow Sync Sample Rate", @@ -9600,7 +9922,7 @@ "kind": "section" }, { - "id": "704-zero-cache-config#shadow-sync-max-rows-per-table", + "id": "727-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", @@ -9610,7 +9932,7 @@ "kind": "section" }, { - "id": "705-zero-cache-config#storage-db-temp-dir", + "id": "728-zero-cache-config#storage-db-temp-dir", "title": "zero-cache Config", "searchTitle": "Storage DB Temp Dir", "sectionTitle": "Storage DB Temp Dir", @@ -9620,7 +9942,7 @@ "kind": "section" }, { - "id": "706-zero-cache-config#task-id", + "id": "729-zero-cache-config#task-id", "title": "zero-cache Config", "searchTitle": "Task ID", "sectionTitle": "Task ID", @@ -9630,7 +9952,7 @@ "kind": "section" }, { - "id": "707-zero-cache-config#upstream-max-connections", + "id": "730-zero-cache-config#upstream-max-connections", "title": "zero-cache Config", "searchTitle": "Upstream Max Connections", "sectionTitle": "Upstream Max Connections", @@ -9640,7 +9962,7 @@ "kind": "section" }, { - "id": "708-zero-cache-config#upstream-pg-replication-slot-failover", + "id": "731-zero-cache-config#upstream-pg-replication-slot-failover", "title": "zero-cache Config", "searchTitle": "Upstream PG Replication Slot Failover", "sectionTitle": "Upstream PG Replication Slot Failover", @@ -9650,7 +9972,17 @@ "kind": "section" }, { - "id": "709-zero-cache-config#upstream-pg-stream-inbound-timeout", + "id": "732-zero-cache-config#upstream-pg-replication-slot-per-replica", + "title": "zero-cache Config", + "searchTitle": "Upstream PG Replication Slot Per Replica", + "sectionTitle": "Upstream PG Replication Slot Per Replica", + "sectionId": "upstream-pg-replication-slot-per-replica", + "url": "/docs/zero-cache-config", + "content": "Create a dedicated replication slot for each replication-manager, instead of having a new replication-manager take over the slot of the one it replaces. A new replication-manager restores its replica from the backup, creates its own slot, and catches up while the previous replication-manager keeps serving. It takes over once it has caught up. Expect one slot per running replication-manager in Postgres, each holding WAL until it is consumed. This requires Litestream v5 backups. The backup and the change DB stay compatible with a shared slot, so setting this to false does not cause a resync. This is the first step towards a high-availability mode in which several replication-managers run at the same time. Running more than one replication-manager is not supported yet. flag: --upstream-pg-replication-slot-per-replica env: ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA default: true", + "kind": "section" + }, + { + "id": "733-zero-cache-config#upstream-pg-stream-inbound-timeout", "title": "zero-cache Config", "searchTitle": "Upstream PG Stream Inbound Timeout", "sectionTitle": "Upstream PG Stream Inbound Timeout", @@ -9660,7 +9992,7 @@ "kind": "section" }, { - "id": "710-zero-cache-config#view-syncer-hydration-budget-ms", + "id": "734-zero-cache-config#view-syncer-hydration-budget-ms", "title": "zero-cache Config", "searchTitle": "View Syncer Hydration Budget (ms)", "sectionTitle": "View Syncer Hydration Budget (ms)", @@ -9670,7 +10002,7 @@ "kind": "section" }, { - "id": "711-zero-cache-config#view-syncer-query-hydration-timeout-ms", + "id": "735-zero-cache-config#view-syncer-query-hydration-timeout-ms", "title": "zero-cache Config", "searchTitle": "View Syncer Query Hydration Timeout (ms)", "sectionTitle": "View Syncer Query Hydration Timeout (ms)", @@ -9680,7 +10012,7 @@ "kind": "section" }, { - "id": "712-zero-cache-config#websocket-compression", + "id": "736-zero-cache-config#websocket-compression", "title": "zero-cache Config", "searchTitle": "Websocket Compression", "sectionTitle": "Websocket Compression", @@ -9690,7 +10022,7 @@ "kind": "section" }, { - "id": "713-zero-cache-config#websocket-compression-options", + "id": "737-zero-cache-config#websocket-compression-options", "title": "zero-cache Config", "searchTitle": "Websocket Compression Options", "sectionTitle": "Websocket Compression Options", @@ -9700,7 +10032,7 @@ "kind": "section" }, { - "id": "714-zero-cache-config#websocket-max-payload-bytes", + "id": "738-zero-cache-config#websocket-max-payload-bytes", "title": "zero-cache Config", "searchTitle": "Websocket Max Payload Bytes", "sectionTitle": "Websocket Max Payload Bytes", @@ -9710,7 +10042,7 @@ "kind": "section" }, { - "id": "715-zero-cache-config#yield-threshold-ms", + "id": "739-zero-cache-config#yield-threshold-ms", "title": "zero-cache Config", "searchTitle": "Yield Threshold (ms)", "sectionTitle": "Yield Threshold (ms)", @@ -9720,7 +10052,7 @@ "kind": "section" }, { - "id": "716-zero-cache-config#deprecated-flags", + "id": "740-zero-cache-config#deprecated-flags", "title": "zero-cache Config", "searchTitle": "Deprecated Flags", "sectionTitle": "Deprecated Flags", @@ -9730,7 +10062,7 @@ "kind": "section" }, { - "id": "717-zero-cache-config#change-max-connections", + "id": "741-zero-cache-config#change-max-connections", "title": "zero-cache Config", "searchTitle": "Change Max Connections", "sectionTitle": "Change Max Connections", @@ -9740,7 +10072,7 @@ "kind": "section" }, { - "id": "718-zero-cache-config#change-streamer-mode", + "id": "742-zero-cache-config#change-streamer-mode", "title": "zero-cache Config", "searchTitle": "Change Streamer Mode", "sectionTitle": "Change Streamer Mode", @@ -9750,7 +10082,7 @@ "kind": "section" }, { - "id": "719-zero-cache-config#litestream-minimum-checkpoint-page-count", + "id": "743-zero-cache-config#litestream-minimum-checkpoint-page-count", "title": "zero-cache Config", "searchTitle": "Litestream Minimum Checkpoint Page Count", "sectionTitle": "Litestream Minimum Checkpoint Page Count", @@ -9760,7 +10092,7 @@ "kind": "section" }, { - "id": "720-zero-cache-config#auth-jwk", + "id": "744-zero-cache-config#auth-jwk", "title": "zero-cache Config", "searchTitle": "Auth JWK", "sectionTitle": "Auth JWK", @@ -9770,7 +10102,7 @@ "kind": "section" }, { - "id": "721-zero-cache-config#auth-jwks-url", + "id": "745-zero-cache-config#auth-jwks-url", "title": "zero-cache Config", "searchTitle": "Auth JWKS URL", "sectionTitle": "Auth JWKS URL", @@ -9780,7 +10112,7 @@ "kind": "section" }, { - "id": "722-zero-cache-config#auth-secret", + "id": "746-zero-cache-config#auth-secret", "title": "zero-cache Config", "searchTitle": "Auth Secret", "sectionTitle": "Auth Secret", @@ -9790,11 +10122,11 @@ "kind": "section" }, { - "id": "78-zql", + "id": "79-zql", "title": "ZQL", "searchTitle": "ZQL", "url": "/docs/zql", - "content": "Inspired by SQL, ZQL is expressed in TypeScript with heavy use of the builder pattern. If you have used Drizzle or Kysely, ZQL will feel familiar. ZQL queries are composed of one or more clauses that are chained together into a query. Create a Builder To get started, use createBuilder. If you use drizzle-zero or prisma-zero, this happens automatically and an instance is stored in the zql constant exported from schema.ts: import {zql} from 'schema.ts' // zql.myTable.where(...) Otherwise, create an instance manually: // schema.ts // ... export const zql = createBuilder(schema) Select ZQL queries start by selecting a table. There is no way to select a subset of columns; ZQL queries always return the entire row, if permissions allow it. import {zql} from 'zero.ts' // Returns a query that selects all rows and columns from the // issue table. zql.issue This is a design tradeoff that allows Zero to better reuse the row locally for future queries. This also makes it easier to share types between different parts of the code. Do not directly modify JavaScript values returned from ZQL queries. ZQL caches values across queries to improve performance and reduce re-renders in frameworks like React and Solid. If you modify a value returned from ZQL, you will modify it everywhere it is used. This can lead to subtle bugs. JavaScript and TypeScript lack true immutable types so we use readonly to help enforce it. But it's easy to cast away the readonly accidentally. Ordering You can sort query results by adding an orderBy clause: zql.issue.orderBy('created', 'desc') Multiple orderBy clauses can be present, in which case the data is sorted by those clauses in order: // Order by priority descending. For any rows with same priority, // then order by created desc. zql.issue .orderBy('priority', 'desc') .orderBy('created', 'desc') All queries in ZQL have a default final order of their primary key. Assuming the issue table has a primary key on the id column, then: // Actually means: zql.issue.orderBy('id', 'asc'); zql.issue // Actually means: zql.issue.orderBy('priority', 'desc').orderBy('id', 'asc'); zql.issue.orderBy('priority', 'desc') Limit You can limit the number of rows to return with limit(): zql.issue.orderBy('created', 'desc').limit(100) Paging You can start the results at or after a particular row with start(): let start: IssueRow | undefined while (true) { let q = zql.issue .orderBy('created', 'desc') .limit(100) if (start) { q = q.start(start) } const batch = await q.run() console.log('got batch', batch) if (batch.length < 100) { break } start = batch[batch.length - 1] } By default start() is exclusive - it returns rows starting after the supplied reference row. This is what you usually want for paging. If you want inclusive results, you can do: zql.issue.start(row, {inclusive: true}) Getting a Single Result If you want exactly zero or one results, use the one() clause. This causes ZQL to return Row|undefined rather than Row[]. const result = await zql.issue .where('id', 42) .one() .run() if (!result) { console.error('not found') } one() overrides any limit() clause that is also present. Relationships You can query related rows using relationships that are defined in your Zero schema. // Get all issues and their related comments zql.issue.related('comments') Relationships are returned as hierarchical data. In the above example, each row will have a comments field, which is an array of the corresponding comments rows. You can fetch multiple relationships in a single query: zql.issue .related('comments') .related('reactions') .related('assignees') Refining Relationships By default all matching relationship rows are returned, but this can be refined. The related method accepts an optional second function which is itself a query. zql.issue.related( 'comments', // It is common to use the 'q' shorthand variable for this parameter, // but it is a _comment_ query in particular here, exactly as if you // had done zql.comment. q => q .orderBy('modified', 'desc') .limit(100) .start(lastSeenComment) ) This relationship query can have all the same clauses that top-level queries can have. Using orderBy or limit in a relationship that goes through a junction table (i.e., a many-to-many relationship) is not currently supported and will throw a runtime error. See bug 3527. You can sometimes work around this by making the junction relationship explicit, depending on your schema and usage. Nested Relationships You can nest relationships arbitrarily: // Get all issues, first 100 comments for each (ordered by modified,desc), // and for each comment all of its reactions. zql.issue.related('comments', q => q .orderBy('modified', 'desc') .limit(100) .related('reactions') ) Where You can filter a query with where(): zql.issue.where('priority', '=', 'high') The first parameter is always a column name from the table being queried. TypeScript completion will offer available options (sourced from your Zero Schema). Comparison Operators where() supports the following comparison operators: TypeScript will restrict you from using operators with types that don’t make sense – you can’t use > with boolean for example. Zero can evaluate an ILIKE filter in three places: in-memory on the client, in ZQLite against the replica, or as SQL in Postgres. ZQLite and the in-memory query engine use Unicode-aware lowercasing before matching. This means common non-ASCII case variants, such as MÜLLER and müller, match in both. If you need non-ASCII text in PG to work with ILIKE, use a Unicode-aware collation for those text columns, such as the database's default UTF-8 locale. Equals is the Default Comparison Operator Because comparing by = is so common, you can leave it out and where defaults to =. zql.issue.where('priority', 'high') Comparing to null As in SQL, ZQL’s null cannot be compared with =, !=, <, or any other normal comparison operator. Comparing any value to null with such operators is always false: These semantics feel a bit weird, but they are consistent with SQL. The reason SQL does it this way is to make join semantics work: if you’re joining employee.orgID on org.id you do not want an employee in no organization to match an org that hasn’t yet been assigned an ID. For when you purposely do want to compare to null ZQL supports IS and IS NOT operators that also work just like in SQL: // Find employees not in any org. zql.employee.where('orgID', 'IS', null) // Find employees in an org other than 42 OR employees in NO org zql.employee.where('orgID', 'IS NOT', 42) TypeScript will prevent you from comparing to null with other operators. Comparing to undefined As a convenience, you can pass undefined to where: zql.issue.where('priority', issue?.priority) This comparison is always false, so the above query always returns no results. Compound Filters The argument to where can also be a callback that returns a complex expression: // Get all issues that have priority 'critical' or else have both // priority 'medium' and not more than 100 votes. zql.issue.where(({cmp, and, or, not}) => or( cmp('priority', 'critical'), and( cmp('priority', 'medium'), not(cmp('numVotes', '>', 100)) ) ) ) cmp is short for compare and works the same as where at the top-level except that it can’t be chained and it only accepts comparison operators (no relationship filters – see below). Note that chaining where() is also a one-level and: // Find issues with priority 3 or higher, owned by aa zql.issue .where('priority', '>=', 3) .where('owner', 'aa') Comparing Literal Values The where clause always expects its first parameter to be a column name as a string. Same with the cmp helper: // \"foo\" is a column name, not a string: zql.issue.where('foo', 'bar') // \"foo\" is a column name, not a string: zql.issue.where(({cmp}) => cmp('foo', 'bar')) To compare to a literal value, use the cmpLit helper: zql.issue.where(cmpLit('foobar', 'foo' + 'bar')) This is particularly useful for implementing permissions, because the first parameter can be a field of your context: zql.issue.where(cmpLit(ctx.role, 'admin')) Relationship Filters Your filter can also test properties of relationships. Currently the only supported test is existence: // Find all orgs that have at least one employee zql.organization.whereExists('employees') The argument to whereExists is a relationship, so just like other relationships, it can be refined with a query: // Find all orgs that have at least one cool employee zql.organization.whereExists('employees', q => q.where('location', 'Hawaii') ) As with querying relationships, relationship filters can be arbitrarily nested: // Get all issues that have comments that have reactions zql.issue.whereExists('comments', q => q.whereExists('reactions') ) The exists helper is also provided which can be used with and, or, cmp, and not to build compound filters that check relationship existence: // Find issues that have at least one comment or are high priority zql.issue.where({cmp, or, exists} => or( cmp('priority', 'high'), exists('comments'), ), ) Type Helpers You can get the TypeScript type of the result of a query using the QueryResultType helper: import type {QueryResultType} from '@rocicorp/zero' const complexQuery = zql.issue.related( 'comments', q => q.related('author') ) type MyComplexResult = QueryResultType // MyComplexResult is: readonly IssueRow & { // readonly comments: readonly (CommentRow & { // readonly author: readonly AuthorRow|undefined; // })[]; // }[] You can get the type of a single row with QueryRowType: import type {QueryRowType} from '@rocicorp/zero' type MySingleRow = QueryRowType // MySingleRow is: readonly IssueRow & { // readonly comments: readonly (CommentRow & { // readonly author: readonly AuthorRow|undefined; // })[]; // } Planning Zero automatically plans queries, selecting the best indexes and join orders in most cases. Inspecting Query Plans You can inspect the plan that Zero generates for any ZQL query using the inspector. Manually Flipping Joins The process Zero uses to optimize joins is called \"join flipping\", because it involves \"flipping\" the order of joins to minimize the number of rows processed. Typically the Zero planner will pick the joins to flip automatically. But in some rare cases, you may want to manually specify the join order. This can be done by passing the flip:true option to whereExists: // Find the first 100 documents that user 42 can edit, // ordered by created desc. Because each user is an editor // of only a few documents, flip:true is much faster than // flip:false. zql.documents.whereExists('editors', e => e.where('userID', 42), {flip: true} ), .orderBy('created', 'desc') .limit(100) Or with exists: // Find issues created by user 42 or that have a comment // by user 42. Because user 42 has commented on only a // few issues, flip:true is much faster than flip:false. zql.issue.where({cmp, or, exists} => or( cmp('creatorID', 42), exists('comments', c => c.where('creatorID', 42), {flip: true}), ), ) You can manually flip just one or a few of the whereExists clauses in a query, leaving the rest to be planned automatically. Scalar Subqueries Scalar subqueries are an optimization for exists queries. Instead of doing a join at query time, Zero pre-resolves the subquery and rewrites it as a simple equality check. To use scalar subqueries, add a unique index in Postgres and declare it as unique in the Zero schema, then add {scalar: true} to your whereExists call: // Instead of joining to find issues where project.name = 'zero' // Zero resolves this server-side to: where('projectId', '123') zql.issue.whereExists( 'project', q => q.where('name', 'zero'), {scalar: true} ) Or with exists: zql.issue.where({cmp, exists} => exists('project', q => q.where('name', 'zero'), {scalar: true}, ), ) Why It Matters Joins are expensive. Sometimes they are needed, but for something like \"give me all issues where the owner's name is Alice\", you don't need a full join — you just need Alice's ID. The scalar optimization pre-fetches that ID and rewrites your query as where('ownerId', aliceId). This can improve query performance significantly. It also allows planning to work better. Since the ID is known at planning time, Zero/SQLite can choose better indexes. Trade-offs The query needs to be \"rehydrated\" (re-run) whenever the scalar subquery result changes. This is fine for relatively stable lookup data like user IDs or project IDs, but you probably wouldn't want it for rapidly-changing data. Scalar subqueries only work when the subquery is guaranteed to return at most one row. The callback must constrain every column of a primary key or a key declared with .unique(...) using equality. Compound keys require an equality constraint for every column. They also only support one-hop relationships, not junctions. Future Work Scalar subqueries are not currently integrated with Zero's planner. You need to manually choose when to use them.", + "content": "Inspired by SQL, ZQL is expressed in TypeScript with heavy use of the builder pattern. If you have used Drizzle or Kysely, ZQL will feel familiar. ZQL queries are composed of one or more clauses that are chained together into a query. Create a Builder To get started, use createBuilder. If you use drizzle-zero or prisma-zero, this happens automatically and an instance is stored in the zql constant exported from schema.ts: import {zql} from 'schema.ts' // zql.myTable.where(...) Otherwise, create an instance manually: // schema.ts // ... export const zql = createBuilder(schema) Select ZQL queries start by selecting a table. There is no way to select a subset of columns; ZQL queries always return the entire row, if permissions allow it. import {zql} from 'zero.ts' // Returns a query that selects all rows and columns from the // issue table. zql.issue This is a design tradeoff that allows Zero to better reuse the row locally for future queries. This also makes it easier to share types between different parts of the code. Do not directly modify JavaScript values returned from ZQL queries. ZQL caches values across queries to improve performance and reduce re-renders in frameworks like React and Solid. If you modify a value returned from ZQL, you will modify it everywhere it is used. This can lead to subtle bugs. JavaScript and TypeScript lack true immutable types so we use readonly to help enforce it. But it's easy to cast away the readonly accidentally. Ordering You can sort query results by adding an orderBy clause: zql.issue.orderBy('created', 'desc') Multiple orderBy clauses can be present, in which case the data is sorted by those clauses in order: // Order by priority descending. For any rows with same priority, // then order by created desc. zql.issue .orderBy('priority', 'desc') .orderBy('created', 'desc') All queries in ZQL have a default final order of their primary key. Assuming the issue table has a primary key on the id column, then: // Actually means: zql.issue.orderBy('id', 'asc'); zql.issue // Actually means: zql.issue.orderBy('priority', 'desc').orderBy('id', 'asc'); zql.issue.orderBy('priority', 'desc') Limit You can limit the number of rows to return with limit(): zql.issue.orderBy('created', 'desc').limit(100) Paging You can start the results at or after a particular row with start(): let start: IssueRow | undefined while (true) { let q = zql.issue .orderBy('created', 'desc') .limit(100) if (start) { q = q.start(start) } const batch = await q.run() console.log('got batch', batch) if (batch.length < 100) { break } start = batch[batch.length - 1] } By default start() is exclusive - it returns rows starting after the supplied reference row. This is what you usually want for paging. If you want inclusive results, you can do: zql.issue.start(row, {inclusive: true}) Getting a Single Result If you want exactly zero or one results, use the one() clause. This causes ZQL to return Row|undefined rather than Row[]. const result = await zql.issue .where('id', 42) .one() .run() if (!result) { console.error('not found') } one() overrides any limit() clause that is also present. Relationships You can query related rows using relationships that are defined in your Zero schema. // Get all issues and their related comments zql.issue.related('comments') Relationships are returned as hierarchical data. In the above example, each row will have a comments field, which is an array of the corresponding comments rows. You can fetch multiple relationships in a single query: zql.issue .related('comments') .related('reactions') .related('assignees') Refining Relationships By default all matching relationship rows are returned, but this can be refined. The related method accepts an optional second function which is itself a query. zql.issue.related( 'comments', // It is common to use the 'q' shorthand variable for this parameter, // but it is a _comment_ query in particular here, exactly as if you // had done zql.comment. q => q .orderBy('modified', 'desc') .limit(100) .start(lastSeenComment) ) This relationship query can have all the same clauses that top-level queries can have. Using orderBy or limit in a relationship that goes through a junction table (i.e., a many-to-many relationship) is not currently supported and will throw a runtime error. See bug 3527. You can sometimes work around this by making the junction relationship explicit, depending on your schema and usage. Nested Relationships You can nest relationships arbitrarily: // Get all issues, first 100 comments for each (ordered by modified,desc), // and for each comment all of its reactions. zql.issue.related('comments', q => q .orderBy('modified', 'desc') .limit(100) .related('reactions') ) Where You can filter a query with where(): zql.issue.where('priority', '=', 'high') The first parameter is always a column name from the table being queried. TypeScript completion will offer available options (sourced from your Zero Schema). Comparison Operators where() supports the following comparison operators: TypeScript will restrict you from using operators with types that don’t make sense – you can’t use > with boolean for example. Zero can evaluate an ILIKE filter in three places: in-memory on the client, in ZQLite against the replica, or as SQL in Postgres. ZQLite and the in-memory query engine use Unicode-aware lowercasing before matching. This means common non-ASCII case variants, such as MÜLLER and müller, match in both. If you need non-ASCII text in PG to work with ILIKE, use a Unicode-aware collation for those text columns, such as the database's default UTF-8 locale. Equals is the Default Comparison Operator Because comparing by = is so common, you can leave it out and where defaults to =. zql.issue.where('priority', 'high') Comparing to null As in SQL, ZQL’s null cannot be compared with =, !=, <, or any other normal comparison operator. Comparing any value to null with such operators is always false: These semantics feel a bit weird, but they are consistent with SQL. The reason SQL does it this way is to make join semantics work: if you’re joining employee.orgID on org.id you do not want an employee in no organization to match an org that hasn’t yet been assigned an ID. For when you purposely do want to compare to null ZQL supports IS and IS NOT operators that also work just like in SQL: // Find employees not in any org. zql.employee.where('orgID', 'IS', null) // Find employees in an org other than 42 OR employees in NO org zql.employee.where('orgID', 'IS NOT', 42) TypeScript will prevent you from comparing to null with other operators. Comparing to undefined As a convenience, you can pass undefined to where: zql.issue.where('priority', issue?.priority) This comparison is always false, so the above query always returns no results. Compound Filters The argument to where can also be a callback that returns a complex expression: // Get all issues that have priority 'critical' or else have both // priority 'medium' and not more than 100 votes. zql.issue.where(({cmp, and, or, not}) => or( cmp('priority', 'critical'), and( cmp('priority', 'medium'), not(cmp('numVotes', '>', 100)) ) ) ) cmp is short for compare and works the same as where at the top-level except that it can’t be chained and it only accepts comparison operators (no relationship filters – see below). Note that chaining where() is also a one-level and: // Find issues with priority 3 or higher, owned by aa zql.issue .where('priority', '>=', 3) .where('owner', 'aa') Comparing Literal Values The where clause always expects its first parameter to be a column name as a string. Same with the cmp helper: // \"foo\" is a column name, not a string: zql.issue.where('foo', 'bar') // \"foo\" is a column name, not a string: zql.issue.where(({cmp}) => cmp('foo', 'bar')) To compare to a literal value, use the cmpLit helper: zql.issue.where(cmpLit('foobar', 'foo' + 'bar')) This is particularly useful for implementing permissions, because the first parameter can be a field of your context: zql.issue.where(cmpLit(ctx.role, 'admin')) JSON Filters You can filter on a value inside a json() column with the json helper. Pass it the column name followed by a path into the value: // Find issues whose metadata.priority is 'high' zql.issue.where(({cmp, json}) => cmp(json('metadata', 'priority'), 'high') ) json can only be used inside a callback, as the first argument to cmp. It works with all the comparison operators, and it can be combined with and, or, not, and exists like any other cmp. Path segments are applied left to right. A string segment reads a key of an object and a number segment reads an index of an array: // metadata.address.zip cmp(json('metadata', 'address', 'zip'), '94110') // The first element of metadata.tags cmp(json('metadata', 'tags', 0), 'bug') Array indexes must be non-negative integers. A string never indexes an array and a number never reads an object key, so json('metadata', 'tags', '0') reads as a missing value (see Missing Values). If the column has a custom JSON type, TypeScript completes the path segments and types the value you compare against. A column declared with plain json() accepts any path. Comparisons Are Type-Strict A JSON value doesn't have a fixed type the way a column does, so comparisons follow JS strict equal (===) semantics. A non-null value of a different type than the one you compare against never matches a positive operator and always matches a negated one: LIKE and ILIKE only match string values. Values can be compared against strings, numbers, and booleans, and against null with IS and IS NOT (see Missing Values). You can't compare a whole object or array. The array passed to IN and NOT IN must contain values of a single type: cmp(json('metadata', 'priority'), 'IN', [ 'high', 'critical' ]) Missing Values A path that doesn't exist and a JSON null both read as null. As with columns, they never match =, !=, or the other value operators. Use IS and IS NOT to find them: // Issues with no metadata.priority (missing or null) zql.issue.where(({cmp, json}) => cmp(json('metadata', 'priority'), 'IS', null) ) Relationship Filters Your filter can also test properties of relationships. Currently the only supported test is existence: // Find all orgs that have at least one employee zql.organization.whereExists('employees') The argument to whereExists is a relationship, so just like other relationships, it can be refined with a query: // Find all orgs that have at least one cool employee zql.organization.whereExists('employees', q => q.where('location', 'Hawaii') ) As with querying relationships, relationship filters can be arbitrarily nested: // Get all issues that have comments that have reactions zql.issue.whereExists('comments', q => q.whereExists('reactions') ) The exists helper is also provided which can be used with and, or, cmp, and not to build compound filters that check relationship existence: // Find issues that have at least one comment or are high priority zql.issue.where({cmp, or, exists} => or( cmp('priority', 'high'), exists('comments'), ), ) Aggregates You can compute count(), sum(), avg(), min(), or max() over a query. Instead of rows, the query returns a single value: // The number of high priority issues zql.issue.where('priority', 'high').count() // The most votes any issue has zql.issue.max('numVotes') Aggregates also work on relationships. Each row then gets the aggregate value in place of the array of related rows: // Each issue has a `comments` field holding its number of comments zql.issue.related('comments', q => q.count()) Zero computes aggregates on the server and syncs only the result, so counting thousands of comments doesn't download them. The value stays up to date as rows change. where(), start(), and limit() apply before the aggregate, just like in SQL, so they have to come before it: zql.issue.orderBy('created', 'desc').limit(10).avg('numVotes') averages the ten newest issues. sum() and avg() take numeric columns, while min() and max() also accept string columns. Over no rows, count() returns 0 and the others return null. To aggregate a query defined with defineQuery(), put the aggregate in its definition. Adding one to the query it returns, as in queries.issues().count(), throws: export const queries = defineQueries({ highPriorityCount: defineQuery(() => zql.issue.where('priority', 'high').count() ) }) When a mutator adds or removes related rows, a relationship's count, sum, or average updates right away, as long as the relationship doesn't go through a junction table or use limit(), start(), or whereExists(). Other aggregates update once the server has run the mutation. Inside a mutator, tx.run() reads an aggregate's synced value if the app also syncs that query, and otherwise computes it from the rows on the client, which can be fewer than the server has. Distinct distinct() returns one row for each distinct combination of the columns you name, as SQL's SELECT DISTINCT does. The rows have only those columns. Following it with count() counts the combinations instead: // The people who created a high priority issue: [{creatorID: 'u1'}, …] zql.issue.where('priority', 'high').distinct('creatorID') // How many of them there are zql.issue .where('priority', 'high') .distinct('creatorID') .count() null is a value like any other: issues with no creator give one row with creatorID: null, and count() counts it as one value. That differs from SQL's COUNT(DISTINCT creatorID), which skips NULL, but it always matches the number of rows distinct() returns. The rows are ordered by the distinct columns, ascending unless orderBy() says otherwise, and orderBy() can only name those columns. Group By groupBy() computes an aggregate for each group of rows that share the values of the columns you name, as SQL's GROUP BY does. It has to be followed by count(), sum(), avg(), min(), or max(), and each row holds the group's columns and the aggregate's value under the function's name: // [{creatorID: 'u1', count: 12}, {creatorID: null, count: 3}, …] zql.issue .where('priority', 'high') .groupBy('creatorID') .count() // [{creatorID: 'u1', priority: 'high', sum: 40}, …] zql.issue.groupBy('creatorID', 'priority').sum('numVotes') The aggregates work as they do without groupBy(), and a group whose values are all null gets null from sum(), avg(), min(), and max(). As with distinct(), rows with a null value form a group of their own, and the rows can only be ordered by the grouped columns. Ordering groups by their aggregate, such as the creators with the most issues first, isn't supported yet. Neither distinct() nor groupBy() can be combined with limit(), start(), one(), related(), or another aggregate yet, and neither can group by a JSON column. They can't be used inside related() or whereExists() either, so counting each issue's distinct commenters isn't possible yet. As with other aggregates, a query defined with defineQuery() has to include them in its definition. When a mutator changes rows, the rows of a distinct() and the values of a groupBy() with count(), sum(), or avg() update right away, as long as the query's where() doesn't use whereExists(). A groupBy() with min() or max(), and distinct().count(), update once the server has run the mutation. Type Helpers You can get the TypeScript type of the result of a query using the QueryResultType helper: import type {QueryResultType} from '@rocicorp/zero' const complexQuery = zql.issue.related( 'comments', q => q.related('author') ) type MyComplexResult = QueryResultType // MyComplexResult is: readonly IssueRow & { // readonly comments: readonly (CommentRow & { // readonly author: readonly AuthorRow|undefined; // })[]; // }[] You can get the type of a single row with QueryRowType: import type {QueryRowType} from '@rocicorp/zero' type MySingleRow = QueryRowType // MySingleRow is: readonly IssueRow & { // readonly comments: readonly (CommentRow & { // readonly author: readonly AuthorRow|undefined; // })[]; // } Planning Zero automatically plans queries, selecting the best indexes and join orders in most cases. Inspecting Query Plans You can inspect the plan that Zero generates for any ZQL query using the inspector. Manually Flipping Joins The process Zero uses to optimize joins is called \"join flipping\", because it involves \"flipping\" the order of joins to minimize the number of rows processed. Typically the Zero planner will pick the joins to flip automatically. But in some rare cases, you may want to manually specify the join order. This can be done by passing the flip:true option to whereExists: // Find the first 100 documents that user 42 can edit, // ordered by created desc. Because each user is an editor // of only a few documents, flip:true is much faster than // flip:false. zql.documents.whereExists('editors', e => e.where('userID', 42), {flip: true} ), .orderBy('created', 'desc') .limit(100) Or with exists: // Find issues created by user 42 or that have a comment // by user 42. Because user 42 has commented on only a // few issues, flip:true is much faster than flip:false. zql.issue.where({cmp, or, exists} => or( cmp('creatorID', 42), exists('comments', c => c.where('creatorID', 42), {flip: true}), ), ) You can manually flip just one or a few of the whereExists clauses in a query, leaving the rest to be planned automatically. Scalar Subqueries Scalar subqueries are an optimization for exists queries. Instead of doing a join at query time, Zero pre-resolves the subquery and rewrites it as a simple equality check. To use scalar subqueries, add a unique index in Postgres and declare it as unique in the Zero schema, then add {scalar: true} to your whereExists call: // Instead of joining to find issues where project.name = 'zero' // Zero resolves this server-side to: where('projectId', '123') zql.issue.whereExists( 'project', q => q.where('name', 'zero'), {scalar: true} ) Or with exists: zql.issue.where({cmp, exists} => exists('project', q => q.where('name', 'zero'), {scalar: true}, ), ) Why It Matters Joins are expensive. Sometimes they are needed, but for something like \"give me all issues where the owner's name is Alice\", you don't need a full join — you just need Alice's ID. The scalar optimization pre-fetches that ID and rewrites your query as where('ownerId', aliceId). This can improve query performance significantly. It also allows planning to work better. Since the ID is known at planning time, Zero/SQLite can choose better indexes. Trade-offs The query needs to be \"rehydrated\" (re-run) whenever the scalar subquery result changes. This is fine for relatively stable lookup data like user IDs or project IDs, but you probably wouldn't want it for rapidly-changing data. Scalar subqueries only work when the subquery is guaranteed to return at most one row. The callback must constrain every column of a primary key or a key declared with .unique(...) using equality. Compound keys require an equality constraint for every column. They also only support one-hop relationships, not junctions. Future Work Scalar subqueries are not currently integrated with Zero's planner. You need to manually choose when to use them.", "headings": [ { "text": "Create a Builder", @@ -9860,10 +10192,34 @@ "text": "Comparing Literal Values", "id": "comparing-literal-values" }, + { + "text": "JSON Filters", + "id": "json-filters" + }, + { + "text": "Comparisons Are Type-Strict", + "id": "comparisons-are-type-strict" + }, + { + "text": "Missing Values", + "id": "missing-values" + }, { "text": "Relationship Filters", "id": "relationship-filters" }, + { + "text": "Aggregates", + "id": "aggregates" + }, + { + "text": "Distinct", + "id": "distinct" + }, + { + "text": "Group By", + "id": "group-by" + }, { "text": "Type Helpers", "id": "type-helpers" @@ -9900,7 +10256,7 @@ "kind": "page" }, { - "id": "723-zql#create-a-builder", + "id": "747-zql#create-a-builder", "title": "ZQL", "searchTitle": "Create a Builder", "sectionTitle": "Create a Builder", @@ -9910,7 +10266,7 @@ "kind": "section" }, { - "id": "724-zql#select", + "id": "748-zql#select", "title": "ZQL", "searchTitle": "Select", "sectionTitle": "Select", @@ -9920,7 +10276,7 @@ "kind": "section" }, { - "id": "725-zql#ordering", + "id": "749-zql#ordering", "title": "ZQL", "searchTitle": "Ordering", "sectionTitle": "Ordering", @@ -9930,7 +10286,7 @@ "kind": "section" }, { - "id": "726-zql#limit", + "id": "750-zql#limit", "title": "ZQL", "searchTitle": "Limit", "sectionTitle": "Limit", @@ -9940,7 +10296,7 @@ "kind": "section" }, { - "id": "727-zql#paging", + "id": "751-zql#paging", "title": "ZQL", "searchTitle": "Paging", "sectionTitle": "Paging", @@ -9950,7 +10306,7 @@ "kind": "section" }, { - "id": "728-zql#getting-a-single-result", + "id": "752-zql#getting-a-single-result", "title": "ZQL", "searchTitle": "Getting a Single Result", "sectionTitle": "Getting a Single Result", @@ -9960,7 +10316,7 @@ "kind": "section" }, { - "id": "729-zql#relationships", + "id": "753-zql#relationships", "title": "ZQL", "searchTitle": "Relationships", "sectionTitle": "Relationships", @@ -9970,7 +10326,7 @@ "kind": "section" }, { - "id": "730-zql#refining-relationships", + "id": "754-zql#refining-relationships", "title": "ZQL", "searchTitle": "Refining Relationships", "sectionTitle": "Refining Relationships", @@ -9980,7 +10336,7 @@ "kind": "section" }, { - "id": "731-zql#nested-relationships", + "id": "755-zql#nested-relationships", "title": "ZQL", "searchTitle": "Nested Relationships", "sectionTitle": "Nested Relationships", @@ -9990,17 +10346,17 @@ "kind": "section" }, { - "id": "732-zql#where", + "id": "756-zql#where", "title": "ZQL", "searchTitle": "Where", "sectionTitle": "Where", "sectionId": "where", "url": "/docs/zql", - "content": "You can filter a query with where(): zql.issue.where('priority', '=', 'high') The first parameter is always a column name from the table being queried. TypeScript completion will offer available options (sourced from your Zero Schema). Comparison Operators where() supports the following comparison operators: TypeScript will restrict you from using operators with types that don’t make sense – you can’t use > with boolean for example. Zero can evaluate an ILIKE filter in three places: in-memory on the client, in ZQLite against the replica, or as SQL in Postgres. ZQLite and the in-memory query engine use Unicode-aware lowercasing before matching. This means common non-ASCII case variants, such as MÜLLER and müller, match in both. If you need non-ASCII text in PG to work with ILIKE, use a Unicode-aware collation for those text columns, such as the database's default UTF-8 locale. Equals is the Default Comparison Operator Because comparing by = is so common, you can leave it out and where defaults to =. zql.issue.where('priority', 'high') Comparing to null As in SQL, ZQL’s null cannot be compared with =, !=, <, or any other normal comparison operator. Comparing any value to null with such operators is always false: These semantics feel a bit weird, but they are consistent with SQL. The reason SQL does it this way is to make join semantics work: if you’re joining employee.orgID on org.id you do not want an employee in no organization to match an org that hasn’t yet been assigned an ID. For when you purposely do want to compare to null ZQL supports IS and IS NOT operators that also work just like in SQL: // Find employees not in any org. zql.employee.where('orgID', 'IS', null) // Find employees in an org other than 42 OR employees in NO org zql.employee.where('orgID', 'IS NOT', 42) TypeScript will prevent you from comparing to null with other operators. Comparing to undefined As a convenience, you can pass undefined to where: zql.issue.where('priority', issue?.priority) This comparison is always false, so the above query always returns no results. Compound Filters The argument to where can also be a callback that returns a complex expression: // Get all issues that have priority 'critical' or else have both // priority 'medium' and not more than 100 votes. zql.issue.where(({cmp, and, or, not}) => or( cmp('priority', 'critical'), and( cmp('priority', 'medium'), not(cmp('numVotes', '>', 100)) ) ) ) cmp is short for compare and works the same as where at the top-level except that it can’t be chained and it only accepts comparison operators (no relationship filters – see below). Note that chaining where() is also a one-level and: // Find issues with priority 3 or higher, owned by aa zql.issue .where('priority', '>=', 3) .where('owner', 'aa') Comparing Literal Values The where clause always expects its first parameter to be a column name as a string. Same with the cmp helper: // \"foo\" is a column name, not a string: zql.issue.where('foo', 'bar') // \"foo\" is a column name, not a string: zql.issue.where(({cmp}) => cmp('foo', 'bar')) To compare to a literal value, use the cmpLit helper: zql.issue.where(cmpLit('foobar', 'foo' + 'bar')) This is particularly useful for implementing permissions, because the first parameter can be a field of your context: zql.issue.where(cmpLit(ctx.role, 'admin')) Relationship Filters Your filter can also test properties of relationships. Currently the only supported test is existence: // Find all orgs that have at least one employee zql.organization.whereExists('employees') The argument to whereExists is a relationship, so just like other relationships, it can be refined with a query: // Find all orgs that have at least one cool employee zql.organization.whereExists('employees', q => q.where('location', 'Hawaii') ) As with querying relationships, relationship filters can be arbitrarily nested: // Get all issues that have comments that have reactions zql.issue.whereExists('comments', q => q.whereExists('reactions') ) The exists helper is also provided which can be used with and, or, cmp, and not to build compound filters that check relationship existence: // Find issues that have at least one comment or are high priority zql.issue.where({cmp, or, exists} => or( cmp('priority', 'high'), exists('comments'), ), )", + "content": "You can filter a query with where(): zql.issue.where('priority', '=', 'high') The first parameter is always a column name from the table being queried. TypeScript completion will offer available options (sourced from your Zero Schema). Comparison Operators where() supports the following comparison operators: TypeScript will restrict you from using operators with types that don’t make sense – you can’t use > with boolean for example. Zero can evaluate an ILIKE filter in three places: in-memory on the client, in ZQLite against the replica, or as SQL in Postgres. ZQLite and the in-memory query engine use Unicode-aware lowercasing before matching. This means common non-ASCII case variants, such as MÜLLER and müller, match in both. If you need non-ASCII text in PG to work with ILIKE, use a Unicode-aware collation for those text columns, such as the database's default UTF-8 locale. Equals is the Default Comparison Operator Because comparing by = is so common, you can leave it out and where defaults to =. zql.issue.where('priority', 'high') Comparing to null As in SQL, ZQL’s null cannot be compared with =, !=, <, or any other normal comparison operator. Comparing any value to null with such operators is always false: These semantics feel a bit weird, but they are consistent with SQL. The reason SQL does it this way is to make join semantics work: if you’re joining employee.orgID on org.id you do not want an employee in no organization to match an org that hasn’t yet been assigned an ID. For when you purposely do want to compare to null ZQL supports IS and IS NOT operators that also work just like in SQL: // Find employees not in any org. zql.employee.where('orgID', 'IS', null) // Find employees in an org other than 42 OR employees in NO org zql.employee.where('orgID', 'IS NOT', 42) TypeScript will prevent you from comparing to null with other operators. Comparing to undefined As a convenience, you can pass undefined to where: zql.issue.where('priority', issue?.priority) This comparison is always false, so the above query always returns no results. Compound Filters The argument to where can also be a callback that returns a complex expression: // Get all issues that have priority 'critical' or else have both // priority 'medium' and not more than 100 votes. zql.issue.where(({cmp, and, or, not}) => or( cmp('priority', 'critical'), and( cmp('priority', 'medium'), not(cmp('numVotes', '>', 100)) ) ) ) cmp is short for compare and works the same as where at the top-level except that it can’t be chained and it only accepts comparison operators (no relationship filters – see below). Note that chaining where() is also a one-level and: // Find issues with priority 3 or higher, owned by aa zql.issue .where('priority', '>=', 3) .where('owner', 'aa') Comparing Literal Values The where clause always expects its first parameter to be a column name as a string. Same with the cmp helper: // \"foo\" is a column name, not a string: zql.issue.where('foo', 'bar') // \"foo\" is a column name, not a string: zql.issue.where(({cmp}) => cmp('foo', 'bar')) To compare to a literal value, use the cmpLit helper: zql.issue.where(cmpLit('foobar', 'foo' + 'bar')) This is particularly useful for implementing permissions, because the first parameter can be a field of your context: zql.issue.where(cmpLit(ctx.role, 'admin')) JSON Filters You can filter on a value inside a json() column with the json helper. Pass it the column name followed by a path into the value: // Find issues whose metadata.priority is 'high' zql.issue.where(({cmp, json}) => cmp(json('metadata', 'priority'), 'high') ) json can only be used inside a callback, as the first argument to cmp. It works with all the comparison operators, and it can be combined with and, or, not, and exists like any other cmp. Path segments are applied left to right. A string segment reads a key of an object and a number segment reads an index of an array: // metadata.address.zip cmp(json('metadata', 'address', 'zip'), '94110') // The first element of metadata.tags cmp(json('metadata', 'tags', 0), 'bug') Array indexes must be non-negative integers. A string never indexes an array and a number never reads an object key, so json('metadata', 'tags', '0') reads as a missing value (see Missing Values). If the column has a custom JSON type, TypeScript completes the path segments and types the value you compare against. A column declared with plain json() accepts any path. Comparisons Are Type-Strict A JSON value doesn't have a fixed type the way a column does, so comparisons follow JS strict equal (===) semantics. A non-null value of a different type than the one you compare against never matches a positive operator and always matches a negated one: LIKE and ILIKE only match string values. Values can be compared against strings, numbers, and booleans, and against null with IS and IS NOT (see Missing Values). You can't compare a whole object or array. The array passed to IN and NOT IN must contain values of a single type: cmp(json('metadata', 'priority'), 'IN', [ 'high', 'critical' ]) Missing Values A path that doesn't exist and a JSON null both read as null. As with columns, they never match =, !=, or the other value operators. Use IS and IS NOT to find them: // Issues with no metadata.priority (missing or null) zql.issue.where(({cmp, json}) => cmp(json('metadata', 'priority'), 'IS', null) ) Relationship Filters Your filter can also test properties of relationships. Currently the only supported test is existence: // Find all orgs that have at least one employee zql.organization.whereExists('employees') The argument to whereExists is a relationship, so just like other relationships, it can be refined with a query: // Find all orgs that have at least one cool employee zql.organization.whereExists('employees', q => q.where('location', 'Hawaii') ) As with querying relationships, relationship filters can be arbitrarily nested: // Get all issues that have comments that have reactions zql.issue.whereExists('comments', q => q.whereExists('reactions') ) The exists helper is also provided which can be used with and, or, cmp, and not to build compound filters that check relationship existence: // Find issues that have at least one comment or are high priority zql.issue.where({cmp, or, exists} => or( cmp('priority', 'high'), exists('comments'), ), )", "kind": "section" }, { - "id": "733-zql#comparison-operators", + "id": "757-zql#comparison-operators", "title": "ZQL", "searchTitle": "Comparison Operators", "sectionTitle": "Comparison Operators", @@ -10010,7 +10366,7 @@ "kind": "section" }, { - "id": "734-zql#equals-is-the-default-comparison-operator", + "id": "758-zql#equals-is-the-default-comparison-operator", "title": "ZQL", "searchTitle": "Equals is the Default Comparison Operator", "sectionTitle": "Equals is the Default Comparison Operator", @@ -10020,7 +10376,7 @@ "kind": "section" }, { - "id": "735-zql#comparing-to-null", + "id": "759-zql#comparing-to-null", "title": "ZQL", "searchTitle": "Comparing to null", "sectionTitle": "Comparing to null", @@ -10030,7 +10386,7 @@ "kind": "section" }, { - "id": "736-zql#comparing-to-undefined", + "id": "760-zql#comparing-to-undefined", "title": "ZQL", "searchTitle": "Comparing to undefined", "sectionTitle": "Comparing to undefined", @@ -10040,7 +10396,7 @@ "kind": "section" }, { - "id": "737-zql#compound-filters", + "id": "761-zql#compound-filters", "title": "ZQL", "searchTitle": "Compound Filters", "sectionTitle": "Compound Filters", @@ -10050,7 +10406,7 @@ "kind": "section" }, { - "id": "738-zql#comparing-literal-values", + "id": "762-zql#comparing-literal-values", "title": "ZQL", "searchTitle": "Comparing Literal Values", "sectionTitle": "Comparing Literal Values", @@ -10060,7 +10416,37 @@ "kind": "section" }, { - "id": "739-zql#relationship-filters", + "id": "763-zql#json-filters", + "title": "ZQL", + "searchTitle": "JSON Filters", + "sectionTitle": "JSON Filters", + "sectionId": "json-filters", + "url": "/docs/zql", + "content": "You can filter on a value inside a json() column with the json helper. Pass it the column name followed by a path into the value: // Find issues whose metadata.priority is 'high' zql.issue.where(({cmp, json}) => cmp(json('metadata', 'priority'), 'high') ) json can only be used inside a callback, as the first argument to cmp. It works with all the comparison operators, and it can be combined with and, or, not, and exists like any other cmp. Path segments are applied left to right. A string segment reads a key of an object and a number segment reads an index of an array: // metadata.address.zip cmp(json('metadata', 'address', 'zip'), '94110') // The first element of metadata.tags cmp(json('metadata', 'tags', 0), 'bug') Array indexes must be non-negative integers. A string never indexes an array and a number never reads an object key, so json('metadata', 'tags', '0') reads as a missing value (see Missing Values). If the column has a custom JSON type, TypeScript completes the path segments and types the value you compare against. A column declared with plain json() accepts any path. Comparisons Are Type-Strict A JSON value doesn't have a fixed type the way a column does, so comparisons follow JS strict equal (===) semantics. A non-null value of a different type than the one you compare against never matches a positive operator and always matches a negated one: LIKE and ILIKE only match string values. Values can be compared against strings, numbers, and booleans, and against null with IS and IS NOT (see Missing Values). You can't compare a whole object or array. The array passed to IN and NOT IN must contain values of a single type: cmp(json('metadata', 'priority'), 'IN', [ 'high', 'critical' ]) Missing Values A path that doesn't exist and a JSON null both read as null. As with columns, they never match =, !=, or the other value operators. Use IS and IS NOT to find them: // Issues with no metadata.priority (missing or null) zql.issue.where(({cmp, json}) => cmp(json('metadata', 'priority'), 'IS', null) )", + "kind": "section" + }, + { + "id": "764-zql#comparisons-are-type-strict", + "title": "ZQL", + "searchTitle": "Comparisons Are Type-Strict", + "sectionTitle": "Comparisons Are Type-Strict", + "sectionId": "comparisons-are-type-strict", + "url": "/docs/zql", + "content": "A JSON value doesn't have a fixed type the way a column does, so comparisons follow JS strict equal (===) semantics. A non-null value of a different type than the one you compare against never matches a positive operator and always matches a negated one: LIKE and ILIKE only match string values. Values can be compared against strings, numbers, and booleans, and against null with IS and IS NOT (see Missing Values). You can't compare a whole object or array. The array passed to IN and NOT IN must contain values of a single type: cmp(json('metadata', 'priority'), 'IN', [ 'high', 'critical' ])", + "kind": "section" + }, + { + "id": "765-zql#missing-values", + "title": "ZQL", + "searchTitle": "Missing Values", + "sectionTitle": "Missing Values", + "sectionId": "missing-values", + "url": "/docs/zql", + "content": "A path that doesn't exist and a JSON null both read as null. As with columns, they never match =, !=, or the other value operators. Use IS and IS NOT to find them: // Issues with no metadata.priority (missing or null) zql.issue.where(({cmp, json}) => cmp(json('metadata', 'priority'), 'IS', null) )", + "kind": "section" + }, + { + "id": "766-zql#relationship-filters", "title": "ZQL", "searchTitle": "Relationship Filters", "sectionTitle": "Relationship Filters", @@ -10070,7 +10456,37 @@ "kind": "section" }, { - "id": "740-zql#type-helpers", + "id": "767-zql#aggregates", + "title": "ZQL", + "searchTitle": "Aggregates", + "sectionTitle": "Aggregates", + "sectionId": "aggregates", + "url": "/docs/zql", + "content": "You can compute count(), sum(), avg(), min(), or max() over a query. Instead of rows, the query returns a single value: // The number of high priority issues zql.issue.where('priority', 'high').count() // The most votes any issue has zql.issue.max('numVotes') Aggregates also work on relationships. Each row then gets the aggregate value in place of the array of related rows: // Each issue has a `comments` field holding its number of comments zql.issue.related('comments', q => q.count()) Zero computes aggregates on the server and syncs only the result, so counting thousands of comments doesn't download them. The value stays up to date as rows change. where(), start(), and limit() apply before the aggregate, just like in SQL, so they have to come before it: zql.issue.orderBy('created', 'desc').limit(10).avg('numVotes') averages the ten newest issues. sum() and avg() take numeric columns, while min() and max() also accept string columns. Over no rows, count() returns 0 and the others return null. To aggregate a query defined with defineQuery(), put the aggregate in its definition. Adding one to the query it returns, as in queries.issues().count(), throws: export const queries = defineQueries({ highPriorityCount: defineQuery(() => zql.issue.where('priority', 'high').count() ) }) When a mutator adds or removes related rows, a relationship's count, sum, or average updates right away, as long as the relationship doesn't go through a junction table or use limit(), start(), or whereExists(). Other aggregates update once the server has run the mutation. Inside a mutator, tx.run() reads an aggregate's synced value if the app also syncs that query, and otherwise computes it from the rows on the client, which can be fewer than the server has. Distinct distinct() returns one row for each distinct combination of the columns you name, as SQL's SELECT DISTINCT does. The rows have only those columns. Following it with count() counts the combinations instead: // The people who created a high priority issue: [{creatorID: 'u1'}, …] zql.issue.where('priority', 'high').distinct('creatorID') // How many of them there are zql.issue .where('priority', 'high') .distinct('creatorID') .count() null is a value like any other: issues with no creator give one row with creatorID: null, and count() counts it as one value. That differs from SQL's COUNT(DISTINCT creatorID), which skips NULL, but it always matches the number of rows distinct() returns. The rows are ordered by the distinct columns, ascending unless orderBy() says otherwise, and orderBy() can only name those columns. Group By groupBy() computes an aggregate for each group of rows that share the values of the columns you name, as SQL's GROUP BY does. It has to be followed by count(), sum(), avg(), min(), or max(), and each row holds the group's columns and the aggregate's value under the function's name: // [{creatorID: 'u1', count: 12}, {creatorID: null, count: 3}, …] zql.issue .where('priority', 'high') .groupBy('creatorID') .count() // [{creatorID: 'u1', priority: 'high', sum: 40}, …] zql.issue.groupBy('creatorID', 'priority').sum('numVotes') The aggregates work as they do without groupBy(), and a group whose values are all null gets null from sum(), avg(), min(), and max(). As with distinct(), rows with a null value form a group of their own, and the rows can only be ordered by the grouped columns. Ordering groups by their aggregate, such as the creators with the most issues first, isn't supported yet. Neither distinct() nor groupBy() can be combined with limit(), start(), one(), related(), or another aggregate yet, and neither can group by a JSON column. They can't be used inside related() or whereExists() either, so counting each issue's distinct commenters isn't possible yet. As with other aggregates, a query defined with defineQuery() has to include them in its definition. When a mutator changes rows, the rows of a distinct() and the values of a groupBy() with count(), sum(), or avg() update right away, as long as the query's where() doesn't use whereExists(). A groupBy() with min() or max(), and distinct().count(), update once the server has run the mutation.", + "kind": "section" + }, + { + "id": "768-zql#distinct", + "title": "ZQL", + "searchTitle": "Distinct", + "sectionTitle": "Distinct", + "sectionId": "distinct", + "url": "/docs/zql", + "content": "distinct() returns one row for each distinct combination of the columns you name, as SQL's SELECT DISTINCT does. The rows have only those columns. Following it with count() counts the combinations instead: // The people who created a high priority issue: [{creatorID: 'u1'}, …] zql.issue.where('priority', 'high').distinct('creatorID') // How many of them there are zql.issue .where('priority', 'high') .distinct('creatorID') .count() null is a value like any other: issues with no creator give one row with creatorID: null, and count() counts it as one value. That differs from SQL's COUNT(DISTINCT creatorID), which skips NULL, but it always matches the number of rows distinct() returns. The rows are ordered by the distinct columns, ascending unless orderBy() says otherwise, and orderBy() can only name those columns.", + "kind": "section" + }, + { + "id": "769-zql#group-by", + "title": "ZQL", + "searchTitle": "Group By", + "sectionTitle": "Group By", + "sectionId": "group-by", + "url": "/docs/zql", + "content": "groupBy() computes an aggregate for each group of rows that share the values of the columns you name, as SQL's GROUP BY does. It has to be followed by count(), sum(), avg(), min(), or max(), and each row holds the group's columns and the aggregate's value under the function's name: // [{creatorID: 'u1', count: 12}, {creatorID: null, count: 3}, …] zql.issue .where('priority', 'high') .groupBy('creatorID') .count() // [{creatorID: 'u1', priority: 'high', sum: 40}, …] zql.issue.groupBy('creatorID', 'priority').sum('numVotes') The aggregates work as they do without groupBy(), and a group whose values are all null gets null from sum(), avg(), min(), and max(). As with distinct(), rows with a null value form a group of their own, and the rows can only be ordered by the grouped columns. Ordering groups by their aggregate, such as the creators with the most issues first, isn't supported yet. Neither distinct() nor groupBy() can be combined with limit(), start(), one(), related(), or another aggregate yet, and neither can group by a JSON column. They can't be used inside related() or whereExists() either, so counting each issue's distinct commenters isn't possible yet. As with other aggregates, a query defined with defineQuery() has to include them in its definition. When a mutator changes rows, the rows of a distinct() and the values of a groupBy() with count(), sum(), or avg() update right away, as long as the query's where() doesn't use whereExists(). A groupBy() with min() or max(), and distinct().count(), update once the server has run the mutation.", + "kind": "section" + }, + { + "id": "770-zql#type-helpers", "title": "ZQL", "searchTitle": "Type Helpers", "sectionTitle": "Type Helpers", @@ -10080,7 +10496,7 @@ "kind": "section" }, { - "id": "741-zql#planning", + "id": "771-zql#planning", "title": "ZQL", "searchTitle": "Planning", "sectionTitle": "Planning", @@ -10090,7 +10506,7 @@ "kind": "section" }, { - "id": "742-zql#inspecting-query-plans", + "id": "772-zql#inspecting-query-plans", "title": "ZQL", "searchTitle": "Inspecting Query Plans", "sectionTitle": "Inspecting Query Plans", @@ -10100,7 +10516,7 @@ "kind": "section" }, { - "id": "743-zql#manually-flipping-joins", + "id": "773-zql#manually-flipping-joins", "title": "ZQL", "searchTitle": "Manually Flipping Joins", "sectionTitle": "Manually Flipping Joins", @@ -10110,7 +10526,7 @@ "kind": "section" }, { - "id": "744-zql#scalar-subqueries", + "id": "774-zql#scalar-subqueries", "title": "ZQL", "searchTitle": "Scalar Subqueries", "sectionTitle": "Scalar Subqueries", @@ -10120,7 +10536,7 @@ "kind": "section" }, { - "id": "745-zql#why-it-matters", + "id": "775-zql#why-it-matters", "title": "ZQL", "searchTitle": "Why It Matters", "sectionTitle": "Why It Matters", @@ -10130,7 +10546,7 @@ "kind": "section" }, { - "id": "746-zql#trade-offs", + "id": "776-zql#trade-offs", "title": "ZQL", "searchTitle": "Trade-offs", "sectionTitle": "Trade-offs", @@ -10140,7 +10556,7 @@ "kind": "section" }, { - "id": "747-zql#future-work", + "id": "777-zql#future-work", "title": "ZQL", "searchTitle": "Future Work", "sectionTitle": "Future Work", diff --git a/contents/docs/release-notes/1.12.mdx b/contents/docs/release-notes/1.12.mdx new file mode 100644 index 00000000..1e3ebc73 --- /dev/null +++ b/contents/docs/release-notes/1.12.mdx @@ -0,0 +1,260 @@ +--- +title: Zero 1.12 +description: Synced Aggregates and JSON Path Filters +--- + +## Installation + +```bash +npm install @rocicorp/zero@1.12 +``` + + + [Deploy `zero-cache` before updating + clients](/docs/self-host#rolling-updates). Zero 1.12 + clients cannot connect to an older server. To roll back, + roll back clients first. In multi-node deployments, + upgrade the replication-manager before the view-syncers, + and roll back the view-syncers first. Then set + [`ZERO_CHANGE_STREAMER_SECRET`](/docs/zero-cache-config#change-streamer-secret) + on the view-syncers, and after that on the + replication-manager. + + +## Overview + +Zero 1.12 syncs aggregates like `count()` and `sum()`, groups rows with `groupBy()` and `distinct()`, and can filter on values inside JSON columns. `zero-cache` hydrates queries and applies updates up to 1.8x faster in our benchmarks, React Native apps start about 3x faster, and each replication-manager now gets its own replication slot. This release also fixes eleven security vulnerabilities, along with a lot of other fixes, especially in replication and connection handling. + +## Security + +Zero 1.12 includes the fixes for the eleven security vulnerabilities fixed in 1.11.5. See the [1.11.5 release notes](/docs/release-notes/1.11#1115) for the advisories. We recommend that everyone upgrade. The fix for client-claimed user IDs is off until you turn it on. See [Server-Validated User IDs](#server-validated-user-ids). + +Some of these fixes change how Zero behaves, so read [Breaking Changes](#breaking-changes) before you upgrade, especially the entries on `userID` and the change-streamer secret. + +## Upgrading + +### Server-Validated User IDs + +`zero-cache` can now refuse a `userID` that your API didn't confirm. To use it: + +1. Return the validated `userID` from your `/query` and `/mutate` endpoints, or `null` for logged-out callers. `handleQueryRequest` and `handleMutateRequest` do this when you [pass them `userID`](/docs/auth#authenticated-client-groups), as recommended since 1.5. +2. Set [`ZERO_AUTH_REQUIRE_SERVER_USER_ID=true`](/docs/zero-cache-config#auth-require-server-user-id). + +Until you do, `zero-cache` trusts the `userID` the client claims and logs a warning. Apps that verify tokens with `ZERO_AUTH_SECRET`, `ZERO_AUTH_JWK` or `ZERO_AUTH_JWKS_URL` don't need this: the token already verifies the `userID`. ([83efa9462](https://github.com/rocicorp/mono/commit/83efa9462), [c1f36d79e](https://github.com/rocicorp/mono/commit/c1f36d79e)) + +## RMv2: Replication-Manager Slots + +We're reworking how replication-managers replicate from Postgres, so that several can run side by side and one can take over from another without a pause. We call this RMv2. Zero 1.12 ships the first step and turns it on by default: each replication-manager now gets its own replication slot. + +A new replication-manager restores its replica from the backup, catches up on its own slot while the old one keeps serving, and then takes over. Slots of replication-managers that are gone are dropped automatically. ([49607b8b3](https://github.com/rocicorp/mono/commit/49607b8b3)) + +A new replication-manager also starts about 20 seconds faster, once both the old and the new one are on 1.12. The new `zero.replication.server_start` metric shows where the start time goes. ([522d2ce57](https://github.com/rocicorp/mono/commit/522d2ce57), [9aca1b0ab](https://github.com/rocicorp/mono/commit/9aca1b0ab)) + +This needs [Litestream v5 backups](/docs/zero-cache-config#litestream-backup-using-v5), the default since 1.11. Expect one slot per running replication-manager in Postgres, each holding WAL until it is consumed. To go back to a shared slot, set [`ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA=false`](/docs/zero-cache-config#upstream-pg-replication-slot-per-replica). That doesn't need a resync. + +Running several replication-managers at once will come in a later release. + +## Features + +- Queries can return a single [aggregate](/docs/zql#aggregates): `zql.issue.where('open', true).count()`, or `zql.issue.max('numVotes')`. A relationship can be reduced the same way, so `related('comments', q => q.count())` gives each issue its number of comments instead of the comments. Only the result is synced, and it stays up to date as rows change. Counts, sums and averages of direct relationships update optimistically when a mutator changes the related rows. `sum()`, `avg()`, `min()` and `max()` can also reduce a value inside a JSON column. ([c9d1b15fc](https://github.com/rocicorp/mono/commit/c9d1b15fc), [e2ecf69ce](https://github.com/rocicorp/mono/commit/e2ecf69ce), [642aece86](https://github.com/rocicorp/mono/commit/642aece86), [523d3b97e](https://github.com/rocicorp/mono/commit/523d3b97e), [8b716b3fe](https://github.com/rocicorp/mono/commit/8b716b3fe)) +- [`distinct()`](/docs/zql#distinct) returns the distinct combinations of some columns, and [`groupBy()`](/docs/zql#group-by) computes an aggregate per group: `zql.issue.groupBy('assigneeID').count()`. Groups appear and disappear optimistically for `count()`, `sum()` and `avg()`. ([effd2da27](https://github.com/rocicorp/mono/commit/effd2da27), [5493ba549](https://github.com/rocicorp/mono/commit/5493ba549), [fe168e640](https://github.com/rocicorp/mono/commit/fe168e640)) +- `where()` can compare a value inside a `json()` column: `cmp(json('metadata', 'priority'), 'high')`. Paths can go through object keys and array indices, comparisons are type-strict, and TypeScript checks the path of a typed JSON column. [JSON path filters](/docs/zql#json-filters) run on the client, in SQLite and in Postgres. ([0a1a0f3d4](https://github.com/rocicorp/mono/commit/0a1a0f3d4)) +- Draining behind a load balancer: a new [`ZERO_PRE_DRAIN_DELAY_MS`](/docs/zero-cache-config#pre-drain-delay-ms) makes `zero-cache` fail its readiness check for a while after `SIGTERM` while still serving, so the load balancer stops routing to it before the drain starts. With sticky sessions, set [`ZERO_AFFINITY_COOKIE_NAME`](/docs/zero-cache-config#affinity-cookie-name) and `zero-cache` expires the cookie on connections it turns away while draining. A new `zero.sync.drain-time` metric estimates how long a view-syncer's drain will take. ([f22ec09b6](https://github.com/rocicorp/mono/commit/f22ec09b6), [f77997349](https://github.com/rocicorp/mono/commit/f77997349), [9a74d2d3c](https://github.com/rocicorp/mono/commit/9a74d2d3c), [04a27634b](https://github.com/rocicorp/mono/commit/04a27634b)) +- Clients retry a failed connection sooner: after 250–500 ms the first time, backing off to the usual 5 seconds by the fifth failure, with jitter so clients that lost the same server don't retry together. A draining `zero-cache` also tells new connections to retry within a second instead of after 5 seconds. ([06622e6ec](https://github.com/rocicorp/mono/commit/06622e6ec), [a5a940321](https://github.com/rocicorp/mono/commit/a5a940321)) +- Replication status events are republished when the replica grows by 1 GiB or 5%, so consumers that size storage from `replicaSize` see growth from ordinary writes. ([76a0a1bd1](https://github.com/rocicorp/mono/commit/76a0a1bd1)) +- Multi-node deployments can authenticate view-syncers to the replication-manager with a shared [`ZERO_CHANGE_STREAMER_SECRET`](/docs/zero-cache-config#change-streamer-secret). A single-node `zero-cache` generates its own. ([b2b8f68e4](https://github.com/rocicorp/mono/commit/b2b8f68e4)) +- [`ZERO_ENABLE_HEAPZ=false`](/docs/zero-cache-config#enable-heapz) turns off the `/heapz` heap-snapshot endpoint while keeping the other diagnostic endpoints and the inspector. ([ea3105333](https://github.com/rocicorp/mono/commit/ea3105333)) + +## Performance + +### Server-Side Hydration + +When a client connects or adds a query, `zero-cache` has to hydrate it: run it against the replica and send the results. In Zero 1.12 this takes about **1.2x less CPU**, and about **1.8x less** for queries with `whereExists`, which are common because permissions use them. We measured 100 client groups on one sync worker, each hydrating its user's top issues with their latest comments. ([6563d5751](https://github.com/rocicorp/mono/commit/6563d5751), [70c771950](https://github.com/rocicorp/mono/commit/70c771950), [c2882cba0](https://github.com/rocicorp/mono/commit/c2882cba0), [5a5fb1af1](https://github.com/rocicorp/mono/commit/5a5fb1af1), [c9c34d145](https://github.com/rocicorp/mono/commit/c9c34d145), [f7233246c](https://github.com/rocicorp/mono/commit/f7233246c), [ae794cc0e](https://github.com/rocicorp/mono/commit/ae794cc0e)) + + + +### Server-Side Incremental Updates + +When a transaction replicates, `zero-cache` has to bring every client group's queries up to date. Zero 1.12 does this with about **1.4x to 1.7x less CPU** than 1.11, which was already 2.5x better than 1.10. We measured the same workload as last release: 100 client groups on one sync worker, each syncing a query with a related subquery. Pushing one write through 300 open queries is about **1.6x faster** too. ([6563d5751](https://github.com/rocicorp/mono/commit/6563d5751), [a35b628ea](https://github.com/rocicorp/mono/commit/a35b628ea), [018a90f86](https://github.com/rocicorp/mono/commit/018a90f86), [f5d11d924](https://github.com/rocicorp/mono/commit/f5d11d924), [8702aea45](https://github.com/rocicorp/mono/commit/8702aea45), [fd30e6437](https://github.com/rocicorp/mono/commit/fd30e6437)) + + + +### React Native Cold Start + +When a React Native app starts, Zero loads the rows it has stored on the device into memory and runs the app's queries over them before anything renders. In Zero 1.12 this is about **3x faster**. Hermes, the JavaScript engine React Native uses, was spending most of that time sorting rows that were already in order. We measured an app with 180,000 stored rows and 21 queries, on the Android emulator and the iOS simulator. ([bfc7e8c24](https://github.com/rocicorp/mono/commit/bfc7e8c24), [e7f8a1d91](https://github.com/rocicorp/mono/commit/e7f8a1d91), [8c21e7960](https://github.com/rocicorp/mono/commit/8c21e7960), [ea4560f4e](https://github.com/rocicorp/mono/commit/ea4560f4e), [e058bfca2](https://github.com/rocicorp/mono/commit/e058bfca2), [6375eeb84](https://github.com/rocicorp/mono/commit/6375eeb84), [855c495b1](https://github.com/rocicorp/mono/commit/855c495b1)) + + + +### Query Planning + +Before `zero-cache` hydrates a query with `whereExists`, it plans which way to run each join. With three to five `whereExists`, or nested ones, planning now takes about **2x to 10x less time**. ([9215b24b2](https://github.com/rocicorp/mono/commit/9215b24b2)) + + + +{/* TODO: CPU with many connections (poke buffer reuse), benchmark still running. */} + +## Fixes + +### Queries and Mutations + +- Server mutators and queries read `time with time zone` columns as negative milliseconds when the zone offset was positive, and a `ServerSchema` written by hand with the short `timestamptz` name read a timestamp as a time of day. ([9a08f1e40](https://github.com/rocicorp/mono/commit/9a08f1e40)) +- Server mutators and queries failed with `type "..." does not exist` for enum and other user-defined Postgres types outside the `public` schema. Casts are now schema-qualified. ([8166580b0](https://github.com/rocicorp/mono/commit/8166580b0)) (thanks [@jschaf](https://github.com/jschaf)!) +- A push that failed at the API server could put other tabs of the same client group into the `Error` state, or fail a socket the client had already replaced. A failed push now affects only the socket that sent it, and that socket's queued pushes are re-sent on the next connection. ([945ce6192](https://github.com/rocicorp/mono/commit/945ce6192)) (thanks [@pawarren](https://github.com/pawarren)!) +- `runAnalyzeCLI --query` no longer evaluates the query text as JavaScript. It accepts query builder calls, literals and arrow functions, and rejects anything else with its position. ([02b6c1bbd](https://github.com/rocicorp/mono/commit/02b6c1bbd)) + +### Clients and React Native + +- On React Native, client garbage collection failed every 5 minutes, and queries whose `related()` entries weren't in alias order threw during normalization, because Hermes lacks `Array.prototype.toSorted`. ([b62ded385](https://github.com/rocicorp/mono/commit/b62ded385)) (thanks [@wikil](https://github.com/wikil)!) +- A SQLite-backed store whose `close()` threw blocked every later `Zero` instance with the same name from opening, and two overlapping `close()` calls could close a shared SQLite connection under another store. ([dee69dc9d](https://github.com/rocicorp/mono/commit/dee69dc9d), [cc861e8f6](https://github.com/rocicorp/mono/commit/cc861e8f6)) (thanks [@wikil](https://github.com/wikil)!) +- An app that refreshed its auth token while the first connection of a new client group was still being validated could be refused for good with `The initConnection message for a new client group must include client schema`. ([874e1ec9d](https://github.com/rocicorp/mono/commit/874e1ec9d)) (thanks [@YevheniiKotyrlo](https://github.com/YevheniiKotyrlo)!) +- After `zero-cache` was rolled back, a client group holding a query the older server could not parse was stuck in an error state until the server rolled forward. The group is now reset and the client starts a fresh one. ([dbf677638](https://github.com/rocicorp/mono/commit/dbf677638)) +- A write that put keys from both the upper Basic Multilingual Plane (U+E000 to U+FFFF) and above it (such as emoji) into the same B-tree node failed with `putMany entries must be sorted and unique`, and a diff could report such a key as removed and re-added instead of changed. ([0e15506e8](https://github.com/rocicorp/mono/commit/0e15506e8)) (thanks [@natew](https://github.com/natew)!) +- Client garbage collection left a store open for every database it inspected. With SQLite, closing `Zero` then no longer closed its database connection, and a custom `StoreProvider` whose `drop` refuses an open store failed every collection. ([bd436ef0d](https://github.com/rocicorp/mono/commit/bd436ef0d)) (thanks [@wikil](https://github.com/wikil)!) +- Client, client group and connection IDs now come from `crypto.getRandomValues` when available instead of `Math.random`. ([e80d6305c](https://github.com/rocicorp/mono/commit/e80d6305c), [c2ebe3df5](https://github.com/rocicorp/mono/commit/c2ebe3df5)) + +### zero-cache Reliability + +- A view-syncer that reloaded a client group's state after an error kept stale row records and query pipelines, which could crash it with `Query … is not in the CVR` or `Unexpected undefined value`, or leave rows that never reached the client. ([1137280b6](https://github.com/rocicorp/mono/commit/1137280b6), [bb348961f](https://github.com/rocicorp/mono/commit/bb348961f), [9e0e85b62](https://github.com/rocicorp/mono/commit/9e0e85b62)) +- With a `statement_timeout` on the CVR database, connecting a client group with many rows could fail with `canceling statement due to statement timeout`. ([892fa553b](https://github.com/rocicorp/mono/commit/892fa553b)) +- View-syncers on slow or throttled disks spent up to a minute inside a single commit running SQLite checkpoints, and were dropped by the replication-manager as slow subscribers. Serving replicas are now checkpointed by a separate process. ([fbdb27ed5](https://github.com/rocicorp/mono/commit/fbdb27ed5)) +- Clients were disabled and reloaded with `SchemaVersionNotSupported` while an upstream migration backfilled a table or column. They now retry with backoff until the backfill is done. ([8280af82f](https://github.com/rocicorp/mono/commit/8280af82f)) +- Malformed client messages, such as a second `initConnection` on a socket, an invalid `baseCookie` or an invalid query registration, were logged as internal errors. They are now client errors logged at `warn`. ([e1c39de03](https://github.com/rocicorp/mono/commit/e1c39de03), [f452e99b5](https://github.com/rocicorp/mono/commit/f452e99b5), [512baff4b](https://github.com/rocicorp/mono/commit/512baff4b)) +- A client group stays with the user it was first opened for across view-syncer restarts; a connection as a different user is refused the same way it already was while the group was live. ([8d9466b13](https://github.com/rocicorp/mono/commit/8d9466b13)) +- Inspector authentication is per connection, and the 1.12 client remembers a password the server accepted, so it doesn't prompt again after reconnecting. Repeated wrong admin or operator passwords are now rate limited, on the inspector and on the HTTP diagnostic endpoints. ([e186c0d59](https://github.com/rocicorp/mono/commit/e186c0d59), [c767ac31c](https://github.com/rocicorp/mono/commit/c767ac31c), [65375a770](https://github.com/rocicorp/mono/commit/65375a770)) +- `zero-cache` logs and traces show a hash of the `clientGroupID` instead of the ID. ([adfce3301](https://github.com/rocicorp/mono/commit/adfce3301), [de9121d99](https://github.com/rocicorp/mono/commit/de9121d99)) + +### Replication + +- A `numeric`, `float4` or `float8` value between about 2^63 and 1e21 stopped replication with a `RangeError`, a `numeric` between 2^53 and 2^63 was stored as the wrong integer, and such a value in a row key or query literal failed the client group after a reload. These values now replicate as doubles. ([340215ab5](https://github.com/rocicorp/mono/commit/340215ab5)) +- A `NULL` element in a `timestamp[]`, `json[]` or `int8[]` column stopped replication, and in text, enum, boolean and numeric arrays it was stored as `"NULL"`, `false` or `null`. Array columns are now parsed correctly, including arrays with explicit bounds. Rows already stored keep their old values until they change. ([fe4bc732a](https://github.com/rocicorp/mono/commit/fe4bc732a)) +- A schema-change or lag-report message on the replication stream that `zero-cache` could not parse stopped replication. Such messages, and schema-change messages that its own upstream functions didn't emit, are now skipped. ([08c634549](https://github.com/rocicorp/mono/commit/08c634549), [f6010bb34](https://github.com/rocicorp/mono/commit/f6010bb34)) +- With [Litestream v5 backups](/docs/zero-cache-config#litestream-backup-using-v5) on Google Cloud Storage or Azure, backup files were never compacted, so old ones were never deleted and the backup kept growing. ([e2f030e63](https://github.com/rocicorp/mono/commit/e2f030e63)) + +## Breaking Changes + +- **`analyze-query` bin removed:** `npx analyze-query` is gone. Use a script that calls [`runAnalyzeCLI`](/docs/debug/analyze-query-cli). `@rocicorp/zero` now depends on `oxc-parser`, which installs a native binding for your platform. ([a3932f51e](https://github.com/rocicorp/mono/commit/a3932f51e), [02b6c1bbd](https://github.com/rocicorp/mono/commit/02b6c1bbd)) +- **`userID` needs a token:** when `ZERO_AUTH_SECRET`, `ZERO_AUTH_JWK` or `ZERO_AUTH_JWKS_URL` is set, a connection that passes a `userID` without an `auth` token is refused with `Unauthorized`. Omit `userID` for logged-out users; the deprecated `userID: 'anon'` without `auth` is refused too. ([522688120](https://github.com/rocicorp/mono/commit/522688120)) +- **Changed defaults:** [`ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA`](/docs/zero-cache-config#upstream-pg-replication-slot-per-replica) is on. It needs Litestream v5 backups, the default since 1.11, and each replication-manager holds its own slot while it runs. Turning it off doesn't resync. ([49607b8b3](https://github.com/rocicorp/mono/commit/49607b8b3)) +- **Multi-node secret:** set [`ZERO_CHANGE_STREAMER_SECRET`](/docs/zero-cache-config#change-streamer-secret) on the view-syncers first, then on the replication-manager. Until you do, the replication-manager still accepts view-syncers without it, but warns at startup. If one of your nodes runs sync workers and is also the replication-manager for other view-syncers, it now makes up its own secret, so set the secret on every node. ([b2b8f68e4](https://github.com/rocicorp/mono/commit/b2b8f68e4)) +- **Legacy CRUD mutations** are keyed by the primary key of the replica's table, not by the key the client sends. This only matters if your client schema declares a different primary key than the table has in Postgres. ([f24fddae7](https://github.com/rocicorp/mono/commit/f24fddae7)) +- **Rollback:** clients keep their local database and unsent mutations when moving between 1.11 and 1.12 in either direction. One exception: a client group that registered an aggregate, `groupBy`, `distinct` or JSON path query is reset when `zero-cache` is rolled back to 1.11, and its unsent mutations are lost. ([8b716b3fe](https://github.com/rocicorp/mono/commit/8b716b3fe), [dbf677638](https://github.com/rocicorp/mono/commit/dbf677638)) diff --git a/contents/docs/release-notes/index.mdx b/contents/docs/release-notes/index.mdx index b7ac9238..8b01a2af 100644 --- a/contents/docs/release-notes/index.mdx +++ b/contents/docs/release-notes/index.mdx @@ -2,6 +2,7 @@ title: Release Notes --- +- [Zero 1.12: Synced Aggregates and JSON Path Filters](/docs/release-notes/1.12) - [Zero 1.11: Cached Results and Sturdier Client Storage](/docs/release-notes/1.11) - [Zero 1.10: Unique Keys and More Reliable Replication](/docs/release-notes/1.10) - [Zero 1.9: Stability and Query Correctness](/docs/release-notes/1.9) diff --git a/contents/docs/zero-cache-config.mdx b/contents/docs/zero-cache-config.mdx index 1b361a8d..b1987eea 100644 --- a/contents/docs/zero-cache-config.mdx +++ b/contents/docs/zero-cache-config.mdx @@ -30,6 +30,32 @@ required: in production (when `NODE_ENV=production`) ## Optional Flags +### Affinity Cookie Name + +The name of your load balancer's session affinity (sticky session) cookie. When a draining `zero-cache` turns away a new connection, it expires this cookie in the WebSocket upgrade response, so the client's next attempt can be routed to a different instance. + +Without it, a draining `zero-cache` still tells the client to reconnect, but leaves its cookies alone, and a load balancer with sticky sessions can keep sending the client back to the instance that is shutting down. + +The name, [path](#affinity-cookie-path) and [domain](#affinity-cookie-domain) must match the cookie your load balancer sets. The cookie is expired with `Secure` and `SameSite=None`, so that it also works for clients on another site over HTTPS. + +flag: `--affinity-cookie-name`
+env: `ZERO_AFFINITY_COOKIE_NAME`
+ +### Affinity Cookie Path + +The path of the [affinity cookie](#affinity-cookie-name) to expire while draining. + +flag: `--affinity-cookie-path`
+env: `ZERO_AFFINITY_COOKIE_PATH`
+default: `/` + +### Affinity Cookie Domain + +The domain of the [affinity cookie](#affinity-cookie-name) to expire while draining. Leave it unset for a host-only cookie. + +flag: `--affinity-cookie-domain`
+env: `ZERO_AFFINITY_COOKIE_DOMAIN`
+ ### Allow Legacy Queries Allows clients to send query ASTs directly to `zero-cache`. [Ad-hoc queries](/docs/deprecated/ad-hoc-queries) (also called legacy queries) work this way. @@ -647,6 +673,18 @@ flag: `--port`
env: `ZERO_PORT`
default: `4848` +### Pre-Drain Delay (ms) + +The time between receiving a `SIGTERM` (or `SIGINT`) and starting to drain. + +During this time `zero-cache` fails its readiness check at `/`, so load balancers stop routing to it, while it keeps accepting and serving connections as usual. The liveness check at `/keepalive` keeps succeeding. + +This takes the place of a pre-stop delay. Set it longer than the time your load balancer needs to notice the failed readiness check, and keep it within your container runner's termination grace period. With the default of `0`, draining starts immediately. + +flag: `--pre-drain-delay-ms`
+env: `ZERO_PRE_DRAIN_DELAY_MS`
+default: `0` + ### 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. @@ -844,6 +882,20 @@ flag: `--upstream-pg-replication-slot-failover`
env: `ZERO_UPSTREAM_PG_REPLICATION_SLOT_FAILOVER`
default: `false` +### Upstream PG Replication Slot Per Replica + +Create a dedicated replication slot for each replication-manager, instead of having a new replication-manager take over the slot of the one it replaces. + +A new replication-manager restores its replica from the backup, creates its own slot, and catches up while the previous replication-manager keeps serving. It takes over once it has caught up. Expect one slot per running replication-manager in Postgres, each holding WAL until it is consumed. + +This requires [Litestream v5 backups](#litestream-backup-using-v5). The backup and the change DB stay compatible with a shared slot, so setting this to `false` does not cause a resync. + +This is the first step towards a high-availability mode in which several replication-managers run at the same time. Running more than one replication-manager is not supported yet. + +flag: `--upstream-pg-replication-slot-per-replica`
+env: `ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA`
+default: `true` + ### 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`. diff --git a/contents/docs/zql.mdx b/contents/docs/zql.mdx index 6bce3748..05acfdc2 100644 --- a/contents/docs/zql.mdx +++ b/contents/docs/zql.mdx @@ -333,6 +333,67 @@ This is particularly useful for implementing [permissions](/docs/auth#read-permi zql.issue.where(cmpLit(ctx.role, 'admin')) ``` +### JSON Filters + +You can filter on a value inside a [`json()` column](/docs/schema#custom-json-types) with the `json` helper. Pass it the column name followed by a path into the value: + +```tsx +// Find issues whose metadata.priority is 'high' +zql.issue.where(({cmp, json}) => + cmp(json('metadata', 'priority'), 'high') +) +``` + +`json` can only be used inside a callback, as the first argument to `cmp`. It works with all the comparison operators, and it can be combined with `and`, `or`, `not`, and `exists` like any other `cmp`. + +Path segments are applied left to right. A string segment reads a key of an object and a number segment reads an index of an array: + +```tsx +// metadata.address.zip +cmp(json('metadata', 'address', 'zip'), '94110') + +// The first element of metadata.tags +cmp(json('metadata', 'tags', 0), 'bug') +``` + +Array indexes must be non-negative integers. A string never indexes an array and a number never reads an object key, so `json('metadata', 'tags', '0')` reads as a missing value (see [Missing Values](#missing-values)). + +If the column has a [custom JSON type](/docs/schema#custom-json-types), TypeScript completes the path segments and types the value you compare against. A column declared with plain `json()` accepts any path. + +#### Comparisons Are Type-Strict + +A JSON value doesn't have a fixed type the way a column does, so comparisons follow JS strict equal (===) semantics. A non-null value of a different type than the one you compare against never matches a positive operator and always matches a negated one: + +| Value at path | Filter | Result | +| ------------- | --------- | ------- | +| `42` | `= 42` | `true` | +| `42` | `= '42'` | `false` | +| `42` | `!= '42'` | `true` | +| `true` | `= 1` | `false` | +| `'abc'` | `> 5` | `false` | + +`LIKE` and `ILIKE` only match string values. Values can be compared against strings, numbers, and booleans, and against `null` with `IS` and `IS NOT` (see [Missing Values](#missing-values)). You can't compare a whole object or array. + +The array passed to `IN` and `NOT IN` must contain values of a single type: + +```tsx +cmp(json('metadata', 'priority'), 'IN', [ + 'high', + 'critical' +]) +``` + +#### Missing Values + +A path that doesn't exist and a JSON `null` both read as `null`. As with columns, they never match `=`, `!=`, or the other value operators. Use `IS` and `IS NOT` to find them: + +```tsx +// Issues with no metadata.priority (missing or null) +zql.issue.where(({cmp, json}) => + cmp(json('metadata', 'priority'), 'IS', null) +) +``` + ### Relationship Filters Your filter can also test properties of relationships. Currently the only supported test is existence: @@ -372,6 +433,79 @@ zql.issue.where({cmp, or, exists} => ) ``` +## Aggregates + +You can compute `count()`, `sum()`, `avg()`, `min()`, or `max()` over a query. Instead of rows, the query returns a single value: + +```tsx +// The number of high priority issues +zql.issue.where('priority', 'high').count() + +// The most votes any issue has +zql.issue.max('numVotes') +``` + +Aggregates also work on relationships. Each row then gets the aggregate value in place of the array of related rows: + +```tsx +// Each issue has a `comments` field holding its number of comments +zql.issue.related('comments', q => q.count()) +``` + +Zero computes aggregates on the server and syncs only the result, so counting thousands of comments doesn't download them. The value stays up to date as rows change. `where()`, `start()`, and `limit()` apply before the aggregate, just like in SQL, so they have to come before it: `zql.issue.orderBy('created', 'desc').limit(10).avg('numVotes')` averages the ten newest issues. + +`sum()` and `avg()` take numeric columns, while `min()` and `max()` also accept string columns. Over no rows, `count()` returns `0` and the others return `null`. + +To aggregate a query defined with [`defineQuery()`](./queries), put the aggregate in its definition. Adding one to the query it returns, as in `queries.issues().count()`, throws: + +```ts +export const queries = defineQueries({ + highPriorityCount: defineQuery(() => + zql.issue.where('priority', 'high').count() + ) +}) +``` + +When a mutator adds or removes related rows, a relationship's count, sum, or average updates right away, as long as the relationship doesn't go through a junction table or use `limit()`, `start()`, or `whereExists()`. Other aggregates update once the server has run the mutation. Inside a mutator, `tx.run()` reads an aggregate's synced value if the app also syncs that query, and otherwise computes it from the rows on the client, which can be fewer than the server has. + +### Distinct + +`distinct()` returns one row for each distinct combination of the columns you name, as SQL's `SELECT DISTINCT` does. The rows have only those columns. Following it with `count()` counts the combinations instead: + +```tsx +// The people who created a high priority issue: [{creatorID: 'u1'}, …] +zql.issue.where('priority', 'high').distinct('creatorID') + +// How many of them there are +zql.issue + .where('priority', 'high') + .distinct('creatorID') + .count() +``` + +`null` is a value like any other: issues with no creator give one row with `creatorID: null`, and `count()` counts it as one value. That differs from SQL's `COUNT(DISTINCT creatorID)`, which skips `NULL`, but it always matches the number of rows `distinct()` returns. The rows are ordered by the distinct columns, ascending unless `orderBy()` says otherwise, and `orderBy()` can only name those columns. + +### Group By + +`groupBy()` computes an aggregate for each group of rows that share the values of the columns you name, as SQL's `GROUP BY` does. It has to be followed by `count()`, `sum()`, `avg()`, `min()`, or `max()`, and each row holds the group's columns and the aggregate's value under the function's name: + +```tsx +// [{creatorID: 'u1', count: 12}, {creatorID: null, count: 3}, …] +zql.issue + .where('priority', 'high') + .groupBy('creatorID') + .count() + +// [{creatorID: 'u1', priority: 'high', sum: 40}, …] +zql.issue.groupBy('creatorID', 'priority').sum('numVotes') +``` + +The aggregates work as they do without `groupBy()`, and a group whose values are all `null` gets `null` from `sum()`, `avg()`, `min()`, and `max()`. As with `distinct()`, rows with a `null` value form a group of their own, and the rows can only be ordered by the grouped columns. Ordering groups by their aggregate, such as the creators with the most issues first, isn't supported yet. + +Neither `distinct()` nor `groupBy()` can be combined with `limit()`, `start()`, `one()`, `related()`, or another aggregate yet, and neither can group by a JSON column. They can't be used inside `related()` or `whereExists()` either, so counting each issue's distinct commenters isn't possible yet. As with other aggregates, a query defined with `defineQuery()` has to include them in its definition. + +When a mutator changes rows, the rows of a `distinct()` and the values of a `groupBy()` with `count()`, `sum()`, or `avg()` update right away, as long as the query's `where()` doesn't use `whereExists()`. A `groupBy()` with `min()` or `max()`, and `distinct().count()`, update once the server has run the mutation. + ## Type Helpers You can get the TypeScript type of the result of a query using the `QueryResultType` helper: From 214d9ff3c55e982f62f1f259397b104252e0a2e3 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Thu, 12 Nov 2026 10:13:36 +0100 Subject: [PATCH 2/3] docs: add the many-connections CPU result to the 1.12 notes --- assets/search-index.json | 610 ++++++++++++++------------- contents/docs/release-notes/1.12.mdx | 31 +- 2 files changed, 342 insertions(+), 299 deletions(-) diff --git a/assets/search-index.json b/assets/search-index.json index 39aa1ff9..ba733faf 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -6019,7 +6019,7 @@ "title": "Zero 1.12", "searchTitle": "Zero 1.12", "url": "/docs/release-notes/1.12", - "content": "Installation npm install @rocicorp/zero@1.12 Deploy zero-cache before updating clients. Zero 1.12 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Then set ZERO_CHANGE_STREAMER_SECRET on the view-syncers, and after that on the replication-manager. Overview Zero 1.12 syncs aggregates like count() and sum(), groups rows with groupBy() and distinct(), and can filter on values inside JSON columns. zero-cache hydrates queries and applies updates up to 1.8x faster in our benchmarks, React Native apps start about 3x faster, and each replication-manager now gets its own replication slot. This release also fixes eleven security vulnerabilities, along with a lot of other fixes, especially in replication and connection handling. Security Zero 1.12 includes the fixes for the eleven security vulnerabilities fixed in 1.11.5. See the 1.11.5 release notes for the advisories. We recommend that everyone upgrade. The fix for client-claimed user IDs is off until you turn it on. See Server-Validated User IDs. Some of these fixes change how Zero behaves, so read Breaking Changes before you upgrade, especially the entries on userID and the change-streamer secret. Upgrading Server-Validated User IDs zero-cache can now refuse a userID that your API didn't confirm. To use it: Return the validated userID from your /query and /mutate endpoints, or null for logged-out callers. handleQueryRequest and handleMutateRequest do this when you pass them userID, as recommended since 1.5. Set ZERO_AUTH_REQUIRE_SERVER_USER_ID=true. Until you do, zero-cache trusts the userID the client claims and logs a warning. Apps that verify tokens with ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL don't need this: the token already verifies the userID. (83efa9462, c1f36d79e) RMv2: Replication-Manager Slots We're reworking how replication-managers replicate from Postgres, so that several can run side by side and one can take over from another without a pause. We call this RMv2. Zero 1.12 ships the first step and turns it on by default: each replication-manager now gets its own replication slot. A new replication-manager restores its replica from the backup, catches up on its own slot while the old one keeps serving, and then takes over. Slots of replication-managers that are gone are dropped automatically. (49607b8b3) A new replication-manager also starts about 20 seconds faster, once both the old and the new one are on 1.12. The new zero.replication.server_start metric shows where the start time goes. (522d2ce57, 9aca1b0ab) This needs Litestream v5 backups, the default since 1.11. Expect one slot per running replication-manager in Postgres, each holding WAL until it is consumed. To go back to a shared slot, set ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA=false. That doesn't need a resync. Running several replication-managers at once will come in a later release. Features Queries can return a single aggregate: zql.issue.where('open', true).count(), or zql.issue.max('numVotes'). A relationship can be reduced the same way, so related('comments', q => q.count()) gives each issue its number of comments instead of the comments. Only the result is synced, and it stays up to date as rows change. Counts, sums and averages of direct relationships update optimistically when a mutator changes the related rows. sum(), avg(), min() and max() can also reduce a value inside a JSON column. (c9d1b15fc, e2ecf69ce, 642aece86, 523d3b97e, 8b716b3fe) distinct() returns the distinct combinations of some columns, and groupBy() computes an aggregate per group: zql.issue.groupBy('assigneeID').count(). Groups appear and disappear optimistically for count(), sum() and avg(). (effd2da27, 5493ba549, fe168e640) where() can compare a value inside a json() column: cmp(json('metadata', 'priority'), 'high'). Paths can go through object keys and array indices, comparisons are type-strict, and TypeScript checks the path of a typed JSON column. JSON path filters run on the client, in SQLite and in Postgres. (0a1a0f3d4) Draining behind a load balancer: a new ZERO_PRE_DRAIN_DELAY_MS makes zero-cache fail its readiness check for a while after SIGTERM while still serving, so the load balancer stops routing to it before the drain starts. With sticky sessions, set ZERO_AFFINITY_COOKIE_NAME and zero-cache expires the cookie on connections it turns away while draining. A new zero.sync.drain-time metric estimates how long a view-syncer's drain will take. (f22ec09b6, f77997349, 9a74d2d3c, 04a27634b) Clients retry a failed connection sooner: after 250–500 ms the first time, backing off to the usual 5 seconds by the fifth failure, with jitter so clients that lost the same server don't retry together. A draining zero-cache also tells new connections to retry within a second instead of after 5 seconds. (06622e6ec, a5a940321) Replication status events are republished when the replica grows by 1 GiB or 5%, so consumers that size storage from replicaSize see growth from ordinary writes. (76a0a1bd1) Multi-node deployments can authenticate view-syncers to the replication-manager with a shared ZERO_CHANGE_STREAMER_SECRET. A single-node zero-cache generates its own. (b2b8f68e4) ZERO_ENABLE_HEAPZ=false turns off the /heapz heap-snapshot endpoint while keeping the other diagnostic endpoints and the inspector. (ea3105333) Performance Server-Side Hydration When a client connects or adds a query, zero-cache has to hydrate it: run it against the replica and send the results. In Zero 1.12 this takes about 1.2x less CPU, and about 1.8x less for queries with whereExists, which are common because permissions use them. We measured 100 client groups on one sync worker, each hydrating its user's top issues with their latest comments. (6563d5751, 70c771950, c2882cba0, 5a5fb1af1, c9c34d145, f7233246c, ae794cc0e) Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.12 does this with about 1.4x to 1.7x less CPU than 1.11, which was already 2.5x better than 1.10. We measured the same workload as last release: 100 client groups on one sync worker, each syncing a query with a related subquery. Pushing one write through 300 open queries is about 1.6x faster too. (6563d5751, a35b628ea, 018a90f86, f5d11d924, 8702aea45, fd30e6437) React Native Cold Start When a React Native app starts, Zero loads the rows it has stored on the device into memory and runs the app's queries over them before anything renders. In Zero 1.12 this is about 3x faster. Hermes, the JavaScript engine React Native uses, was spending most of that time sorting rows that were already in order. We measured an app with 180,000 stored rows and 21 queries, on the Android emulator and the iOS simulator. (bfc7e8c24, e7f8a1d91, 8c21e7960, ea4560f4e, e058bfca2, 6375eeb84, 855c495b1) Query Planning Before zero-cache hydrates a query with whereExists, it plans which way to run each join. With three to five whereExists, or nested ones, planning now takes about 2x to 10x less time. (9215b24b2) Fixes Queries and Mutations Server mutators and queries read time with time zone columns as negative milliseconds when the zone offset was positive, and a ServerSchema written by hand with the short timestamptz name read a timestamp as a time of day. (9a08f1e40) Server mutators and queries failed with type \"...\" does not exist for enum and other user-defined Postgres types outside the public schema. Casts are now schema-qualified. (8166580b0) (thanks @jschaf!) A push that failed at the API server could put other tabs of the same client group into the Error state, or fail a socket the client had already replaced. A failed push now affects only the socket that sent it, and that socket's queued pushes are re-sent on the next connection. (945ce6192) (thanks @pawarren!) runAnalyzeCLI --query no longer evaluates the query text as JavaScript. It accepts query builder calls, literals and arrow functions, and rejects anything else with its position. (02b6c1bbd) Clients and React Native On React Native, client garbage collection failed every 5 minutes, and queries whose related() entries weren't in alias order threw during normalization, because Hermes lacks Array.prototype.toSorted. (b62ded385) (thanks @wikil!) A SQLite-backed store whose close() threw blocked every later Zero instance with the same name from opening, and two overlapping close() calls could close a shared SQLite connection under another store. (dee69dc9d, cc861e8f6) (thanks @wikil!) An app that refreshed its auth token while the first connection of a new client group was still being validated could be refused for good with The initConnection message for a new client group must include client schema. (874e1ec9d) (thanks @YevheniiKotyrlo!) After zero-cache was rolled back, a client group holding a query the older server could not parse was stuck in an error state until the server rolled forward. The group is now reset and the client starts a fresh one. (dbf677638) A write that put keys from both the upper Basic Multilingual Plane (U+E000 to U+FFFF) and above it (such as emoji) into the same B-tree node failed with putMany entries must be sorted and unique, and a diff could report such a key as removed and re-added instead of changed. (0e15506e8) (thanks @natew!) Client garbage collection left a store open for every database it inspected. With SQLite, closing Zero then no longer closed its database connection, and a custom StoreProvider whose drop refuses an open store failed every collection. (bd436ef0d) (thanks @wikil!) Client, client group and connection IDs now come from crypto.getRandomValues when available instead of Math.random. (e80d6305c, c2ebe3df5) zero-cache Reliability A view-syncer that reloaded a client group's state after an error kept stale row records and query pipelines, which could crash it with Query … is not in the CVR or Unexpected undefined value, or leave rows that never reached the client. (1137280b6, bb348961f, 9e0e85b62) With a statement_timeout on the CVR database, connecting a client group with many rows could fail with canceling statement due to statement timeout. (892fa553b) View-syncers on slow or throttled disks spent up to a minute inside a single commit running SQLite checkpoints, and were dropped by the replication-manager as slow subscribers. Serving replicas are now checkpointed by a separate process. (fbdb27ed5) Clients were disabled and reloaded with SchemaVersionNotSupported while an upstream migration backfilled a table or column. They now retry with backoff until the backfill is done. (8280af82f) Malformed client messages, such as a second initConnection on a socket, an invalid baseCookie or an invalid query registration, were logged as internal errors. They are now client errors logged at warn. (e1c39de03, f452e99b5, 512baff4b) A client group stays with the user it was first opened for across view-syncer restarts; a connection as a different user is refused the same way it already was while the group was live. (8d9466b13) Inspector authentication is per connection, and the 1.12 client remembers a password the server accepted, so it doesn't prompt again after reconnecting. Repeated wrong admin or operator passwords are now rate limited, on the inspector and on the HTTP diagnostic endpoints. (e186c0d59, c767ac31c, 65375a770) zero-cache logs and traces show a hash of the clientGroupID instead of the ID. (adfce3301, de9121d99) Replication A numeric, float4 or float8 value between about 2^63 and 1e21 stopped replication with a RangeError, a numeric between 2^53 and 2^63 was stored as the wrong integer, and such a value in a row key or query literal failed the client group after a reload. These values now replicate as doubles. (340215ab5) A NULL element in a timestamp[], json[] or int8[] column stopped replication, and in text, enum, boolean and numeric arrays it was stored as \"NULL\", false or null. Array columns are now parsed correctly, including arrays with explicit bounds. Rows already stored keep their old values until they change. (fe4bc732a) A schema-change or lag-report message on the replication stream that zero-cache could not parse stopped replication. Such messages, and schema-change messages that its own upstream functions didn't emit, are now skipped. (08c634549, f6010bb34) With Litestream v5 backups on Google Cloud Storage or Azure, backup files were never compacted, so old ones were never deleted and the backup kept growing. (e2f030e63) Breaking Changes analyze-query bin removed: npx analyze-query is gone. Use a script that calls runAnalyzeCLI. @rocicorp/zero now depends on oxc-parser, which installs a native binding for your platform. (a3932f51e, 02b6c1bbd) userID needs a token: when ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL is set, a connection that passes a userID without an auth token is refused with Unauthorized. Omit userID for logged-out users; the deprecated userID: 'anon' without auth is refused too. (522688120) Changed defaults: ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA is on. It needs Litestream v5 backups, the default since 1.11, and each replication-manager holds its own slot while it runs. Turning it off doesn't resync. (49607b8b3) Multi-node secret: set ZERO_CHANGE_STREAMER_SECRET on the view-syncers first, then on the replication-manager. Until you do, the replication-manager still accepts view-syncers without it, but warns at startup. If one of your nodes runs sync workers and is also the replication-manager for other view-syncers, it now makes up its own secret, so set the secret on every node. (b2b8f68e4) Legacy CRUD mutations are keyed by the primary key of the replica's table, not by the key the client sends. This only matters if your client schema declares a different primary key than the table has in Postgres. (f24fddae7) Rollback: clients keep their local database and unsent mutations when moving between 1.11 and 1.12 in either direction. One exception: a client group that registered an aggregate, groupBy, distinct or JSON path query is reset when zero-cache is rolled back to 1.11, and its unsent mutations are lost. (8b716b3fe, dbf677638)", + "content": "Installation npm install @rocicorp/zero@1.12 Deploy zero-cache before updating clients. Zero 1.12 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Then set ZERO_CHANGE_STREAMER_SECRET on the view-syncers, and after that on the replication-manager. Overview Zero 1.12 syncs aggregates like count() and sum(), groups rows with groupBy() and distinct(), and can filter on values inside JSON columns. zero-cache hydrates queries and applies updates up to 1.8x faster in our benchmarks, React Native apps start about 3x faster, and each replication-manager now gets its own replication slot. This release also fixes eleven security vulnerabilities, along with a lot of other fixes, especially in replication and connection handling. Security Zero 1.12 includes the fixes for the eleven security vulnerabilities fixed in 1.11.5. See the 1.11.5 release notes for the advisories. We recommend that everyone upgrade. The fix for client-claimed user IDs is off until you turn it on. See Server-Validated User IDs. Some of these fixes change how Zero behaves, so read Breaking Changes before you upgrade, especially the entries on userID and the change-streamer secret. Upgrading Server-Validated User IDs zero-cache can now refuse a userID that your API didn't confirm. To use it: Return the validated userID from your /query and /mutate endpoints, or null for logged-out callers. handleQueryRequest and handleMutateRequest do this when you pass them userID, as recommended since 1.5. Set ZERO_AUTH_REQUIRE_SERVER_USER_ID=true. Until you do, zero-cache trusts the userID the client claims and logs a warning. Apps that verify tokens with ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL don't need this: the token already verifies the userID. (83efa9462, c1f36d79e) RMv2: Replication-Manager Slots We're reworking how replication-managers replicate from Postgres, so that several can run side by side and one can take over from another without a pause. We call this RMv2. Zero 1.12 ships the first step and turns it on by default: each replication-manager now gets its own replication slot. A new replication-manager restores its replica from the backup, catches up on its own slot while the old one keeps serving, and then takes over. Slots of replication-managers that are gone are dropped automatically. (49607b8b3) A new replication-manager also starts about 20 seconds faster, once both the old and the new one are on 1.12. The new zero.replication.server_start metric shows where the start time goes. (522d2ce57, 9aca1b0ab) This needs Litestream v5 backups, the default since 1.11. Expect one slot per running replication-manager in Postgres, each holding WAL until it is consumed. To go back to a shared slot, set ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA=false. That doesn't need a resync. Running several replication-managers at once will come in a later release. Features Queries can return a single aggregate: zql.issue.where('open', true).count(), or zql.issue.max('numVotes'). A relationship can be reduced the same way, so related('comments', q => q.count()) gives each issue its number of comments instead of the comments. Only the result is synced, and it stays up to date as rows change. Counts, sums and averages of direct relationships update optimistically when a mutator changes the related rows. sum(), avg(), min() and max() can also reduce a value inside a JSON column. (c9d1b15fc, e2ecf69ce, 642aece86, 523d3b97e, 8b716b3fe) distinct() returns the distinct combinations of some columns, and groupBy() computes an aggregate per group: zql.issue.groupBy('assigneeID').count(). Groups appear and disappear optimistically for count(), sum() and avg(). (effd2da27, 5493ba549, fe168e640) where() can compare a value inside a json() column: cmp(json('metadata', 'priority'), 'high'). Paths can go through object keys and array indices, comparisons are type-strict, and TypeScript checks the path of a typed JSON column. JSON path filters run on the client, in SQLite and in Postgres. (0a1a0f3d4) Draining behind a load balancer: a new ZERO_PRE_DRAIN_DELAY_MS makes zero-cache fail its readiness check for a while after SIGTERM while still serving, so the load balancer stops routing to it before the drain starts. With sticky sessions, set ZERO_AFFINITY_COOKIE_NAME and zero-cache expires the cookie on connections it turns away while draining. A new zero.sync.drain-time metric estimates how long a view-syncer's drain will take. (f22ec09b6, f77997349, 9a74d2d3c, 04a27634b) Clients retry a failed connection sooner: after 250–500 ms the first time, backing off to the usual 5 seconds by the fifth failure, with jitter so clients that lost the same server don't retry together. A draining zero-cache also tells new connections to retry within a second instead of after 5 seconds. (06622e6ec, a5a940321) Replication status events are republished when the replica grows by 1 GiB or 5%, so consumers that size storage from replicaSize see growth from ordinary writes. (76a0a1bd1) Multi-node deployments can authenticate view-syncers to the replication-manager with a shared ZERO_CHANGE_STREAMER_SECRET. A single-node zero-cache generates its own. (b2b8f68e4) ZERO_ENABLE_HEAPZ=false turns off the /heapz heap-snapshot endpoint while keeping the other diagnostic endpoints and the inspector. (ea3105333) Performance Server-Side Hydration When a client connects or adds a query, zero-cache has to hydrate it: run it against the replica and send the results. In Zero 1.12 this takes about 1.2x less CPU, and about 1.8x less for queries with whereExists, which are common because permissions use them. We measured 100 client groups on one sync worker, each hydrating its user's top issues with their latest comments. (6563d5751, 70c771950, c2882cba0, 5a5fb1af1, c9c34d145, f7233246c, ae794cc0e) Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.12 does this with about 1.4x to 1.7x less CPU than 1.11, which was already 2.5x better than 1.10. We measured the same workload as last release: 100 client groups on one sync worker, each syncing a query with a related subquery. Pushing one write through 300 open queries is about 1.6x faster too. (6563d5751, a35b628ea, 018a90f86, f5d11d924, 8702aea45, fd30e6437) React Native Cold Start When a React Native app starts, Zero loads the rows it has stored on the device into memory and runs the app's queries over them before anything renders. In Zero 1.12 this is about 3x faster. Hermes, the JavaScript engine React Native uses, was spending most of that time sorting rows that were already in order. We measured an app with 180,000 stored rows and 21 queries, on the Android emulator and the iOS simulator. (bfc7e8c24, e7f8a1d91, 8c21e7960, ea4560f4e, e058bfca2, 6375eeb84, 855c495b1) Query Planning Before zero-cache hydrates a query with whereExists, it plans which way to run each join. With three to five whereExists, or nested ones, planning now takes about 2x to 10x less time. (9215b24b2) Many Connections Every change zero-cache syncs reaches each connected client as a poke. In 1.11, every poke allocated a fresh 1 MiB buffer, so with many clients connected the garbage collector was busy cleaning up after them. Zero 1.12 keeps a small buffer per connection and reuses it. With 600 clients connected and 100 writes per second on one sync worker, the sync worker now uses about 1.9x less CPU. (ac45c279f) Fixes Queries and Mutations Server mutators and queries read time with time zone columns as negative milliseconds when the zone offset was positive, and a ServerSchema written by hand with the short timestamptz name read a timestamp as a time of day. (9a08f1e40) Server mutators and queries failed with type \"...\" does not exist for enum and other user-defined Postgres types outside the public schema. Casts are now schema-qualified. (8166580b0) (thanks @jschaf!) A push that failed at the API server could put other tabs of the same client group into the Error state, or fail a socket the client had already replaced. A failed push now affects only the socket that sent it, and that socket's queued pushes are re-sent on the next connection. (945ce6192) (thanks @pawarren!) runAnalyzeCLI --query no longer evaluates the query text as JavaScript. It accepts query builder calls, literals and arrow functions, and rejects anything else with its position. (02b6c1bbd) Clients and React Native On React Native, client garbage collection failed every 5 minutes, and queries whose related() entries weren't in alias order threw during normalization, because Hermes lacks Array.prototype.toSorted. (b62ded385) (thanks @wikil!) A SQLite-backed store whose close() threw blocked every later Zero instance with the same name from opening, and two overlapping close() calls could close a shared SQLite connection under another store. (dee69dc9d, cc861e8f6) (thanks @wikil!) An app that refreshed its auth token while the first connection of a new client group was still being validated could be refused for good with The initConnection message for a new client group must include client schema. (874e1ec9d) (thanks @YevheniiKotyrlo!) After zero-cache was rolled back, a client group holding a query the older server could not parse was stuck in an error state until the server rolled forward. The group is now reset and the client starts a fresh one. (dbf677638) A write that put keys from both the upper Basic Multilingual Plane (U+E000 to U+FFFF) and above it (such as emoji) into the same B-tree node failed with putMany entries must be sorted and unique, and a diff could report such a key as removed and re-added instead of changed. (0e15506e8) (thanks @natew!) Client garbage collection left a store open for every database it inspected. With SQLite, closing Zero then no longer closed its database connection, and a custom StoreProvider whose drop refuses an open store failed every collection. (bd436ef0d) (thanks @wikil!) Client, client group and connection IDs now come from crypto.getRandomValues when available instead of Math.random. (e80d6305c, c2ebe3df5) zero-cache Reliability A view-syncer that reloaded a client group's state after an error kept stale row records and query pipelines, which could crash it with Query … is not in the CVR or Unexpected undefined value, or leave rows that never reached the client. (1137280b6, bb348961f, 9e0e85b62) With a statement_timeout on the CVR database, connecting a client group with many rows could fail with canceling statement due to statement timeout. (892fa553b) View-syncers on slow or throttled disks spent up to a minute inside a single commit running SQLite checkpoints, and were dropped by the replication-manager as slow subscribers. Serving replicas are now checkpointed by a separate process. (fbdb27ed5) Clients were disabled and reloaded with SchemaVersionNotSupported while an upstream migration backfilled a table or column. They now retry with backoff until the backfill is done. (8280af82f) Malformed client messages, such as a second initConnection on a socket, an invalid baseCookie or an invalid query registration, were logged as internal errors. They are now client errors logged at warn. (e1c39de03, f452e99b5, 512baff4b) A client group stays with the user it was first opened for across view-syncer restarts; a connection as a different user is refused the same way it already was while the group was live. (8d9466b13) Inspector authentication is per connection, and the 1.12 client remembers a password the server accepted, so it doesn't prompt again after reconnecting. Repeated wrong admin or operator passwords are now rate limited, on the inspector and on the HTTP diagnostic endpoints. (e186c0d59, c767ac31c, 65375a770) zero-cache logs and traces show a hash of the clientGroupID instead of the ID. (adfce3301, de9121d99) Replication A numeric, float4 or float8 value between about 2^63 and 1e21 stopped replication with a RangeError, a numeric between 2^53 and 2^63 was stored as the wrong integer, and such a value in a row key or query literal failed the client group after a reload. These values now replicate as doubles. (340215ab5) A NULL element in a timestamp[], json[] or int8[] column stopped replication, and in text, enum, boolean and numeric arrays it was stored as \"NULL\", false or null. Array columns are now parsed correctly, including arrays with explicit bounds. Rows already stored keep their old values until they change. (fe4bc732a) A schema-change or lag-report message on the replication stream that zero-cache could not parse stopped replication. Such messages, and schema-change messages that its own upstream functions didn't emit, are now skipped. (08c634549, f6010bb34) With Litestream v5 backups on Google Cloud Storage or Azure, backup files were never compacted, so old ones were never deleted and the backup kept growing. (e2f030e63) Breaking Changes analyze-query bin removed: npx analyze-query is gone. Use a script that calls runAnalyzeCLI. @rocicorp/zero now depends on oxc-parser, which installs a native binding for your platform. (a3932f51e, 02b6c1bbd) userID needs a token: when ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL is set, a connection that passes a userID without an auth token is refused with Unauthorized. Omit userID for logged-out users; the deprecated userID: 'anon' without auth is refused too. (522688120) Changed defaults: ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA is on. It needs Litestream v5 backups, the default since 1.11, and each replication-manager holds its own slot while it runs. Turning it off doesn't resync. (49607b8b3) Multi-node secret: set ZERO_CHANGE_STREAMER_SECRET on the view-syncers first, then on the replication-manager. Until you do, the replication-manager still accepts view-syncers without it, but warns at startup. If one of your nodes runs sync workers and is also the replication-manager for other view-syncers, it now makes up its own secret, so set the secret on every node. (b2b8f68e4) Legacy CRUD mutations are keyed by the primary key of the replica's table, not by the key the client sends. This only matters if your client schema declares a different primary key than the table has in Postgres. (f24fddae7) Rollback: clients keep their local database and unsent mutations when moving between 1.11 and 1.12 in either direction. One exception: a client group that registered an aggregate, groupBy, distinct or JSON path query is reset when zero-cache is rolled back to 1.11, and its unsent mutations are lost. (8b716b3fe, dbf677638)", "headings": [ { "text": "Installation", @@ -6069,6 +6069,10 @@ "text": "Query Planning", "id": "query-planning" }, + { + "text": "Many Connections", + "id": "many-connections" + }, { "text": "Fixes", "id": "fixes" @@ -6173,7 +6177,7 @@ "sectionTitle": "Performance", "sectionId": "performance", "url": "/docs/release-notes/1.12", - "content": "Server-Side Hydration When a client connects or adds a query, zero-cache has to hydrate it: run it against the replica and send the results. In Zero 1.12 this takes about 1.2x less CPU, and about 1.8x less for queries with whereExists, which are common because permissions use them. We measured 100 client groups on one sync worker, each hydrating its user's top issues with their latest comments. (6563d5751, 70c771950, c2882cba0, 5a5fb1af1, c9c34d145, f7233246c, ae794cc0e) Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.12 does this with about 1.4x to 1.7x less CPU than 1.11, which was already 2.5x better than 1.10. We measured the same workload as last release: 100 client groups on one sync worker, each syncing a query with a related subquery. Pushing one write through 300 open queries is about 1.6x faster too. (6563d5751, a35b628ea, 018a90f86, f5d11d924, 8702aea45, fd30e6437) React Native Cold Start When a React Native app starts, Zero loads the rows it has stored on the device into memory and runs the app's queries over them before anything renders. In Zero 1.12 this is about 3x faster. Hermes, the JavaScript engine React Native uses, was spending most of that time sorting rows that were already in order. We measured an app with 180,000 stored rows and 21 queries, on the Android emulator and the iOS simulator. (bfc7e8c24, e7f8a1d91, 8c21e7960, ea4560f4e, e058bfca2, 6375eeb84, 855c495b1) Query Planning Before zero-cache hydrates a query with whereExists, it plans which way to run each join. With three to five whereExists, or nested ones, planning now takes about 2x to 10x less time. (9215b24b2)", + "content": "Server-Side Hydration When a client connects or adds a query, zero-cache has to hydrate it: run it against the replica and send the results. In Zero 1.12 this takes about 1.2x less CPU, and about 1.8x less for queries with whereExists, which are common because permissions use them. We measured 100 client groups on one sync worker, each hydrating its user's top issues with their latest comments. (6563d5751, 70c771950, c2882cba0, 5a5fb1af1, c9c34d145, f7233246c, ae794cc0e) Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.12 does this with about 1.4x to 1.7x less CPU than 1.11, which was already 2.5x better than 1.10. We measured the same workload as last release: 100 client groups on one sync worker, each syncing a query with a related subquery. Pushing one write through 300 open queries is about 1.6x faster too. (6563d5751, a35b628ea, 018a90f86, f5d11d924, 8702aea45, fd30e6437) React Native Cold Start When a React Native app starts, Zero loads the rows it has stored on the device into memory and runs the app's queries over them before anything renders. In Zero 1.12 this is about 3x faster. Hermes, the JavaScript engine React Native uses, was spending most of that time sorting rows that were already in order. We measured an app with 180,000 stored rows and 21 queries, on the Android emulator and the iOS simulator. (bfc7e8c24, e7f8a1d91, 8c21e7960, ea4560f4e, e058bfca2, 6375eeb84, 855c495b1) Query Planning Before zero-cache hydrates a query with whereExists, it plans which way to run each join. With three to five whereExists, or nested ones, planning now takes about 2x to 10x less time. (9215b24b2) Many Connections Every change zero-cache syncs reaches each connected client as a poke. In 1.11, every poke allocated a fresh 1 MiB buffer, so with many clients connected the garbage collector was busy cleaning up after them. Zero 1.12 keeps a small buffer per connection and reuses it. With 600 clients connected and 100 writes per second on one sync worker, the sync worker now uses about 1.9x less CPU. (ac45c279f)", "kind": "section" }, { @@ -6217,7 +6221,17 @@ "kind": "section" }, { - "id": "482-release-notes/1.12#fixes", + "id": "482-release-notes/1.12#many-connections", + "title": "Zero 1.12", + "searchTitle": "Many Connections", + "sectionTitle": "Many Connections", + "sectionId": "many-connections", + "url": "/docs/release-notes/1.12", + "content": "Every change zero-cache syncs reaches each connected client as a poke. In 1.11, every poke allocated a fresh 1 MiB buffer, so with many clients connected the garbage collector was busy cleaning up after them. Zero 1.12 keeps a small buffer per connection and reuses it. With 600 clients connected and 100 writes per second on one sync worker, the sync worker now uses about 1.9x less CPU. (ac45c279f)", + "kind": "section" + }, + { + "id": "483-release-notes/1.12#fixes", "title": "Zero 1.12", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6227,7 +6241,7 @@ "kind": "section" }, { - "id": "483-release-notes/1.12#queries-and-mutations", + "id": "484-release-notes/1.12#queries-and-mutations", "title": "Zero 1.12", "searchTitle": "Queries and Mutations", "sectionTitle": "Queries and Mutations", @@ -6237,7 +6251,7 @@ "kind": "section" }, { - "id": "484-release-notes/1.12#clients-and-react-native", + "id": "485-release-notes/1.12#clients-and-react-native", "title": "Zero 1.12", "searchTitle": "Clients and React Native", "sectionTitle": "Clients and React Native", @@ -6247,7 +6261,7 @@ "kind": "section" }, { - "id": "485-release-notes/1.12#zero-cache-reliability", + "id": "486-release-notes/1.12#zero-cache-reliability", "title": "Zero 1.12", "searchTitle": "zero-cache Reliability", "sectionTitle": "zero-cache Reliability", @@ -6257,7 +6271,7 @@ "kind": "section" }, { - "id": "486-release-notes/1.12#replication", + "id": "487-release-notes/1.12#replication", "title": "Zero 1.12", "searchTitle": "Replication", "sectionTitle": "Replication", @@ -6267,7 +6281,7 @@ "kind": "section" }, { - "id": "487-release-notes/1.12#breaking-changes", + "id": "488-release-notes/1.12#breaking-changes", "title": "Zero 1.12", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6307,7 +6321,7 @@ "kind": "page" }, { - "id": "488-release-notes/1.2#installation", + "id": "489-release-notes/1.2#installation", "title": "Zero 1.2", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6317,7 +6331,7 @@ "kind": "section" }, { - "id": "489-release-notes/1.2#features", + "id": "490-release-notes/1.2#features", "title": "Zero 1.2", "searchTitle": "Features", "sectionTitle": "Features", @@ -6327,7 +6341,7 @@ "kind": "section" }, { - "id": "490-release-notes/1.2#performance", + "id": "491-release-notes/1.2#performance", "title": "Zero 1.2", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6337,7 +6351,7 @@ "kind": "section" }, { - "id": "491-release-notes/1.2#fixes", + "id": "492-release-notes/1.2#fixes", "title": "Zero 1.2", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6347,7 +6361,7 @@ "kind": "section" }, { - "id": "492-release-notes/1.2#breaking-changes", + "id": "493-release-notes/1.2#breaking-changes", "title": "Zero 1.2", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6387,7 +6401,7 @@ "kind": "page" }, { - "id": "493-release-notes/1.3#installation", + "id": "494-release-notes/1.3#installation", "title": "Zero 1.3", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6397,7 +6411,7 @@ "kind": "section" }, { - "id": "494-release-notes/1.3#features", + "id": "495-release-notes/1.3#features", "title": "Zero 1.3", "searchTitle": "Features", "sectionTitle": "Features", @@ -6407,7 +6421,7 @@ "kind": "section" }, { - "id": "495-release-notes/1.3#performance", + "id": "496-release-notes/1.3#performance", "title": "Zero 1.3", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6417,7 +6431,7 @@ "kind": "section" }, { - "id": "496-release-notes/1.3#fixes", + "id": "497-release-notes/1.3#fixes", "title": "Zero 1.3", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6427,7 +6441,7 @@ "kind": "section" }, { - "id": "497-release-notes/1.3#breaking-changes", + "id": "498-release-notes/1.3#breaking-changes", "title": "Zero 1.3", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6475,7 +6489,7 @@ "kind": "page" }, { - "id": "498-release-notes/1.4#installation", + "id": "499-release-notes/1.4#installation", "title": "Zero 1.4", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6485,7 +6499,7 @@ "kind": "section" }, { - "id": "499-release-notes/1.4#upgrading", + "id": "500-release-notes/1.4#upgrading", "title": "Zero 1.4", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -6495,7 +6509,7 @@ "kind": "section" }, { - "id": "500-release-notes/1.4#userid-anon", + "id": "501-release-notes/1.4#userid-anon", "title": "Zero 1.4", "searchTitle": "userID: \"anon\"", "sectionTitle": "userID: \"anon\"", @@ -6505,7 +6519,7 @@ "kind": "section" }, { - "id": "501-release-notes/1.4#features", + "id": "502-release-notes/1.4#features", "title": "Zero 1.4", "searchTitle": "Features", "sectionTitle": "Features", @@ -6515,7 +6529,7 @@ "kind": "section" }, { - "id": "502-release-notes/1.4#performance", + "id": "503-release-notes/1.4#performance", "title": "Zero 1.4", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6525,7 +6539,7 @@ "kind": "section" }, { - "id": "503-release-notes/1.4#fixes", + "id": "504-release-notes/1.4#fixes", "title": "Zero 1.4", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6535,7 +6549,7 @@ "kind": "section" }, { - "id": "504-release-notes/1.4#breaking-changes", + "id": "505-release-notes/1.4#breaking-changes", "title": "Zero 1.4", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6587,7 +6601,7 @@ "kind": "page" }, { - "id": "505-release-notes/1.5#installation", + "id": "506-release-notes/1.5#installation", "title": "Zero 1.5", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6597,7 +6611,7 @@ "kind": "section" }, { - "id": "506-release-notes/1.5#upgrading", + "id": "507-release-notes/1.5#upgrading", "title": "Zero 1.5", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -6607,7 +6621,7 @@ "kind": "section" }, { - "id": "507-release-notes/1.5#authenticated-client-groups", + "id": "508-release-notes/1.5#authenticated-client-groups", "title": "Zero 1.5", "searchTitle": "Authenticated Client Groups", "sectionTitle": "Authenticated Client Groups", @@ -6617,7 +6631,7 @@ "kind": "section" }, { - "id": "508-release-notes/1.5#deploy-order", + "id": "509-release-notes/1.5#deploy-order", "title": "Zero 1.5", "searchTitle": "Deploy Order", "sectionTitle": "Deploy Order", @@ -6627,7 +6641,7 @@ "kind": "section" }, { - "id": "509-release-notes/1.5#features", + "id": "510-release-notes/1.5#features", "title": "Zero 1.5", "searchTitle": "Features", "sectionTitle": "Features", @@ -6637,7 +6651,7 @@ "kind": "section" }, { - "id": "510-release-notes/1.5#performance", + "id": "511-release-notes/1.5#performance", "title": "Zero 1.5", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6647,7 +6661,7 @@ "kind": "section" }, { - "id": "511-release-notes/1.5#fixes", + "id": "512-release-notes/1.5#fixes", "title": "Zero 1.5", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6657,7 +6671,7 @@ "kind": "section" }, { - "id": "512-release-notes/1.5#breaking-changes", + "id": "513-release-notes/1.5#breaking-changes", "title": "Zero 1.5", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6705,7 +6719,7 @@ "kind": "page" }, { - "id": "513-release-notes/1.6#installation", + "id": "514-release-notes/1.6#installation", "title": "Zero 1.6", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6715,7 +6729,7 @@ "kind": "section" }, { - "id": "514-release-notes/1.6#upgrading", + "id": "515-release-notes/1.6#upgrading", "title": "Zero 1.6", "searchTitle": "Upgrading", "sectionTitle": "Upgrading", @@ -6725,7 +6739,7 @@ "kind": "section" }, { - "id": "515-release-notes/1.6#planetscale-failover", + "id": "516-release-notes/1.6#planetscale-failover", "title": "Zero 1.6", "searchTitle": "PlanetScale Failover", "sectionTitle": "PlanetScale Failover", @@ -6735,7 +6749,7 @@ "kind": "section" }, { - "id": "516-release-notes/1.6#features", + "id": "517-release-notes/1.6#features", "title": "Zero 1.6", "searchTitle": "Features", "sectionTitle": "Features", @@ -6745,7 +6759,7 @@ "kind": "section" }, { - "id": "517-release-notes/1.6#performance", + "id": "518-release-notes/1.6#performance", "title": "Zero 1.6", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6755,7 +6769,7 @@ "kind": "section" }, { - "id": "518-release-notes/1.6#fixes", + "id": "519-release-notes/1.6#fixes", "title": "Zero 1.6", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6765,7 +6779,7 @@ "kind": "section" }, { - "id": "519-release-notes/1.6#breaking-changes", + "id": "520-release-notes/1.6#breaking-changes", "title": "Zero 1.6", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6817,7 +6831,7 @@ "kind": "page" }, { - "id": "520-release-notes/1.7#installation", + "id": "521-release-notes/1.7#installation", "title": "Zero 1.7", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6827,7 +6841,7 @@ "kind": "section" }, { - "id": "521-release-notes/1.7#overview", + "id": "522-release-notes/1.7#overview", "title": "Zero 1.7", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6837,7 +6851,7 @@ "kind": "section" }, { - "id": "522-release-notes/1.7#features", + "id": "523-release-notes/1.7#features", "title": "Zero 1.7", "searchTitle": "Features", "sectionTitle": "Features", @@ -6847,7 +6861,7 @@ "kind": "section" }, { - "id": "523-release-notes/1.7#performance", + "id": "524-release-notes/1.7#performance", "title": "Zero 1.7", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6857,7 +6871,7 @@ "kind": "section" }, { - "id": "524-release-notes/1.7#replication", + "id": "525-release-notes/1.7#replication", "title": "Zero 1.7", "searchTitle": "Replication", "sectionTitle": "Replication", @@ -6867,7 +6881,7 @@ "kind": "section" }, { - "id": "525-release-notes/1.7#flipped-exists-queries", + "id": "526-release-notes/1.7#flipped-exists-queries", "title": "Zero 1.7", "searchTitle": "Flipped Exists Queries", "sectionTitle": "Flipped Exists Queries", @@ -6877,7 +6891,7 @@ "kind": "section" }, { - "id": "526-release-notes/1.7#fixes", + "id": "527-release-notes/1.7#fixes", "title": "Zero 1.7", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -6887,7 +6901,7 @@ "kind": "section" }, { - "id": "527-release-notes/1.7#breaking-changes", + "id": "528-release-notes/1.7#breaking-changes", "title": "Zero 1.7", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -6943,7 +6957,7 @@ "kind": "page" }, { - "id": "528-release-notes/1.8#installation", + "id": "529-release-notes/1.8#installation", "title": "Zero 1.8", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -6953,7 +6967,7 @@ "kind": "section" }, { - "id": "529-release-notes/1.8#overview", + "id": "530-release-notes/1.8#overview", "title": "Zero 1.8", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -6963,7 +6977,7 @@ "kind": "section" }, { - "id": "530-release-notes/1.8#features", + "id": "531-release-notes/1.8#features", "title": "Zero 1.8", "searchTitle": "Features", "sectionTitle": "Features", @@ -6973,7 +6987,7 @@ "kind": "section" }, { - "id": "531-release-notes/1.8#performance", + "id": "532-release-notes/1.8#performance", "title": "Zero 1.8", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -6983,7 +6997,7 @@ "kind": "section" }, { - "id": "532-release-notes/1.8#replicating-large-transactions", + "id": "533-release-notes/1.8#replicating-large-transactions", "title": "Zero 1.8", "searchTitle": "Replicating Large Transactions", "sectionTitle": "Replicating Large Transactions", @@ -6993,7 +7007,7 @@ "kind": "section" }, { - "id": "533-release-notes/1.8#maintaining-limit-queries", + "id": "534-release-notes/1.8#maintaining-limit-queries", "title": "Zero 1.8", "searchTitle": "Maintaining limit() Queries", "sectionTitle": "Maintaining limit() Queries", @@ -7003,7 +7017,7 @@ "kind": "section" }, { - "id": "534-release-notes/1.8#client-side-hydration", + "id": "535-release-notes/1.8#client-side-hydration", "title": "Zero 1.8", "searchTitle": "Client-Side Hydration", "sectionTitle": "Client-Side Hydration", @@ -7013,7 +7027,7 @@ "kind": "section" }, { - "id": "535-release-notes/1.8#fixes", + "id": "536-release-notes/1.8#fixes", "title": "Zero 1.8", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -7023,7 +7037,7 @@ "kind": "section" }, { - "id": "536-release-notes/1.8#breaking-changes", + "id": "537-release-notes/1.8#breaking-changes", "title": "Zero 1.8", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -7067,7 +7081,7 @@ "kind": "page" }, { - "id": "537-release-notes/1.9#installation", + "id": "538-release-notes/1.9#installation", "title": "Zero 1.9", "searchTitle": "Installation", "sectionTitle": "Installation", @@ -7077,7 +7091,7 @@ "kind": "section" }, { - "id": "538-release-notes/1.9#overview", + "id": "539-release-notes/1.9#overview", "title": "Zero 1.9", "searchTitle": "Overview", "sectionTitle": "Overview", @@ -7087,7 +7101,7 @@ "kind": "section" }, { - "id": "539-release-notes/1.9#features", + "id": "540-release-notes/1.9#features", "title": "Zero 1.9", "searchTitle": "Features", "sectionTitle": "Features", @@ -7097,7 +7111,7 @@ "kind": "section" }, { - "id": "540-release-notes/1.9#performance", + "id": "541-release-notes/1.9#performance", "title": "Zero 1.9", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -7107,7 +7121,7 @@ "kind": "section" }, { - "id": "541-release-notes/1.9#cold-mutation-latency", + "id": "542-release-notes/1.9#cold-mutation-latency", "title": "Zero 1.9", "searchTitle": "Cold Mutation Latency", "sectionTitle": "Cold Mutation Latency", @@ -7117,7 +7131,7 @@ "kind": "section" }, { - "id": "542-release-notes/1.9#fixes", + "id": "543-release-notes/1.9#fixes", "title": "Zero 1.9", "searchTitle": "Fixes", "sectionTitle": "Fixes", @@ -7154,7 +7168,7 @@ "kind": "page" }, { - "id": "543-reporting-bugs#zbugs", + "id": "544-reporting-bugs#zbugs", "title": "Reporting Bugs", "searchTitle": "zbugs", "sectionTitle": "zbugs", @@ -7164,7 +7178,7 @@ "kind": "section" }, { - "id": "544-reporting-bugs#discord", + "id": "545-reporting-bugs#discord", "title": "Reporting Bugs", "searchTitle": "Discord", "sectionTitle": "Discord", @@ -7200,7 +7214,7 @@ "kind": "page" }, { - "id": "545-rest#pattern", + "id": "546-rest#pattern", "title": "REST", "searchTitle": "Pattern", "sectionTitle": "Pattern", @@ -7210,7 +7224,7 @@ "kind": "section" }, { - "id": "546-rest#tanstack-start-example", + "id": "547-rest#tanstack-start-example", "title": "REST", "searchTitle": "TanStack Start Example", "sectionTitle": "TanStack Start Example", @@ -7220,7 +7234,7 @@ "kind": "section" }, { - "id": "547-rest#openapi-generation", + "id": "548-rest#openapi-generation", "title": "REST", "searchTitle": "OpenAPI Generation", "sectionTitle": "OpenAPI Generation", @@ -7230,7 +7244,7 @@ "kind": "section" }, { - "id": "548-rest#full-working-example", + "id": "549-rest#full-working-example", "title": "REST", "searchTitle": "Full Working Example", "sectionTitle": "Full Working Example", @@ -7258,7 +7272,7 @@ "kind": "page" }, { - "id": "549-roadmap#q4-2025", + "id": "550-roadmap#q4-2025", "title": "Roadmap", "searchTitle": "Q4 2025", "sectionTitle": "Q4 2025", @@ -7268,7 +7282,7 @@ "kind": "section" }, { - "id": "550-roadmap#beyond", + "id": "551-roadmap#beyond", "title": "Roadmap", "searchTitle": "Beyond", "sectionTitle": "Beyond", @@ -7304,7 +7318,7 @@ "kind": "page" }, { - "id": "551-samples#gigabugs", + "id": "552-samples#gigabugs", "title": "Samples", "searchTitle": "Gigabugs", "sectionTitle": "Gigabugs", @@ -7314,7 +7328,7 @@ "kind": "section" }, { - "id": "552-samples#ztunes", + "id": "553-samples#ztunes", "title": "Samples", "searchTitle": "ztunes", "sectionTitle": "ztunes", @@ -7324,7 +7338,7 @@ "kind": "section" }, { - "id": "553-samples#zslack", + "id": "554-samples#zslack", "title": "Samples", "searchTitle": "zslack", "sectionTitle": "zslack", @@ -7334,7 +7348,7 @@ "kind": "section" }, { - "id": "554-samples#zero-music", + "id": "555-samples#zero-music", "title": "Samples", "searchTitle": "zero-music", "sectionTitle": "zero-music", @@ -7474,7 +7488,7 @@ "kind": "page" }, { - "id": "555-schema#generating-from-database", + "id": "556-schema#generating-from-database", "title": "Zero Schema", "searchTitle": "Generating from Database", "sectionTitle": "Generating from Database", @@ -7484,7 +7498,7 @@ "kind": "section" }, { - "id": "556-schema#writing-by-hand", + "id": "557-schema#writing-by-hand", "title": "Zero Schema", "searchTitle": "Writing by Hand", "sectionTitle": "Writing by Hand", @@ -7494,7 +7508,7 @@ "kind": "section" }, { - "id": "557-schema#table-schemas", + "id": "558-schema#table-schemas", "title": "Zero Schema", "searchTitle": "Table Schemas", "sectionTitle": "Table Schemas", @@ -7504,7 +7518,7 @@ "kind": "section" }, { - "id": "558-schema#name-mapping", + "id": "559-schema#name-mapping", "title": "Zero Schema", "searchTitle": "Name Mapping", "sectionTitle": "Name Mapping", @@ -7514,7 +7528,7 @@ "kind": "section" }, { - "id": "559-schema#multiple-schemas", + "id": "560-schema#multiple-schemas", "title": "Zero Schema", "searchTitle": "Multiple Schemas", "sectionTitle": "Multiple Schemas", @@ -7524,7 +7538,7 @@ "kind": "section" }, { - "id": "560-schema#optional-columns", + "id": "561-schema#optional-columns", "title": "Zero Schema", "searchTitle": "Optional Columns", "sectionTitle": "Optional Columns", @@ -7534,7 +7548,7 @@ "kind": "section" }, { - "id": "561-schema#enumerations", + "id": "562-schema#enumerations", "title": "Zero Schema", "searchTitle": "Enumerations", "sectionTitle": "Enumerations", @@ -7544,7 +7558,7 @@ "kind": "section" }, { - "id": "562-schema#custom-json-types", + "id": "563-schema#custom-json-types", "title": "Zero Schema", "searchTitle": "Custom JSON Types", "sectionTitle": "Custom JSON Types", @@ -7554,7 +7568,7 @@ "kind": "section" }, { - "id": "563-schema#compound-primary-keys", + "id": "564-schema#compound-primary-keys", "title": "Zero Schema", "searchTitle": "Compound Primary Keys", "sectionTitle": "Compound Primary Keys", @@ -7564,7 +7578,7 @@ "kind": "section" }, { - "id": "564-schema#unique-keys", + "id": "565-schema#unique-keys", "title": "Zero Schema", "searchTitle": "Unique Keys", "sectionTitle": "Unique Keys", @@ -7574,7 +7588,7 @@ "kind": "section" }, { - "id": "565-schema#relationships", + "id": "566-schema#relationships", "title": "Zero Schema", "searchTitle": "Relationships", "sectionTitle": "Relationships", @@ -7584,7 +7598,7 @@ "kind": "section" }, { - "id": "566-schema#many-to-many-relationships", + "id": "567-schema#many-to-many-relationships", "title": "Zero Schema", "searchTitle": "Many-to-Many Relationships", "sectionTitle": "Many-to-Many Relationships", @@ -7594,7 +7608,7 @@ "kind": "section" }, { - "id": "567-schema#compound-keys-relationships", + "id": "568-schema#compound-keys-relationships", "title": "Zero Schema", "searchTitle": "Compound Keys Relationships", "sectionTitle": "Compound Keys Relationships", @@ -7604,7 +7618,7 @@ "kind": "section" }, { - "id": "568-schema#circular-relationships", + "id": "569-schema#circular-relationships", "title": "Zero Schema", "searchTitle": "Circular Relationships", "sectionTitle": "Circular Relationships", @@ -7614,7 +7628,7 @@ "kind": "section" }, { - "id": "569-schema#database-schemas", + "id": "570-schema#database-schemas", "title": "Zero Schema", "searchTitle": "Database Schemas", "sectionTitle": "Database Schemas", @@ -7624,7 +7638,7 @@ "kind": "section" }, { - "id": "570-schema#register-schema-type", + "id": "571-schema#register-schema-type", "title": "Zero Schema", "searchTitle": "Register Schema Type", "sectionTitle": "Register Schema Type", @@ -7634,7 +7648,7 @@ "kind": "section" }, { - "id": "571-schema#schema-changes", + "id": "572-schema#schema-changes", "title": "Zero Schema", "searchTitle": "Schema Changes", "sectionTitle": "Schema Changes", @@ -7644,7 +7658,7 @@ "kind": "section" }, { - "id": "572-schema#development", + "id": "573-schema#development", "title": "Zero Schema", "searchTitle": "Development", "sectionTitle": "Development", @@ -7654,7 +7668,7 @@ "kind": "section" }, { - "id": "573-schema#production", + "id": "574-schema#production", "title": "Zero Schema", "searchTitle": "Production", "sectionTitle": "Production", @@ -7664,7 +7678,7 @@ "kind": "section" }, { - "id": "574-schema#expand-changes", + "id": "575-schema#expand-changes", "title": "Zero Schema", "searchTitle": "Expand Changes", "sectionTitle": "Expand Changes", @@ -7674,7 +7688,7 @@ "kind": "section" }, { - "id": "575-schema#contract-changes", + "id": "576-schema#contract-changes", "title": "Zero Schema", "searchTitle": "Contract Changes", "sectionTitle": "Contract Changes", @@ -7684,7 +7698,7 @@ "kind": "section" }, { - "id": "576-schema#compound-changes", + "id": "577-schema#compound-changes", "title": "Zero Schema", "searchTitle": "Compound Changes", "sectionTitle": "Compound Changes", @@ -7694,7 +7708,7 @@ "kind": "section" }, { - "id": "577-schema#examples", + "id": "578-schema#examples", "title": "Zero Schema", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -7704,7 +7718,7 @@ "kind": "section" }, { - "id": "578-schema#adding-a-column", + "id": "579-schema#adding-a-column", "title": "Zero Schema", "searchTitle": "Adding a Column", "sectionTitle": "Adding a Column", @@ -7714,7 +7728,7 @@ "kind": "section" }, { - "id": "579-schema#removing-a-column", + "id": "580-schema#removing-a-column", "title": "Zero Schema", "searchTitle": "Removing a Column", "sectionTitle": "Removing a Column", @@ -7724,7 +7738,7 @@ "kind": "section" }, { - "id": "580-schema#renaming-a-column", + "id": "581-schema#renaming-a-column", "title": "Zero Schema", "searchTitle": "Renaming a Column", "sectionTitle": "Renaming a Column", @@ -7734,7 +7748,7 @@ "kind": "section" }, { - "id": "581-schema#making-a-column-optional", + "id": "582-schema#making-a-column-optional", "title": "Zero Schema", "searchTitle": "Making a Column Optional", "sectionTitle": "Making a Column Optional", @@ -7744,7 +7758,7 @@ "kind": "section" }, { - "id": "582-schema#quick-reference", + "id": "583-schema#quick-reference", "title": "Zero Schema", "searchTitle": "Quick Reference", "sectionTitle": "Quick Reference", @@ -7754,7 +7768,7 @@ "kind": "section" }, { - "id": "583-schema#backfill", + "id": "584-schema#backfill", "title": "Zero Schema", "searchTitle": "Backfill", "sectionTitle": "Backfill", @@ -7764,7 +7778,7 @@ "kind": "section" }, { - "id": "584-schema#monitoring-backfill-progress", + "id": "585-schema#monitoring-backfill-progress", "title": "Zero Schema", "searchTitle": "Monitoring Backfill Progress", "sectionTitle": "Monitoring Backfill Progress", @@ -7836,7 +7850,7 @@ "kind": "page" }, { - "id": "585-self-host#docker-images", + "id": "586-self-host#docker-images", "title": "Self-Hosting Zero", "searchTitle": "Docker Images", "sectionTitle": "Docker Images", @@ -7846,7 +7860,7 @@ "kind": "section" }, { - "id": "586-self-host#minimum-viable-strategy", + "id": "587-self-host#minimum-viable-strategy", "title": "Self-Hosting Zero", "searchTitle": "Minimum Viable Strategy", "sectionTitle": "Minimum Viable Strategy", @@ -7856,7 +7870,7 @@ "kind": "section" }, { - "id": "587-self-host#maximal-strategy", + "id": "588-self-host#maximal-strategy", "title": "Self-Hosting Zero", "searchTitle": "Maximal Strategy", "sectionTitle": "Maximal Strategy", @@ -7866,7 +7880,7 @@ "kind": "section" }, { - "id": "588-self-host#replica-lifecycle", + "id": "589-self-host#replica-lifecycle", "title": "Self-Hosting Zero", "searchTitle": "Replica Lifecycle", "sectionTitle": "Replica Lifecycle", @@ -7876,7 +7890,7 @@ "kind": "section" }, { - "id": "589-self-host#performance", + "id": "590-self-host#performance", "title": "Self-Hosting Zero", "searchTitle": "Performance", "sectionTitle": "Performance", @@ -7886,7 +7900,7 @@ "kind": "section" }, { - "id": "590-self-host#hydration", + "id": "591-self-host#hydration", "title": "Self-Hosting Zero", "searchTitle": "Hydration", "sectionTitle": "Hydration", @@ -7896,7 +7910,7 @@ "kind": "section" }, { - "id": "591-self-host#ivm-advancement", + "id": "592-self-host#ivm-advancement", "title": "Self-Hosting Zero", "searchTitle": "IVM advancement", "sectionTitle": "IVM advancement", @@ -7906,7 +7920,7 @@ "kind": "section" }, { - "id": "592-self-host#system-level", + "id": "593-self-host#system-level", "title": "Self-Hosting Zero", "searchTitle": "System-level", "sectionTitle": "System-level", @@ -7916,7 +7930,7 @@ "kind": "section" }, { - "id": "593-self-host#networking", + "id": "594-self-host#networking", "title": "Self-Hosting Zero", "searchTitle": "Networking", "sectionTitle": "Networking", @@ -7926,7 +7940,7 @@ "kind": "section" }, { - "id": "594-self-host#sticky-sessions", + "id": "595-self-host#sticky-sessions", "title": "Self-Hosting Zero", "searchTitle": "Sticky Sessions", "sectionTitle": "Sticky Sessions", @@ -7936,7 +7950,7 @@ "kind": "section" }, { - "id": "595-self-host#rolling-updates", + "id": "596-self-host#rolling-updates", "title": "Self-Hosting Zero", "searchTitle": "Rolling Updates", "sectionTitle": "Rolling Updates", @@ -7946,7 +7960,7 @@ "kind": "section" }, { - "id": "596-self-host#clientserver-version-compatibility", + "id": "597-self-host#clientserver-version-compatibility", "title": "Self-Hosting Zero", "searchTitle": "Client/Server Version Compatibility", "sectionTitle": "Client/Server Version Compatibility", @@ -7956,7 +7970,7 @@ "kind": "section" }, { - "id": "597-self-host#configuration", + "id": "598-self-host#configuration", "title": "Self-Hosting Zero", "searchTitle": "Configuration", "sectionTitle": "Configuration", @@ -7996,7 +8010,7 @@ "kind": "page" }, { - "id": "598-server-zql#creating-a-database", + "id": "599-server-zql#creating-a-database", "title": "ZQL on the Server", "searchTitle": "Creating a Database", "sectionTitle": "Creating a Database", @@ -8006,7 +8020,7 @@ "kind": "section" }, { - "id": "599-server-zql#isolation-level", + "id": "600-server-zql#isolation-level", "title": "ZQL on the Server", "searchTitle": "Isolation Level", "sectionTitle": "Isolation Level", @@ -8016,7 +8030,7 @@ "kind": "section" }, { - "id": "600-server-zql#custom-database", + "id": "601-server-zql#custom-database", "title": "ZQL on the Server", "searchTitle": "Custom Database", "sectionTitle": "Custom Database", @@ -8026,7 +8040,7 @@ "kind": "section" }, { - "id": "601-server-zql#running-zql", + "id": "602-server-zql#running-zql", "title": "ZQL on the Server", "searchTitle": "Running ZQL", "sectionTitle": "Running ZQL", @@ -8036,7 +8050,7 @@ "kind": "section" }, { - "id": "602-server-zql#ssr", + "id": "603-server-zql#ssr", "title": "ZQL on the Server", "searchTitle": "SSR", "sectionTitle": "SSR", @@ -8068,7 +8082,7 @@ "kind": "page" }, { - "id": "603-solidjs#setup", + "id": "604-solidjs#setup", "title": "SolidJS", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -8078,7 +8092,7 @@ "kind": "section" }, { - "id": "604-solidjs#usage", + "id": "605-solidjs#usage", "title": "SolidJS", "searchTitle": "Usage", "sectionTitle": "Usage", @@ -8088,7 +8102,7 @@ "kind": "section" }, { - "id": "605-solidjs#examples", + "id": "606-solidjs#examples", "title": "SolidJS", "searchTitle": "Examples", "sectionTitle": "Examples", @@ -8124,7 +8138,7 @@ "kind": "page" }, { - "id": "606-status#breaking-changes", + "id": "607-status#breaking-changes", "title": "Project Status", "searchTitle": "Breaking Changes", "sectionTitle": "Breaking Changes", @@ -8134,7 +8148,7 @@ "kind": "section" }, { - "id": "607-status#roadmap", + "id": "608-status#roadmap", "title": "Project Status", "searchTitle": "Roadmap", "sectionTitle": "Roadmap", @@ -8144,7 +8158,7 @@ "kind": "section" }, { - "id": "608-status#2026", + "id": "609-status#2026", "title": "Project Status", "searchTitle": "2026", "sectionTitle": "2026", @@ -8154,7 +8168,7 @@ "kind": "section" }, { - "id": "609-status#soon", + "id": "610-status#soon", "title": "Project Status", "searchTitle": "Soon", "sectionTitle": "Soon", @@ -8186,7 +8200,7 @@ "kind": "page" }, { - "id": "610-sync#problem", + "id": "611-sync#problem", "title": "What is Sync?", "searchTitle": "Problem", "sectionTitle": "Problem", @@ -8196,7 +8210,7 @@ "kind": "section" }, { - "id": "611-sync#solution", + "id": "612-sync#solution", "title": "What is Sync?", "searchTitle": "Solution", "sectionTitle": "Solution", @@ -8206,7 +8220,7 @@ "kind": "section" }, { - "id": "612-sync#history-of-sync", + "id": "613-sync#history-of-sync", "title": "What is Sync?", "searchTitle": "History of Sync", "sectionTitle": "History of Sync", @@ -8290,7 +8304,7 @@ "kind": "page" }, { - "id": "613-tutorial#setup", + "id": "614-tutorial#setup", "title": "Tutorial", "searchTitle": "Setup", "sectionTitle": "Setup", @@ -8300,7 +8314,7 @@ "kind": "section" }, { - "id": "614-tutorial#create-a-project", + "id": "615-tutorial#create-a-project", "title": "Tutorial", "searchTitle": "Create a Project", "sectionTitle": "Create a Project", @@ -8310,7 +8324,7 @@ "kind": "section" }, { - "id": "615-tutorial#set-up-your-database", + "id": "616-tutorial#set-up-your-database", "title": "Tutorial", "searchTitle": "Set Up Your Database", "sectionTitle": "Set Up Your Database", @@ -8320,7 +8334,7 @@ "kind": "section" }, { - "id": "616-tutorial#install-and-run-zero-cache", + "id": "617-tutorial#install-and-run-zero-cache", "title": "Tutorial", "searchTitle": "Install and Run Zero-Cache", "sectionTitle": "Install and Run Zero-Cache", @@ -8330,7 +8344,7 @@ "kind": "section" }, { - "id": "617-tutorial#integrate-zero", + "id": "618-tutorial#integrate-zero", "title": "Tutorial", "searchTitle": "Integrate Zero", "sectionTitle": "Integrate Zero", @@ -8340,7 +8354,7 @@ "kind": "section" }, { - "id": "618-tutorial#set-up-your-zero-schema", + "id": "619-tutorial#set-up-your-zero-schema", "title": "Tutorial", "searchTitle": "Set Up Your Zero Schema", "sectionTitle": "Set Up Your Zero Schema", @@ -8350,7 +8364,7 @@ "kind": "section" }, { - "id": "619-tutorial#set-up-the-zero-client", + "id": "620-tutorial#set-up-the-zero-client", "title": "Tutorial", "searchTitle": "Set Up the Zero Client", "sectionTitle": "Set Up the Zero Client", @@ -8360,7 +8374,7 @@ "kind": "section" }, { - "id": "620-tutorial#sync-data", + "id": "621-tutorial#sync-data", "title": "Tutorial", "searchTitle": "Sync Data", "sectionTitle": "Sync Data", @@ -8370,7 +8384,7 @@ "kind": "section" }, { - "id": "621-tutorial#define-query", + "id": "622-tutorial#define-query", "title": "Tutorial", "searchTitle": "Define Query", "sectionTitle": "Define Query", @@ -8380,7 +8394,7 @@ "kind": "section" }, { - "id": "622-tutorial#add-query-endpoint", + "id": "623-tutorial#add-query-endpoint", "title": "Tutorial", "searchTitle": "Add Query Endpoint", "sectionTitle": "Add Query Endpoint", @@ -8390,7 +8404,7 @@ "kind": "section" }, { - "id": "623-tutorial#invoke-query", + "id": "624-tutorial#invoke-query", "title": "Tutorial", "searchTitle": "Invoke Query", "sectionTitle": "Invoke Query", @@ -8400,7 +8414,7 @@ "kind": "section" }, { - "id": "624-tutorial#mutate-data", + "id": "625-tutorial#mutate-data", "title": "Tutorial", "searchTitle": "Mutate Data", "sectionTitle": "Mutate Data", @@ -8410,7 +8424,7 @@ "kind": "section" }, { - "id": "625-tutorial#define-mutators", + "id": "626-tutorial#define-mutators", "title": "Tutorial", "searchTitle": "Define Mutators", "sectionTitle": "Define Mutators", @@ -8420,7 +8434,7 @@ "kind": "section" }, { - "id": "626-tutorial#add-mutate-endpoint", + "id": "627-tutorial#add-mutate-endpoint", "title": "Tutorial", "searchTitle": "Add Mutate Endpoint", "sectionTitle": "Add Mutate Endpoint", @@ -8430,7 +8444,7 @@ "kind": "section" }, { - "id": "627-tutorial#invoke-mutators", + "id": "628-tutorial#invoke-mutators", "title": "Tutorial", "searchTitle": "Invoke Mutators", "sectionTitle": "Invoke Mutators", @@ -8440,7 +8454,7 @@ "kind": "section" }, { - "id": "628-tutorial#next-steps", + "id": "629-tutorial#next-steps", "title": "Tutorial", "searchTitle": "Next Steps", "sectionTitle": "Next Steps", @@ -8516,7 +8530,7 @@ "kind": "page" }, { - "id": "629-when-to-use#zero-might-be-a-good-fit", + "id": "630-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", @@ -8526,7 +8540,7 @@ "kind": "section" }, { - "id": "630-when-to-use#you-want-to-sync-only-a-small-subset-of-data-to-client", + "id": "631-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", @@ -8536,7 +8550,7 @@ "kind": "section" }, { - "id": "631-when-to-use#you-need-fine-grained-read-or-write-permissions", + "id": "632-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", @@ -8546,7 +8560,7 @@ "kind": "section" }, { - "id": "632-when-to-use#you-are-building-a-traditional-client-server-web-app", + "id": "633-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", @@ -8556,7 +8570,7 @@ "kind": "section" }, { - "id": "633-when-to-use#you-use-postgresql", + "id": "634-when-to-use#you-use-postgresql", "title": "When To Use Zero", "searchTitle": "You use PostgreSQL", "sectionTitle": "You use PostgreSQL", @@ -8566,7 +8580,7 @@ "kind": "section" }, { - "id": "634-when-to-use#your-app-is-broadly-like-linear", + "id": "635-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\"", @@ -8576,7 +8590,7 @@ "kind": "section" }, { - "id": "635-when-to-use#interaction-performance-is-very-important-to-you", + "id": "636-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", @@ -8586,7 +8600,7 @@ "kind": "section" }, { - "id": "636-when-to-use#zero-might-not-be-a-good-fit", + "id": "637-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", @@ -8596,7 +8610,7 @@ "kind": "section" }, { - "id": "637-when-to-use#you-need-the-privacy-or-data-ownership-benefits-of-local-first", + "id": "638-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", @@ -8606,7 +8620,7 @@ "kind": "section" }, { - "id": "638-when-to-use#you-need-to-support-offline-writes-or-long-periods-offline", + "id": "639-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", @@ -8616,7 +8630,7 @@ "kind": "section" }, { - "id": "639-when-to-use#you-are-building-a-native-mobile-app", + "id": "640-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", @@ -8626,7 +8640,7 @@ "kind": "section" }, { - "id": "640-when-to-use#the-total-backend-dataset-is--100gb", + "id": "641-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", @@ -8636,7 +8650,7 @@ "kind": "section" }, { - "id": "641-when-to-use#zero-might-not-be-a-good-fit-yet", + "id": "642-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", @@ -8646,7 +8660,7 @@ "kind": "section" }, { - "id": "642-when-to-use#alternatives", + "id": "643-when-to-use#alternatives", "title": "When To Use Zero", "searchTitle": "Alternatives", "sectionTitle": "Alternatives", @@ -9082,7 +9096,7 @@ "kind": "page" }, { - "id": "643-zero-cache-config#required-flags", + "id": "644-zero-cache-config#required-flags", "title": "zero-cache Config", "searchTitle": "Required Flags", "sectionTitle": "Required Flags", @@ -9092,7 +9106,7 @@ "kind": "section" }, { - "id": "644-zero-cache-config#upstream-db", + "id": "645-zero-cache-config#upstream-db", "title": "zero-cache Config", "searchTitle": "Upstream DB", "sectionTitle": "Upstream DB", @@ -9102,7 +9116,7 @@ "kind": "section" }, { - "id": "645-zero-cache-config#admin-password", + "id": "646-zero-cache-config#admin-password", "title": "zero-cache Config", "searchTitle": "Admin Password", "sectionTitle": "Admin Password", @@ -9112,7 +9126,7 @@ "kind": "section" }, { - "id": "646-zero-cache-config#optional-flags", + "id": "647-zero-cache-config#optional-flags", "title": "zero-cache Config", "searchTitle": "Optional Flags", "sectionTitle": "Optional Flags", @@ -9122,7 +9136,7 @@ "kind": "section" }, { - "id": "647-zero-cache-config#affinity-cookie-name", + "id": "648-zero-cache-config#affinity-cookie-name", "title": "zero-cache Config", "searchTitle": "Affinity Cookie Name", "sectionTitle": "Affinity Cookie Name", @@ -9132,7 +9146,7 @@ "kind": "section" }, { - "id": "648-zero-cache-config#affinity-cookie-path", + "id": "649-zero-cache-config#affinity-cookie-path", "title": "zero-cache Config", "searchTitle": "Affinity Cookie Path", "sectionTitle": "Affinity Cookie Path", @@ -9142,7 +9156,7 @@ "kind": "section" }, { - "id": "649-zero-cache-config#affinity-cookie-domain", + "id": "650-zero-cache-config#affinity-cookie-domain", "title": "zero-cache Config", "searchTitle": "Affinity Cookie Domain", "sectionTitle": "Affinity Cookie Domain", @@ -9152,7 +9166,7 @@ "kind": "section" }, { - "id": "650-zero-cache-config#allow-legacy-queries", + "id": "651-zero-cache-config#allow-legacy-queries", "title": "zero-cache Config", "searchTitle": "Allow Legacy Queries", "sectionTitle": "Allow Legacy Queries", @@ -9162,7 +9176,7 @@ "kind": "section" }, { - "id": "651-zero-cache-config#app-id", + "id": "652-zero-cache-config#app-id", "title": "zero-cache Config", "searchTitle": "App ID", "sectionTitle": "App ID", @@ -9172,7 +9186,7 @@ "kind": "section" }, { - "id": "652-zero-cache-config#app-publications", + "id": "653-zero-cache-config#app-publications", "title": "zero-cache Config", "searchTitle": "App Publications", "sectionTitle": "App Publications", @@ -9182,7 +9196,7 @@ "kind": "section" }, { - "id": "653-zero-cache-config#auth-require-server-user-id", + "id": "654-zero-cache-config#auth-require-server-user-id", "title": "zero-cache Config", "searchTitle": "Auth Require Server User ID", "sectionTitle": "Auth Require Server User ID", @@ -9192,7 +9206,7 @@ "kind": "section" }, { - "id": "654-zero-cache-config#auth-revalidate-interval-seconds", + "id": "655-zero-cache-config#auth-revalidate-interval-seconds", "title": "zero-cache Config", "searchTitle": "Auth Revalidate Interval Seconds", "sectionTitle": "Auth Revalidate Interval Seconds", @@ -9202,7 +9216,7 @@ "kind": "section" }, { - "id": "655-zero-cache-config#auth-retransform-interval-seconds", + "id": "656-zero-cache-config#auth-retransform-interval-seconds", "title": "zero-cache Config", "searchTitle": "Auth Retransform Interval Seconds", "sectionTitle": "Auth Retransform Interval Seconds", @@ -9212,7 +9226,7 @@ "kind": "section" }, { - "id": "656-zero-cache-config#auto-reset", + "id": "657-zero-cache-config#auto-reset", "title": "zero-cache Config", "searchTitle": "Auto Reset", "sectionTitle": "Auto Reset", @@ -9222,7 +9236,7 @@ "kind": "section" }, { - "id": "657-zero-cache-config#change-db", + "id": "658-zero-cache-config#change-db", "title": "zero-cache Config", "searchTitle": "Change DB", "sectionTitle": "Change DB", @@ -9232,7 +9246,7 @@ "kind": "section" }, { - "id": "658-zero-cache-config#change-streamer-back-pressure-limit-heap-proportion", + "id": "659-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", @@ -9242,7 +9256,7 @@ "kind": "section" }, { - "id": "659-zero-cache-config#change-streamer-flow-control-consensus-timeout-proportion", + "id": "660-zero-cache-config#change-streamer-flow-control-consensus-timeout-proportion", "title": "zero-cache Config", "searchTitle": "Change Streamer Flow Control Consensus Timeout Proportion", "sectionTitle": "Change Streamer Flow Control Consensus Timeout Proportion", @@ -9252,7 +9266,7 @@ "kind": "section" }, { - "id": "660-zero-cache-config#change-streamer-flow-control-slow-subscriber-grace-period-seconds", + "id": "661-zero-cache-config#change-streamer-flow-control-slow-subscriber-grace-period-seconds", "title": "zero-cache Config", "searchTitle": "Change Streamer Flow Control Slow Subscriber Grace Period Seconds", "sectionTitle": "Change Streamer Flow Control Slow Subscriber Grace Period Seconds", @@ -9262,7 +9276,7 @@ "kind": "section" }, { - "id": "661-zero-cache-config#change-streamer-port", + "id": "662-zero-cache-config#change-streamer-port", "title": "zero-cache Config", "searchTitle": "Change Streamer Port", "sectionTitle": "Change Streamer Port", @@ -9272,7 +9286,7 @@ "kind": "section" }, { - "id": "662-zero-cache-config#change-streamer-secret", + "id": "663-zero-cache-config#change-streamer-secret", "title": "zero-cache Config", "searchTitle": "Change Streamer Secret", "sectionTitle": "Change Streamer Secret", @@ -9282,7 +9296,7 @@ "kind": "section" }, { - "id": "663-zero-cache-config#change-streamer-startup-delay-ms", + "id": "664-zero-cache-config#change-streamer-startup-delay-ms", "title": "zero-cache Config", "searchTitle": "Change Streamer Startup Delay (ms)", "sectionTitle": "Change Streamer Startup Delay (ms)", @@ -9292,7 +9306,7 @@ "kind": "section" }, { - "id": "664-zero-cache-config#change-streamer-uri", + "id": "665-zero-cache-config#change-streamer-uri", "title": "zero-cache Config", "searchTitle": "Change Streamer URI", "sectionTitle": "Change Streamer URI", @@ -9302,7 +9316,7 @@ "kind": "section" }, { - "id": "665-zero-cache-config#cvr-db", + "id": "666-zero-cache-config#cvr-db", "title": "zero-cache Config", "searchTitle": "CVR DB", "sectionTitle": "CVR DB", @@ -9312,7 +9326,7 @@ "kind": "section" }, { - "id": "666-zero-cache-config#cvr-garbage-collection-inactivity-threshold-hours", + "id": "667-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", @@ -9322,7 +9336,7 @@ "kind": "section" }, { - "id": "667-zero-cache-config#cvr-garbage-collection-initial-batch-size", + "id": "668-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", @@ -9332,7 +9346,7 @@ "kind": "section" }, { - "id": "668-zero-cache-config#cvr-garbage-collection-initial-interval-seconds", + "id": "669-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", @@ -9342,7 +9356,7 @@ "kind": "section" }, { - "id": "669-zero-cache-config#cvr-max-connections", + "id": "670-zero-cache-config#cvr-max-connections", "title": "zero-cache Config", "searchTitle": "CVR Max Connections", "sectionTitle": "CVR Max Connections", @@ -9352,7 +9366,7 @@ "kind": "section" }, { - "id": "670-zero-cache-config#enable-heapz", + "id": "671-zero-cache-config#enable-heapz", "title": "zero-cache Config", "searchTitle": "Enable Heapz", "sectionTitle": "Enable Heapz", @@ -9362,7 +9376,7 @@ "kind": "section" }, { - "id": "671-zero-cache-config#enable-query-planner", + "id": "672-zero-cache-config#enable-query-planner", "title": "zero-cache Config", "searchTitle": "Enable Query Planner", "sectionTitle": "Enable Query Planner", @@ -9372,7 +9386,7 @@ "kind": "section" }, { - "id": "672-zero-cache-config#enable-crud-mutations", + "id": "673-zero-cache-config#enable-crud-mutations", "title": "zero-cache Config", "searchTitle": "Enable CRUD Mutations", "sectionTitle": "Enable CRUD Mutations", @@ -9382,7 +9396,7 @@ "kind": "section" }, { - "id": "673-zero-cache-config#enable-telemetry", + "id": "674-zero-cache-config#enable-telemetry", "title": "zero-cache Config", "searchTitle": "Enable Telemetry", "sectionTitle": "Enable Telemetry", @@ -9392,7 +9406,7 @@ "kind": "section" }, { - "id": "674-zero-cache-config#initial-sync-table-copy-workers", + "id": "675-zero-cache-config#initial-sync-table-copy-workers", "title": "zero-cache Config", "searchTitle": "Initial Sync Table Copy Workers", "sectionTitle": "Initial Sync Table Copy Workers", @@ -9402,7 +9416,7 @@ "kind": "section" }, { - "id": "675-zero-cache-config#lazy-startup", + "id": "676-zero-cache-config#lazy-startup", "title": "zero-cache Config", "searchTitle": "Lazy Startup", "sectionTitle": "Lazy Startup", @@ -9412,7 +9426,7 @@ "kind": "section" }, { - "id": "676-zero-cache-config#litestream-backup-url", + "id": "677-zero-cache-config#litestream-backup-url", "title": "zero-cache Config", "searchTitle": "Litestream Backup URL", "sectionTitle": "Litestream Backup URL", @@ -9422,7 +9436,7 @@ "kind": "section" }, { - "id": "677-zero-cache-config#litestream-endpoint", + "id": "678-zero-cache-config#litestream-endpoint", "title": "zero-cache Config", "searchTitle": "Litestream Endpoint", "sectionTitle": "Litestream Endpoint", @@ -9432,7 +9446,7 @@ "kind": "section" }, { - "id": "678-zero-cache-config#litestream-checkpoint-threshold-mb", + "id": "679-zero-cache-config#litestream-checkpoint-threshold-mb", "title": "zero-cache Config", "searchTitle": "Litestream Checkpoint Threshold MB", "sectionTitle": "Litestream Checkpoint Threshold MB", @@ -9442,7 +9456,7 @@ "kind": "section" }, { - "id": "679-zero-cache-config#litestream-config-path", + "id": "680-zero-cache-config#litestream-config-path", "title": "zero-cache Config", "searchTitle": "Litestream Config Path", "sectionTitle": "Litestream Config Path", @@ -9452,7 +9466,7 @@ "kind": "section" }, { - "id": "680-zero-cache-config#litestream-config-path-v5", + "id": "681-zero-cache-config#litestream-config-path-v5", "title": "zero-cache Config", "searchTitle": "Litestream Config Path V5", "sectionTitle": "Litestream Config Path V5", @@ -9462,7 +9476,7 @@ "kind": "section" }, { - "id": "681-zero-cache-config#litestream-executable", + "id": "682-zero-cache-config#litestream-executable", "title": "zero-cache Config", "searchTitle": "Litestream Executable", "sectionTitle": "Litestream Executable", @@ -9472,7 +9486,7 @@ "kind": "section" }, { - "id": "682-zero-cache-config#litestream-v5-executable", + "id": "683-zero-cache-config#litestream-v5-executable", "title": "zero-cache Config", "searchTitle": "Litestream V5 Executable", "sectionTitle": "Litestream V5 Executable", @@ -9482,7 +9496,7 @@ "kind": "section" }, { - "id": "683-zero-cache-config#litestream-restore-using-v5", + "id": "684-zero-cache-config#litestream-restore-using-v5", "title": "zero-cache Config", "searchTitle": "Litestream Restore Using V5", "sectionTitle": "Litestream Restore Using V5", @@ -9492,7 +9506,7 @@ "kind": "section" }, { - "id": "684-zero-cache-config#litestream-backup-using-v5", + "id": "685-zero-cache-config#litestream-backup-using-v5", "title": "zero-cache Config", "searchTitle": "Litestream Backup Using V5", "sectionTitle": "Litestream Backup Using V5", @@ -9502,7 +9516,7 @@ "kind": "section" }, { - "id": "685-zero-cache-config#litestream-incremental-backup-interval-minutes", + "id": "686-zero-cache-config#litestream-incremental-backup-interval-minutes", "title": "zero-cache Config", "searchTitle": "Litestream Incremental Backup Interval Minutes", "sectionTitle": "Litestream Incremental Backup Interval Minutes", @@ -9512,7 +9526,7 @@ "kind": "section" }, { - "id": "686-zero-cache-config#litestream-incremental-backup-interval-seconds", + "id": "687-zero-cache-config#litestream-incremental-backup-interval-seconds", "title": "zero-cache Config", "searchTitle": "Litestream Incremental Backup Interval Seconds", "sectionTitle": "Litestream Incremental Backup Interval Seconds", @@ -9522,7 +9536,7 @@ "kind": "section" }, { - "id": "687-zero-cache-config#litestream-maximum-checkpoint-page-count", + "id": "688-zero-cache-config#litestream-maximum-checkpoint-page-count", "title": "zero-cache Config", "searchTitle": "Litestream Maximum Checkpoint Page Count", "sectionTitle": "Litestream Maximum Checkpoint Page Count", @@ -9532,7 +9546,7 @@ "kind": "section" }, { - "id": "688-zero-cache-config#litestream-multipart-concurrency", + "id": "689-zero-cache-config#litestream-multipart-concurrency", "title": "zero-cache Config", "searchTitle": "Litestream Multipart Concurrency", "sectionTitle": "Litestream Multipart Concurrency", @@ -9542,7 +9556,7 @@ "kind": "section" }, { - "id": "689-zero-cache-config#litestream-multipart-size", + "id": "690-zero-cache-config#litestream-multipart-size", "title": "zero-cache Config", "searchTitle": "Litestream Multipart Size", "sectionTitle": "Litestream Multipart Size", @@ -9552,7 +9566,7 @@ "kind": "section" }, { - "id": "690-zero-cache-config#litestream-log-level", + "id": "691-zero-cache-config#litestream-log-level", "title": "zero-cache Config", "searchTitle": "Litestream Log Level", "sectionTitle": "Litestream Log Level", @@ -9562,7 +9576,7 @@ "kind": "section" }, { - "id": "691-zero-cache-config#litestream-port", + "id": "692-zero-cache-config#litestream-port", "title": "zero-cache Config", "searchTitle": "Litestream Port", "sectionTitle": "Litestream Port", @@ -9572,7 +9586,7 @@ "kind": "section" }, { - "id": "692-zero-cache-config#litestream-region", + "id": "693-zero-cache-config#litestream-region", "title": "zero-cache Config", "searchTitle": "Litestream Region", "sectionTitle": "Litestream Region", @@ -9582,7 +9596,7 @@ "kind": "section" }, { - "id": "693-zero-cache-config#litestream-restore-parallelism", + "id": "694-zero-cache-config#litestream-restore-parallelism", "title": "zero-cache Config", "searchTitle": "Litestream Restore Parallelism", "sectionTitle": "Litestream Restore Parallelism", @@ -9592,7 +9606,7 @@ "kind": "section" }, { - "id": "694-zero-cache-config#litestream-snapshot-backup-interval-hours", + "id": "695-zero-cache-config#litestream-snapshot-backup-interval-hours", "title": "zero-cache Config", "searchTitle": "Litestream Snapshot Backup Interval Hours", "sectionTitle": "Litestream Snapshot Backup Interval Hours", @@ -9602,7 +9616,7 @@ "kind": "section" }, { - "id": "695-zero-cache-config#log-format", + "id": "696-zero-cache-config#log-format", "title": "zero-cache Config", "searchTitle": "Log Format", "sectionTitle": "Log Format", @@ -9612,7 +9626,7 @@ "kind": "section" }, { - "id": "696-zero-cache-config#log-ivm-sampling", + "id": "697-zero-cache-config#log-ivm-sampling", "title": "zero-cache Config", "searchTitle": "Log IVM Sampling", "sectionTitle": "Log IVM Sampling", @@ -9622,7 +9636,7 @@ "kind": "section" }, { - "id": "697-zero-cache-config#log-level", + "id": "698-zero-cache-config#log-level", "title": "zero-cache Config", "searchTitle": "Log Level", "sectionTitle": "Log Level", @@ -9632,7 +9646,7 @@ "kind": "section" }, { - "id": "698-zero-cache-config#log-slow-hydrate-threshold", + "id": "699-zero-cache-config#log-slow-hydrate-threshold", "title": "zero-cache Config", "searchTitle": "Log Slow Hydrate Threshold", "sectionTitle": "Log Slow Hydrate Threshold", @@ -9642,7 +9656,7 @@ "kind": "section" }, { - "id": "699-zero-cache-config#log-slow-row-threshold", + "id": "700-zero-cache-config#log-slow-row-threshold", "title": "zero-cache Config", "searchTitle": "Log Slow Row Threshold", "sectionTitle": "Log Slow Row Threshold", @@ -9652,7 +9666,7 @@ "kind": "section" }, { - "id": "700-zero-cache-config#mutate-api-key", + "id": "701-zero-cache-config#mutate-api-key", "title": "zero-cache Config", "searchTitle": "Mutate API Key", "sectionTitle": "Mutate API Key", @@ -9662,7 +9676,7 @@ "kind": "section" }, { - "id": "701-zero-cache-config#mutate-allowed-client-headers", + "id": "702-zero-cache-config#mutate-allowed-client-headers", "title": "zero-cache Config", "searchTitle": "Mutate Allowed Client Headers", "sectionTitle": "Mutate Allowed Client Headers", @@ -9672,7 +9686,7 @@ "kind": "section" }, { - "id": "702-zero-cache-config#mutate-allowed-request-headers", + "id": "703-zero-cache-config#mutate-allowed-request-headers", "title": "zero-cache Config", "searchTitle": "Mutate Allowed Request Headers", "sectionTitle": "Mutate Allowed Request Headers", @@ -9682,7 +9696,7 @@ "kind": "section" }, { - "id": "703-zero-cache-config#mutate-forward-cookies", + "id": "704-zero-cache-config#mutate-forward-cookies", "title": "zero-cache Config", "searchTitle": "Mutate Forward Cookies", "sectionTitle": "Mutate Forward Cookies", @@ -9692,7 +9706,7 @@ "kind": "section" }, { - "id": "704-zero-cache-config#mutate-url", + "id": "705-zero-cache-config#mutate-url", "title": "zero-cache Config", "searchTitle": "Mutate URL", "sectionTitle": "Mutate URL", @@ -9702,7 +9716,7 @@ "kind": "section" }, { - "id": "705-zero-cache-config#number-of-sync-workers", + "id": "706-zero-cache-config#number-of-sync-workers", "title": "zero-cache Config", "searchTitle": "Number of Sync Workers", "sectionTitle": "Number of Sync Workers", @@ -9712,7 +9726,7 @@ "kind": "section" }, { - "id": "706-zero-cache-config#operator-password", + "id": "707-zero-cache-config#operator-password", "title": "zero-cache Config", "searchTitle": "Operator Password", "sectionTitle": "Operator Password", @@ -9722,7 +9736,7 @@ "kind": "section" }, { - "id": "707-zero-cache-config#per-user-mutation-limit-max", + "id": "708-zero-cache-config#per-user-mutation-limit-max", "title": "zero-cache Config", "searchTitle": "Per User Mutation Limit Max", "sectionTitle": "Per User Mutation Limit Max", @@ -9732,7 +9746,7 @@ "kind": "section" }, { - "id": "708-zero-cache-config#per-user-mutation-limit-window-ms", + "id": "709-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)", @@ -9742,7 +9756,7 @@ "kind": "section" }, { - "id": "709-zero-cache-config#pg-replication-slot-failover", + "id": "710-zero-cache-config#pg-replication-slot-failover", "title": "zero-cache Config", "searchTitle": "PG Replication Slot Failover", "sectionTitle": "PG Replication Slot Failover", @@ -9752,7 +9766,7 @@ "kind": "section" }, { - "id": "710-zero-cache-config#port", + "id": "711-zero-cache-config#port", "title": "zero-cache Config", "searchTitle": "Port", "sectionTitle": "Port", @@ -9762,7 +9776,7 @@ "kind": "section" }, { - "id": "711-zero-cache-config#pre-drain-delay-ms", + "id": "712-zero-cache-config#pre-drain-delay-ms", "title": "zero-cache Config", "searchTitle": "Pre-Drain Delay (ms)", "sectionTitle": "Pre-Drain Delay (ms)", @@ -9772,7 +9786,7 @@ "kind": "section" }, { - "id": "712-zero-cache-config#query-api-key", + "id": "713-zero-cache-config#query-api-key", "title": "zero-cache Config", "searchTitle": "Query API Key", "sectionTitle": "Query API Key", @@ -9782,7 +9796,7 @@ "kind": "section" }, { - "id": "713-zero-cache-config#query-allowed-client-headers", + "id": "714-zero-cache-config#query-allowed-client-headers", "title": "zero-cache Config", "searchTitle": "Query Allowed Client Headers", "sectionTitle": "Query Allowed Client Headers", @@ -9792,7 +9806,7 @@ "kind": "section" }, { - "id": "714-zero-cache-config#query-allowed-request-headers", + "id": "715-zero-cache-config#query-allowed-request-headers", "title": "zero-cache Config", "searchTitle": "Query Allowed Request Headers", "sectionTitle": "Query Allowed Request Headers", @@ -9802,7 +9816,7 @@ "kind": "section" }, { - "id": "715-zero-cache-config#query-forward-cookies", + "id": "716-zero-cache-config#query-forward-cookies", "title": "zero-cache Config", "searchTitle": "Query Forward Cookies", "sectionTitle": "Query Forward Cookies", @@ -9812,7 +9826,7 @@ "kind": "section" }, { - "id": "716-zero-cache-config#query-hydration-stats", + "id": "717-zero-cache-config#query-hydration-stats", "title": "zero-cache Config", "searchTitle": "Query Hydration Stats", "sectionTitle": "Query Hydration Stats", @@ -9822,7 +9836,7 @@ "kind": "section" }, { - "id": "717-zero-cache-config#query-url", + "id": "718-zero-cache-config#query-url", "title": "zero-cache Config", "searchTitle": "Query URL", "sectionTitle": "Query URL", @@ -9832,7 +9846,7 @@ "kind": "section" }, { - "id": "718-zero-cache-config#replica-file", + "id": "719-zero-cache-config#replica-file", "title": "zero-cache Config", "searchTitle": "Replica File", "sectionTitle": "Replica File", @@ -9842,7 +9856,7 @@ "kind": "section" }, { - "id": "719-zero-cache-config#replica-vacuum-interval-hours", + "id": "720-zero-cache-config#replica-vacuum-interval-hours", "title": "zero-cache Config", "searchTitle": "Replica Vacuum Interval Hours", "sectionTitle": "Replica Vacuum Interval Hours", @@ -9852,7 +9866,7 @@ "kind": "section" }, { - "id": "720-zero-cache-config#replication-lag-initial-readiness-threshold-ms", + "id": "721-zero-cache-config#replication-lag-initial-readiness-threshold-ms", "title": "zero-cache Config", "searchTitle": "Replication Lag Initial Readiness Threshold (ms)", "sectionTitle": "Replication Lag Initial Readiness Threshold (ms)", @@ -9862,7 +9876,7 @@ "kind": "section" }, { - "id": "721-zero-cache-config#replication-lag-max-readiness-delay-ms", + "id": "722-zero-cache-config#replication-lag-max-readiness-delay-ms", "title": "zero-cache Config", "searchTitle": "Replication Lag Max Readiness Delay (ms)", "sectionTitle": "Replication Lag Max Readiness Delay (ms)", @@ -9872,7 +9886,7 @@ "kind": "section" }, { - "id": "722-zero-cache-config#replication-lag-report-interval-ms", + "id": "723-zero-cache-config#replication-lag-report-interval-ms", "title": "zero-cache Config", "searchTitle": "Replication Lag Report Interval (ms)", "sectionTitle": "Replication Lag Report Interval (ms)", @@ -9882,7 +9896,7 @@ "kind": "section" }, { - "id": "723-zero-cache-config#server-version", + "id": "724-zero-cache-config#server-version", "title": "zero-cache Config", "searchTitle": "Server Version", "sectionTitle": "Server Version", @@ -9892,7 +9906,7 @@ "kind": "section" }, { - "id": "724-zero-cache-config#shadow-sync-enabled", + "id": "725-zero-cache-config#shadow-sync-enabled", "title": "zero-cache Config", "searchTitle": "Shadow Sync Enabled", "sectionTitle": "Shadow Sync Enabled", @@ -9902,7 +9916,7 @@ "kind": "section" }, { - "id": "725-zero-cache-config#shadow-sync-interval-hours", + "id": "726-zero-cache-config#shadow-sync-interval-hours", "title": "zero-cache Config", "searchTitle": "Shadow Sync Interval Hours", "sectionTitle": "Shadow Sync Interval Hours", @@ -9912,7 +9926,7 @@ "kind": "section" }, { - "id": "726-zero-cache-config#shadow-sync-sample-rate", + "id": "727-zero-cache-config#shadow-sync-sample-rate", "title": "zero-cache Config", "searchTitle": "Shadow Sync Sample Rate", "sectionTitle": "Shadow Sync Sample Rate", @@ -9922,7 +9936,7 @@ "kind": "section" }, { - "id": "727-zero-cache-config#shadow-sync-max-rows-per-table", + "id": "728-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", @@ -9932,7 +9946,7 @@ "kind": "section" }, { - "id": "728-zero-cache-config#storage-db-temp-dir", + "id": "729-zero-cache-config#storage-db-temp-dir", "title": "zero-cache Config", "searchTitle": "Storage DB Temp Dir", "sectionTitle": "Storage DB Temp Dir", @@ -9942,7 +9956,7 @@ "kind": "section" }, { - "id": "729-zero-cache-config#task-id", + "id": "730-zero-cache-config#task-id", "title": "zero-cache Config", "searchTitle": "Task ID", "sectionTitle": "Task ID", @@ -9952,7 +9966,7 @@ "kind": "section" }, { - "id": "730-zero-cache-config#upstream-max-connections", + "id": "731-zero-cache-config#upstream-max-connections", "title": "zero-cache Config", "searchTitle": "Upstream Max Connections", "sectionTitle": "Upstream Max Connections", @@ -9962,7 +9976,7 @@ "kind": "section" }, { - "id": "731-zero-cache-config#upstream-pg-replication-slot-failover", + "id": "732-zero-cache-config#upstream-pg-replication-slot-failover", "title": "zero-cache Config", "searchTitle": "Upstream PG Replication Slot Failover", "sectionTitle": "Upstream PG Replication Slot Failover", @@ -9972,7 +9986,7 @@ "kind": "section" }, { - "id": "732-zero-cache-config#upstream-pg-replication-slot-per-replica", + "id": "733-zero-cache-config#upstream-pg-replication-slot-per-replica", "title": "zero-cache Config", "searchTitle": "Upstream PG Replication Slot Per Replica", "sectionTitle": "Upstream PG Replication Slot Per Replica", @@ -9982,7 +9996,7 @@ "kind": "section" }, { - "id": "733-zero-cache-config#upstream-pg-stream-inbound-timeout", + "id": "734-zero-cache-config#upstream-pg-stream-inbound-timeout", "title": "zero-cache Config", "searchTitle": "Upstream PG Stream Inbound Timeout", "sectionTitle": "Upstream PG Stream Inbound Timeout", @@ -9992,7 +10006,7 @@ "kind": "section" }, { - "id": "734-zero-cache-config#view-syncer-hydration-budget-ms", + "id": "735-zero-cache-config#view-syncer-hydration-budget-ms", "title": "zero-cache Config", "searchTitle": "View Syncer Hydration Budget (ms)", "sectionTitle": "View Syncer Hydration Budget (ms)", @@ -10002,7 +10016,7 @@ "kind": "section" }, { - "id": "735-zero-cache-config#view-syncer-query-hydration-timeout-ms", + "id": "736-zero-cache-config#view-syncer-query-hydration-timeout-ms", "title": "zero-cache Config", "searchTitle": "View Syncer Query Hydration Timeout (ms)", "sectionTitle": "View Syncer Query Hydration Timeout (ms)", @@ -10012,7 +10026,7 @@ "kind": "section" }, { - "id": "736-zero-cache-config#websocket-compression", + "id": "737-zero-cache-config#websocket-compression", "title": "zero-cache Config", "searchTitle": "Websocket Compression", "sectionTitle": "Websocket Compression", @@ -10022,7 +10036,7 @@ "kind": "section" }, { - "id": "737-zero-cache-config#websocket-compression-options", + "id": "738-zero-cache-config#websocket-compression-options", "title": "zero-cache Config", "searchTitle": "Websocket Compression Options", "sectionTitle": "Websocket Compression Options", @@ -10032,7 +10046,7 @@ "kind": "section" }, { - "id": "738-zero-cache-config#websocket-max-payload-bytes", + "id": "739-zero-cache-config#websocket-max-payload-bytes", "title": "zero-cache Config", "searchTitle": "Websocket Max Payload Bytes", "sectionTitle": "Websocket Max Payload Bytes", @@ -10042,7 +10056,7 @@ "kind": "section" }, { - "id": "739-zero-cache-config#yield-threshold-ms", + "id": "740-zero-cache-config#yield-threshold-ms", "title": "zero-cache Config", "searchTitle": "Yield Threshold (ms)", "sectionTitle": "Yield Threshold (ms)", @@ -10052,7 +10066,7 @@ "kind": "section" }, { - "id": "740-zero-cache-config#deprecated-flags", + "id": "741-zero-cache-config#deprecated-flags", "title": "zero-cache Config", "searchTitle": "Deprecated Flags", "sectionTitle": "Deprecated Flags", @@ -10062,7 +10076,7 @@ "kind": "section" }, { - "id": "741-zero-cache-config#change-max-connections", + "id": "742-zero-cache-config#change-max-connections", "title": "zero-cache Config", "searchTitle": "Change Max Connections", "sectionTitle": "Change Max Connections", @@ -10072,7 +10086,7 @@ "kind": "section" }, { - "id": "742-zero-cache-config#change-streamer-mode", + "id": "743-zero-cache-config#change-streamer-mode", "title": "zero-cache Config", "searchTitle": "Change Streamer Mode", "sectionTitle": "Change Streamer Mode", @@ -10082,7 +10096,7 @@ "kind": "section" }, { - "id": "743-zero-cache-config#litestream-minimum-checkpoint-page-count", + "id": "744-zero-cache-config#litestream-minimum-checkpoint-page-count", "title": "zero-cache Config", "searchTitle": "Litestream Minimum Checkpoint Page Count", "sectionTitle": "Litestream Minimum Checkpoint Page Count", @@ -10092,7 +10106,7 @@ "kind": "section" }, { - "id": "744-zero-cache-config#auth-jwk", + "id": "745-zero-cache-config#auth-jwk", "title": "zero-cache Config", "searchTitle": "Auth JWK", "sectionTitle": "Auth JWK", @@ -10102,7 +10116,7 @@ "kind": "section" }, { - "id": "745-zero-cache-config#auth-jwks-url", + "id": "746-zero-cache-config#auth-jwks-url", "title": "zero-cache Config", "searchTitle": "Auth JWKS URL", "sectionTitle": "Auth JWKS URL", @@ -10112,7 +10126,7 @@ "kind": "section" }, { - "id": "746-zero-cache-config#auth-secret", + "id": "747-zero-cache-config#auth-secret", "title": "zero-cache Config", "searchTitle": "Auth Secret", "sectionTitle": "Auth Secret", @@ -10256,7 +10270,7 @@ "kind": "page" }, { - "id": "747-zql#create-a-builder", + "id": "748-zql#create-a-builder", "title": "ZQL", "searchTitle": "Create a Builder", "sectionTitle": "Create a Builder", @@ -10266,7 +10280,7 @@ "kind": "section" }, { - "id": "748-zql#select", + "id": "749-zql#select", "title": "ZQL", "searchTitle": "Select", "sectionTitle": "Select", @@ -10276,7 +10290,7 @@ "kind": "section" }, { - "id": "749-zql#ordering", + "id": "750-zql#ordering", "title": "ZQL", "searchTitle": "Ordering", "sectionTitle": "Ordering", @@ -10286,7 +10300,7 @@ "kind": "section" }, { - "id": "750-zql#limit", + "id": "751-zql#limit", "title": "ZQL", "searchTitle": "Limit", "sectionTitle": "Limit", @@ -10296,7 +10310,7 @@ "kind": "section" }, { - "id": "751-zql#paging", + "id": "752-zql#paging", "title": "ZQL", "searchTitle": "Paging", "sectionTitle": "Paging", @@ -10306,7 +10320,7 @@ "kind": "section" }, { - "id": "752-zql#getting-a-single-result", + "id": "753-zql#getting-a-single-result", "title": "ZQL", "searchTitle": "Getting a Single Result", "sectionTitle": "Getting a Single Result", @@ -10316,7 +10330,7 @@ "kind": "section" }, { - "id": "753-zql#relationships", + "id": "754-zql#relationships", "title": "ZQL", "searchTitle": "Relationships", "sectionTitle": "Relationships", @@ -10326,7 +10340,7 @@ "kind": "section" }, { - "id": "754-zql#refining-relationships", + "id": "755-zql#refining-relationships", "title": "ZQL", "searchTitle": "Refining Relationships", "sectionTitle": "Refining Relationships", @@ -10336,7 +10350,7 @@ "kind": "section" }, { - "id": "755-zql#nested-relationships", + "id": "756-zql#nested-relationships", "title": "ZQL", "searchTitle": "Nested Relationships", "sectionTitle": "Nested Relationships", @@ -10346,7 +10360,7 @@ "kind": "section" }, { - "id": "756-zql#where", + "id": "757-zql#where", "title": "ZQL", "searchTitle": "Where", "sectionTitle": "Where", @@ -10356,7 +10370,7 @@ "kind": "section" }, { - "id": "757-zql#comparison-operators", + "id": "758-zql#comparison-operators", "title": "ZQL", "searchTitle": "Comparison Operators", "sectionTitle": "Comparison Operators", @@ -10366,7 +10380,7 @@ "kind": "section" }, { - "id": "758-zql#equals-is-the-default-comparison-operator", + "id": "759-zql#equals-is-the-default-comparison-operator", "title": "ZQL", "searchTitle": "Equals is the Default Comparison Operator", "sectionTitle": "Equals is the Default Comparison Operator", @@ -10376,7 +10390,7 @@ "kind": "section" }, { - "id": "759-zql#comparing-to-null", + "id": "760-zql#comparing-to-null", "title": "ZQL", "searchTitle": "Comparing to null", "sectionTitle": "Comparing to null", @@ -10386,7 +10400,7 @@ "kind": "section" }, { - "id": "760-zql#comparing-to-undefined", + "id": "761-zql#comparing-to-undefined", "title": "ZQL", "searchTitle": "Comparing to undefined", "sectionTitle": "Comparing to undefined", @@ -10396,7 +10410,7 @@ "kind": "section" }, { - "id": "761-zql#compound-filters", + "id": "762-zql#compound-filters", "title": "ZQL", "searchTitle": "Compound Filters", "sectionTitle": "Compound Filters", @@ -10406,7 +10420,7 @@ "kind": "section" }, { - "id": "762-zql#comparing-literal-values", + "id": "763-zql#comparing-literal-values", "title": "ZQL", "searchTitle": "Comparing Literal Values", "sectionTitle": "Comparing Literal Values", @@ -10416,7 +10430,7 @@ "kind": "section" }, { - "id": "763-zql#json-filters", + "id": "764-zql#json-filters", "title": "ZQL", "searchTitle": "JSON Filters", "sectionTitle": "JSON Filters", @@ -10426,7 +10440,7 @@ "kind": "section" }, { - "id": "764-zql#comparisons-are-type-strict", + "id": "765-zql#comparisons-are-type-strict", "title": "ZQL", "searchTitle": "Comparisons Are Type-Strict", "sectionTitle": "Comparisons Are Type-Strict", @@ -10436,7 +10450,7 @@ "kind": "section" }, { - "id": "765-zql#missing-values", + "id": "766-zql#missing-values", "title": "ZQL", "searchTitle": "Missing Values", "sectionTitle": "Missing Values", @@ -10446,7 +10460,7 @@ "kind": "section" }, { - "id": "766-zql#relationship-filters", + "id": "767-zql#relationship-filters", "title": "ZQL", "searchTitle": "Relationship Filters", "sectionTitle": "Relationship Filters", @@ -10456,7 +10470,7 @@ "kind": "section" }, { - "id": "767-zql#aggregates", + "id": "768-zql#aggregates", "title": "ZQL", "searchTitle": "Aggregates", "sectionTitle": "Aggregates", @@ -10466,7 +10480,7 @@ "kind": "section" }, { - "id": "768-zql#distinct", + "id": "769-zql#distinct", "title": "ZQL", "searchTitle": "Distinct", "sectionTitle": "Distinct", @@ -10476,7 +10490,7 @@ "kind": "section" }, { - "id": "769-zql#group-by", + "id": "770-zql#group-by", "title": "ZQL", "searchTitle": "Group By", "sectionTitle": "Group By", @@ -10486,7 +10500,7 @@ "kind": "section" }, { - "id": "770-zql#type-helpers", + "id": "771-zql#type-helpers", "title": "ZQL", "searchTitle": "Type Helpers", "sectionTitle": "Type Helpers", @@ -10496,7 +10510,7 @@ "kind": "section" }, { - "id": "771-zql#planning", + "id": "772-zql#planning", "title": "ZQL", "searchTitle": "Planning", "sectionTitle": "Planning", @@ -10506,7 +10520,7 @@ "kind": "section" }, { - "id": "772-zql#inspecting-query-plans", + "id": "773-zql#inspecting-query-plans", "title": "ZQL", "searchTitle": "Inspecting Query Plans", "sectionTitle": "Inspecting Query Plans", @@ -10516,7 +10530,7 @@ "kind": "section" }, { - "id": "773-zql#manually-flipping-joins", + "id": "774-zql#manually-flipping-joins", "title": "ZQL", "searchTitle": "Manually Flipping Joins", "sectionTitle": "Manually Flipping Joins", @@ -10526,7 +10540,7 @@ "kind": "section" }, { - "id": "774-zql#scalar-subqueries", + "id": "775-zql#scalar-subqueries", "title": "ZQL", "searchTitle": "Scalar Subqueries", "sectionTitle": "Scalar Subqueries", @@ -10536,7 +10550,7 @@ "kind": "section" }, { - "id": "775-zql#why-it-matters", + "id": "776-zql#why-it-matters", "title": "ZQL", "searchTitle": "Why It Matters", "sectionTitle": "Why It Matters", @@ -10546,7 +10560,7 @@ "kind": "section" }, { - "id": "776-zql#trade-offs", + "id": "777-zql#trade-offs", "title": "ZQL", "searchTitle": "Trade-offs", "sectionTitle": "Trade-offs", @@ -10556,7 +10570,7 @@ "kind": "section" }, { - "id": "777-zql#future-work", + "id": "778-zql#future-work", "title": "ZQL", "searchTitle": "Future Work", "sectionTitle": "Future Work", diff --git a/contents/docs/release-notes/1.12.mdx b/contents/docs/release-notes/1.12.mdx index 1e3ebc73..f495bfde 100644 --- a/contents/docs/release-notes/1.12.mdx +++ b/contents/docs/release-notes/1.12.mdx @@ -211,7 +211,36 @@ Before `zero-cache` hydrates a query with `whereExists`, it plans which way to r ]} /> -{/* TODO: CPU with many connections (poke buffer reuse), benchmark still running. */} +### Many Connections + +Every change `zero-cache` syncs reaches each connected client as a poke. In 1.11, every poke allocated a fresh 1 MiB buffer, so with many clients connected the garbage collector was busy cleaning up after them. Zero 1.12 keeps a small buffer per connection and reuses it. With 600 clients connected and 100 writes per second on one sync worker, the sync worker now uses about **1.9x less CPU**. ([ac45c279f](https://github.com/rocicorp/mono/commit/ac45c279f)) + + ## Fixes From dc5024988a67d8f666117e3bc6b6083235bd31ae Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Thu, 12 Nov 2026 11:13:43 +0100 Subject: [PATCH 3/3] docs: count twelve security fixes in the 1.12 release notes --- assets/search-index.json | 8 ++++---- contents/docs/release-notes/1.12.mdx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/search-index.json b/assets/search-index.json index ba733faf..c339b4fb 100644 --- a/assets/search-index.json +++ b/assets/search-index.json @@ -6019,7 +6019,7 @@ "title": "Zero 1.12", "searchTitle": "Zero 1.12", "url": "/docs/release-notes/1.12", - "content": "Installation npm install @rocicorp/zero@1.12 Deploy zero-cache before updating clients. Zero 1.12 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Then set ZERO_CHANGE_STREAMER_SECRET on the view-syncers, and after that on the replication-manager. Overview Zero 1.12 syncs aggregates like count() and sum(), groups rows with groupBy() and distinct(), and can filter on values inside JSON columns. zero-cache hydrates queries and applies updates up to 1.8x faster in our benchmarks, React Native apps start about 3x faster, and each replication-manager now gets its own replication slot. This release also fixes eleven security vulnerabilities, along with a lot of other fixes, especially in replication and connection handling. Security Zero 1.12 includes the fixes for the eleven security vulnerabilities fixed in 1.11.5. See the 1.11.5 release notes for the advisories. We recommend that everyone upgrade. The fix for client-claimed user IDs is off until you turn it on. See Server-Validated User IDs. Some of these fixes change how Zero behaves, so read Breaking Changes before you upgrade, especially the entries on userID and the change-streamer secret. Upgrading Server-Validated User IDs zero-cache can now refuse a userID that your API didn't confirm. To use it: Return the validated userID from your /query and /mutate endpoints, or null for logged-out callers. handleQueryRequest and handleMutateRequest do this when you pass them userID, as recommended since 1.5. Set ZERO_AUTH_REQUIRE_SERVER_USER_ID=true. Until you do, zero-cache trusts the userID the client claims and logs a warning. Apps that verify tokens with ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL don't need this: the token already verifies the userID. (83efa9462, c1f36d79e) RMv2: Replication-Manager Slots We're reworking how replication-managers replicate from Postgres, so that several can run side by side and one can take over from another without a pause. We call this RMv2. Zero 1.12 ships the first step and turns it on by default: each replication-manager now gets its own replication slot. A new replication-manager restores its replica from the backup, catches up on its own slot while the old one keeps serving, and then takes over. Slots of replication-managers that are gone are dropped automatically. (49607b8b3) A new replication-manager also starts about 20 seconds faster, once both the old and the new one are on 1.12. The new zero.replication.server_start metric shows where the start time goes. (522d2ce57, 9aca1b0ab) This needs Litestream v5 backups, the default since 1.11. Expect one slot per running replication-manager in Postgres, each holding WAL until it is consumed. To go back to a shared slot, set ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA=false. That doesn't need a resync. Running several replication-managers at once will come in a later release. Features Queries can return a single aggregate: zql.issue.where('open', true).count(), or zql.issue.max('numVotes'). A relationship can be reduced the same way, so related('comments', q => q.count()) gives each issue its number of comments instead of the comments. Only the result is synced, and it stays up to date as rows change. Counts, sums and averages of direct relationships update optimistically when a mutator changes the related rows. sum(), avg(), min() and max() can also reduce a value inside a JSON column. (c9d1b15fc, e2ecf69ce, 642aece86, 523d3b97e, 8b716b3fe) distinct() returns the distinct combinations of some columns, and groupBy() computes an aggregate per group: zql.issue.groupBy('assigneeID').count(). Groups appear and disappear optimistically for count(), sum() and avg(). (effd2da27, 5493ba549, fe168e640) where() can compare a value inside a json() column: cmp(json('metadata', 'priority'), 'high'). Paths can go through object keys and array indices, comparisons are type-strict, and TypeScript checks the path of a typed JSON column. JSON path filters run on the client, in SQLite and in Postgres. (0a1a0f3d4) Draining behind a load balancer: a new ZERO_PRE_DRAIN_DELAY_MS makes zero-cache fail its readiness check for a while after SIGTERM while still serving, so the load balancer stops routing to it before the drain starts. With sticky sessions, set ZERO_AFFINITY_COOKIE_NAME and zero-cache expires the cookie on connections it turns away while draining. A new zero.sync.drain-time metric estimates how long a view-syncer's drain will take. (f22ec09b6, f77997349, 9a74d2d3c, 04a27634b) Clients retry a failed connection sooner: after 250–500 ms the first time, backing off to the usual 5 seconds by the fifth failure, with jitter so clients that lost the same server don't retry together. A draining zero-cache also tells new connections to retry within a second instead of after 5 seconds. (06622e6ec, a5a940321) Replication status events are republished when the replica grows by 1 GiB or 5%, so consumers that size storage from replicaSize see growth from ordinary writes. (76a0a1bd1) Multi-node deployments can authenticate view-syncers to the replication-manager with a shared ZERO_CHANGE_STREAMER_SECRET. A single-node zero-cache generates its own. (b2b8f68e4) ZERO_ENABLE_HEAPZ=false turns off the /heapz heap-snapshot endpoint while keeping the other diagnostic endpoints and the inspector. (ea3105333) Performance Server-Side Hydration When a client connects or adds a query, zero-cache has to hydrate it: run it against the replica and send the results. In Zero 1.12 this takes about 1.2x less CPU, and about 1.8x less for queries with whereExists, which are common because permissions use them. We measured 100 client groups on one sync worker, each hydrating its user's top issues with their latest comments. (6563d5751, 70c771950, c2882cba0, 5a5fb1af1, c9c34d145, f7233246c, ae794cc0e) Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.12 does this with about 1.4x to 1.7x less CPU than 1.11, which was already 2.5x better than 1.10. We measured the same workload as last release: 100 client groups on one sync worker, each syncing a query with a related subquery. Pushing one write through 300 open queries is about 1.6x faster too. (6563d5751, a35b628ea, 018a90f86, f5d11d924, 8702aea45, fd30e6437) React Native Cold Start When a React Native app starts, Zero loads the rows it has stored on the device into memory and runs the app's queries over them before anything renders. In Zero 1.12 this is about 3x faster. Hermes, the JavaScript engine React Native uses, was spending most of that time sorting rows that were already in order. We measured an app with 180,000 stored rows and 21 queries, on the Android emulator and the iOS simulator. (bfc7e8c24, e7f8a1d91, 8c21e7960, ea4560f4e, e058bfca2, 6375eeb84, 855c495b1) Query Planning Before zero-cache hydrates a query with whereExists, it plans which way to run each join. With three to five whereExists, or nested ones, planning now takes about 2x to 10x less time. (9215b24b2) Many Connections Every change zero-cache syncs reaches each connected client as a poke. In 1.11, every poke allocated a fresh 1 MiB buffer, so with many clients connected the garbage collector was busy cleaning up after them. Zero 1.12 keeps a small buffer per connection and reuses it. With 600 clients connected and 100 writes per second on one sync worker, the sync worker now uses about 1.9x less CPU. (ac45c279f) Fixes Queries and Mutations Server mutators and queries read time with time zone columns as negative milliseconds when the zone offset was positive, and a ServerSchema written by hand with the short timestamptz name read a timestamp as a time of day. (9a08f1e40) Server mutators and queries failed with type \"...\" does not exist for enum and other user-defined Postgres types outside the public schema. Casts are now schema-qualified. (8166580b0) (thanks @jschaf!) A push that failed at the API server could put other tabs of the same client group into the Error state, or fail a socket the client had already replaced. A failed push now affects only the socket that sent it, and that socket's queued pushes are re-sent on the next connection. (945ce6192) (thanks @pawarren!) runAnalyzeCLI --query no longer evaluates the query text as JavaScript. It accepts query builder calls, literals and arrow functions, and rejects anything else with its position. (02b6c1bbd) Clients and React Native On React Native, client garbage collection failed every 5 minutes, and queries whose related() entries weren't in alias order threw during normalization, because Hermes lacks Array.prototype.toSorted. (b62ded385) (thanks @wikil!) A SQLite-backed store whose close() threw blocked every later Zero instance with the same name from opening, and two overlapping close() calls could close a shared SQLite connection under another store. (dee69dc9d, cc861e8f6) (thanks @wikil!) An app that refreshed its auth token while the first connection of a new client group was still being validated could be refused for good with The initConnection message for a new client group must include client schema. (874e1ec9d) (thanks @YevheniiKotyrlo!) After zero-cache was rolled back, a client group holding a query the older server could not parse was stuck in an error state until the server rolled forward. The group is now reset and the client starts a fresh one. (dbf677638) A write that put keys from both the upper Basic Multilingual Plane (U+E000 to U+FFFF) and above it (such as emoji) into the same B-tree node failed with putMany entries must be sorted and unique, and a diff could report such a key as removed and re-added instead of changed. (0e15506e8) (thanks @natew!) Client garbage collection left a store open for every database it inspected. With SQLite, closing Zero then no longer closed its database connection, and a custom StoreProvider whose drop refuses an open store failed every collection. (bd436ef0d) (thanks @wikil!) Client, client group and connection IDs now come from crypto.getRandomValues when available instead of Math.random. (e80d6305c, c2ebe3df5) zero-cache Reliability A view-syncer that reloaded a client group's state after an error kept stale row records and query pipelines, which could crash it with Query … is not in the CVR or Unexpected undefined value, or leave rows that never reached the client. (1137280b6, bb348961f, 9e0e85b62) With a statement_timeout on the CVR database, connecting a client group with many rows could fail with canceling statement due to statement timeout. (892fa553b) View-syncers on slow or throttled disks spent up to a minute inside a single commit running SQLite checkpoints, and were dropped by the replication-manager as slow subscribers. Serving replicas are now checkpointed by a separate process. (fbdb27ed5) Clients were disabled and reloaded with SchemaVersionNotSupported while an upstream migration backfilled a table or column. They now retry with backoff until the backfill is done. (8280af82f) Malformed client messages, such as a second initConnection on a socket, an invalid baseCookie or an invalid query registration, were logged as internal errors. They are now client errors logged at warn. (e1c39de03, f452e99b5, 512baff4b) A client group stays with the user it was first opened for across view-syncer restarts; a connection as a different user is refused the same way it already was while the group was live. (8d9466b13) Inspector authentication is per connection, and the 1.12 client remembers a password the server accepted, so it doesn't prompt again after reconnecting. Repeated wrong admin or operator passwords are now rate limited, on the inspector and on the HTTP diagnostic endpoints. (e186c0d59, c767ac31c, 65375a770) zero-cache logs and traces show a hash of the clientGroupID instead of the ID. (adfce3301, de9121d99) Replication A numeric, float4 or float8 value between about 2^63 and 1e21 stopped replication with a RangeError, a numeric between 2^53 and 2^63 was stored as the wrong integer, and such a value in a row key or query literal failed the client group after a reload. These values now replicate as doubles. (340215ab5) A NULL element in a timestamp[], json[] or int8[] column stopped replication, and in text, enum, boolean and numeric arrays it was stored as \"NULL\", false or null. Array columns are now parsed correctly, including arrays with explicit bounds. Rows already stored keep their old values until they change. (fe4bc732a) A schema-change or lag-report message on the replication stream that zero-cache could not parse stopped replication. Such messages, and schema-change messages that its own upstream functions didn't emit, are now skipped. (08c634549, f6010bb34) With Litestream v5 backups on Google Cloud Storage or Azure, backup files were never compacted, so old ones were never deleted and the backup kept growing. (e2f030e63) Breaking Changes analyze-query bin removed: npx analyze-query is gone. Use a script that calls runAnalyzeCLI. @rocicorp/zero now depends on oxc-parser, which installs a native binding for your platform. (a3932f51e, 02b6c1bbd) userID needs a token: when ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL is set, a connection that passes a userID without an auth token is refused with Unauthorized. Omit userID for logged-out users; the deprecated userID: 'anon' without auth is refused too. (522688120) Changed defaults: ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA is on. It needs Litestream v5 backups, the default since 1.11, and each replication-manager holds its own slot while it runs. Turning it off doesn't resync. (49607b8b3) Multi-node secret: set ZERO_CHANGE_STREAMER_SECRET on the view-syncers first, then on the replication-manager. Until you do, the replication-manager still accepts view-syncers without it, but warns at startup. If one of your nodes runs sync workers and is also the replication-manager for other view-syncers, it now makes up its own secret, so set the secret on every node. (b2b8f68e4) Legacy CRUD mutations are keyed by the primary key of the replica's table, not by the key the client sends. This only matters if your client schema declares a different primary key than the table has in Postgres. (f24fddae7) Rollback: clients keep their local database and unsent mutations when moving between 1.11 and 1.12 in either direction. One exception: a client group that registered an aggregate, groupBy, distinct or JSON path query is reset when zero-cache is rolled back to 1.11, and its unsent mutations are lost. (8b716b3fe, dbf677638)", + "content": "Installation npm install @rocicorp/zero@1.12 Deploy zero-cache before updating clients. Zero 1.12 clients cannot connect to an older server. To roll back, roll back clients first. In multi-node deployments, upgrade the replication-manager before the view-syncers, and roll back the view-syncers first. Then set ZERO_CHANGE_STREAMER_SECRET on the view-syncers, and after that on the replication-manager. Overview Zero 1.12 syncs aggregates like count() and sum(), groups rows with groupBy() and distinct(), and can filter on values inside JSON columns. zero-cache hydrates queries and applies updates up to 1.8x faster in our benchmarks, React Native apps start about 3x faster, and each replication-manager now gets its own replication slot. This release also fixes twelve security vulnerabilities, along with a lot of other fixes, especially in replication and connection handling. Security Zero 1.12 includes the fixes for the twelve security vulnerabilities fixed in 1.11.5. See the 1.11.5 release notes for the advisories. We recommend that everyone upgrade. The fix for client-claimed user IDs is off until you turn it on. See Server-Validated User IDs. Some of these fixes change how Zero behaves, so read Breaking Changes before you upgrade, especially the entries on userID and the change-streamer secret. Upgrading Server-Validated User IDs zero-cache can now refuse a userID that your API didn't confirm. To use it: Return the validated userID from your /query and /mutate endpoints, or null for logged-out callers. handleQueryRequest and handleMutateRequest do this when you pass them userID, as recommended since 1.5. Set ZERO_AUTH_REQUIRE_SERVER_USER_ID=true. Until you do, zero-cache trusts the userID the client claims and logs a warning. Apps that verify tokens with ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL don't need this: the token already verifies the userID. (83efa9462, c1f36d79e) RMv2: Replication-Manager Slots We're reworking how replication-managers replicate from Postgres, so that several can run side by side and one can take over from another without a pause. We call this RMv2. Zero 1.12 ships the first step and turns it on by default: each replication-manager now gets its own replication slot. A new replication-manager restores its replica from the backup, catches up on its own slot while the old one keeps serving, and then takes over. Slots of replication-managers that are gone are dropped automatically. (49607b8b3) A new replication-manager also starts about 20 seconds faster, once both the old and the new one are on 1.12. The new zero.replication.server_start metric shows where the start time goes. (522d2ce57, 9aca1b0ab) This needs Litestream v5 backups, the default since 1.11. Expect one slot per running replication-manager in Postgres, each holding WAL until it is consumed. To go back to a shared slot, set ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA=false. That doesn't need a resync. Running several replication-managers at once will come in a later release. Features Queries can return a single aggregate: zql.issue.where('open', true).count(), or zql.issue.max('numVotes'). A relationship can be reduced the same way, so related('comments', q => q.count()) gives each issue its number of comments instead of the comments. Only the result is synced, and it stays up to date as rows change. Counts, sums and averages of direct relationships update optimistically when a mutator changes the related rows. sum(), avg(), min() and max() can also reduce a value inside a JSON column. (c9d1b15fc, e2ecf69ce, 642aece86, 523d3b97e, 8b716b3fe) distinct() returns the distinct combinations of some columns, and groupBy() computes an aggregate per group: zql.issue.groupBy('assigneeID').count(). Groups appear and disappear optimistically for count(), sum() and avg(). (effd2da27, 5493ba549, fe168e640) where() can compare a value inside a json() column: cmp(json('metadata', 'priority'), 'high'). Paths can go through object keys and array indices, comparisons are type-strict, and TypeScript checks the path of a typed JSON column. JSON path filters run on the client, in SQLite and in Postgres. (0a1a0f3d4) Draining behind a load balancer: a new ZERO_PRE_DRAIN_DELAY_MS makes zero-cache fail its readiness check for a while after SIGTERM while still serving, so the load balancer stops routing to it before the drain starts. With sticky sessions, set ZERO_AFFINITY_COOKIE_NAME and zero-cache expires the cookie on connections it turns away while draining. A new zero.sync.drain-time metric estimates how long a view-syncer's drain will take. (f22ec09b6, f77997349, 9a74d2d3c, 04a27634b) Clients retry a failed connection sooner: after 250–500 ms the first time, backing off to the usual 5 seconds by the fifth failure, with jitter so clients that lost the same server don't retry together. A draining zero-cache also tells new connections to retry within a second instead of after 5 seconds. (06622e6ec, a5a940321) Replication status events are republished when the replica grows by 1 GiB or 5%, so consumers that size storage from replicaSize see growth from ordinary writes. (76a0a1bd1) Multi-node deployments can authenticate view-syncers to the replication-manager with a shared ZERO_CHANGE_STREAMER_SECRET. A single-node zero-cache generates its own. (b2b8f68e4) ZERO_ENABLE_HEAPZ=false turns off the /heapz heap-snapshot endpoint while keeping the other diagnostic endpoints and the inspector. (ea3105333) Performance Server-Side Hydration When a client connects or adds a query, zero-cache has to hydrate it: run it against the replica and send the results. In Zero 1.12 this takes about 1.2x less CPU, and about 1.8x less for queries with whereExists, which are common because permissions use them. We measured 100 client groups on one sync worker, each hydrating its user's top issues with their latest comments. (6563d5751, 70c771950, c2882cba0, 5a5fb1af1, c9c34d145, f7233246c, ae794cc0e) Server-Side Incremental Updates When a transaction replicates, zero-cache has to bring every client group's queries up to date. Zero 1.12 does this with about 1.4x to 1.7x less CPU than 1.11, which was already 2.5x better than 1.10. We measured the same workload as last release: 100 client groups on one sync worker, each syncing a query with a related subquery. Pushing one write through 300 open queries is about 1.6x faster too. (6563d5751, a35b628ea, 018a90f86, f5d11d924, 8702aea45, fd30e6437) React Native Cold Start When a React Native app starts, Zero loads the rows it has stored on the device into memory and runs the app's queries over them before anything renders. In Zero 1.12 this is about 3x faster. Hermes, the JavaScript engine React Native uses, was spending most of that time sorting rows that were already in order. We measured an app with 180,000 stored rows and 21 queries, on the Android emulator and the iOS simulator. (bfc7e8c24, e7f8a1d91, 8c21e7960, ea4560f4e, e058bfca2, 6375eeb84, 855c495b1) Query Planning Before zero-cache hydrates a query with whereExists, it plans which way to run each join. With three to five whereExists, or nested ones, planning now takes about 2x to 10x less time. (9215b24b2) Many Connections Every change zero-cache syncs reaches each connected client as a poke. In 1.11, every poke allocated a fresh 1 MiB buffer, so with many clients connected the garbage collector was busy cleaning up after them. Zero 1.12 keeps a small buffer per connection and reuses it. With 600 clients connected and 100 writes per second on one sync worker, the sync worker now uses about 1.9x less CPU. (ac45c279f) Fixes Queries and Mutations Server mutators and queries read time with time zone columns as negative milliseconds when the zone offset was positive, and a ServerSchema written by hand with the short timestamptz name read a timestamp as a time of day. (9a08f1e40) Server mutators and queries failed with type \"...\" does not exist for enum and other user-defined Postgres types outside the public schema. Casts are now schema-qualified. (8166580b0) (thanks @jschaf!) A push that failed at the API server could put other tabs of the same client group into the Error state, or fail a socket the client had already replaced. A failed push now affects only the socket that sent it, and that socket's queued pushes are re-sent on the next connection. (945ce6192) (thanks @pawarren!) runAnalyzeCLI --query no longer evaluates the query text as JavaScript. It accepts query builder calls, literals and arrow functions, and rejects anything else with its position. (02b6c1bbd) Clients and React Native On React Native, client garbage collection failed every 5 minutes, and queries whose related() entries weren't in alias order threw during normalization, because Hermes lacks Array.prototype.toSorted. (b62ded385) (thanks @wikil!) A SQLite-backed store whose close() threw blocked every later Zero instance with the same name from opening, and two overlapping close() calls could close a shared SQLite connection under another store. (dee69dc9d, cc861e8f6) (thanks @wikil!) An app that refreshed its auth token while the first connection of a new client group was still being validated could be refused for good with The initConnection message for a new client group must include client schema. (874e1ec9d) (thanks @YevheniiKotyrlo!) After zero-cache was rolled back, a client group holding a query the older server could not parse was stuck in an error state until the server rolled forward. The group is now reset and the client starts a fresh one. (dbf677638) A write that put keys from both the upper Basic Multilingual Plane (U+E000 to U+FFFF) and above it (such as emoji) into the same B-tree node failed with putMany entries must be sorted and unique, and a diff could report such a key as removed and re-added instead of changed. (0e15506e8) (thanks @natew!) Client garbage collection left a store open for every database it inspected. With SQLite, closing Zero then no longer closed its database connection, and a custom StoreProvider whose drop refuses an open store failed every collection. (bd436ef0d) (thanks @wikil!) Client, client group and connection IDs now come from crypto.getRandomValues when available instead of Math.random. (e80d6305c, c2ebe3df5) zero-cache Reliability A view-syncer that reloaded a client group's state after an error kept stale row records and query pipelines, which could crash it with Query … is not in the CVR or Unexpected undefined value, or leave rows that never reached the client. (1137280b6, bb348961f, 9e0e85b62) With a statement_timeout on the CVR database, connecting a client group with many rows could fail with canceling statement due to statement timeout. (892fa553b) View-syncers on slow or throttled disks spent up to a minute inside a single commit running SQLite checkpoints, and were dropped by the replication-manager as slow subscribers. Serving replicas are now checkpointed by a separate process. (fbdb27ed5) Clients were disabled and reloaded with SchemaVersionNotSupported while an upstream migration backfilled a table or column. They now retry with backoff until the backfill is done. (8280af82f) Malformed client messages, such as a second initConnection on a socket, an invalid baseCookie or an invalid query registration, were logged as internal errors. They are now client errors logged at warn. (e1c39de03, f452e99b5, 512baff4b) A client group stays with the user it was first opened for across view-syncer restarts; a connection as a different user is refused the same way it already was while the group was live. (8d9466b13) Inspector authentication is per connection, and the 1.12 client remembers a password the server accepted, so it doesn't prompt again after reconnecting. Repeated wrong admin or operator passwords are now rate limited, on the inspector and on the HTTP diagnostic endpoints. (e186c0d59, c767ac31c, 65375a770) zero-cache logs and traces show a hash of the clientGroupID instead of the ID. (adfce3301, de9121d99) Replication A numeric, float4 or float8 value between about 2^63 and 1e21 stopped replication with a RangeError, a numeric between 2^53 and 2^63 was stored as the wrong integer, and such a value in a row key or query literal failed the client group after a reload. These values now replicate as doubles. (340215ab5) A NULL element in a timestamp[], json[] or int8[] column stopped replication, and in text, enum, boolean and numeric arrays it was stored as \"NULL\", false or null. Array columns are now parsed correctly, including arrays with explicit bounds. Rows already stored keep their old values until they change. (fe4bc732a) A schema-change or lag-report message on the replication stream that zero-cache could not parse stopped replication. Such messages, and schema-change messages that its own upstream functions didn't emit, are now skipped. (08c634549, f6010bb34) With Litestream v5 backups on Google Cloud Storage or Azure, backup files were never compacted, so old ones were never deleted and the backup kept growing. (e2f030e63) Breaking Changes analyze-query bin removed: npx analyze-query is gone. Use a script that calls runAnalyzeCLI. @rocicorp/zero now depends on oxc-parser, which installs a native binding for your platform. (a3932f51e, 02b6c1bbd) userID needs a token: when ZERO_AUTH_SECRET, ZERO_AUTH_JWK or ZERO_AUTH_JWKS_URL is set, a connection that passes a userID without an auth token is refused with Unauthorized. Omit userID for logged-out users; the deprecated userID: 'anon' without auth is refused too. (522688120) Changed defaults: ZERO_UPSTREAM_PG_REPLICATION_SLOT_PER_REPLICA is on. It needs Litestream v5 backups, the default since 1.11, and each replication-manager holds its own slot while it runs. Turning it off doesn't resync. (49607b8b3) Multi-node secret: set ZERO_CHANGE_STREAMER_SECRET on the view-syncers first, then on the replication-manager. Until you do, the replication-manager still accepts view-syncers without it, but warns at startup. If one of your nodes runs sync workers and is also the replication-manager for other view-syncers, it now makes up its own secret, so set the secret on every node. (b2b8f68e4) Legacy CRUD mutations are keyed by the primary key of the replica's table, not by the key the client sends. This only matters if your client schema declares a different primary key than the table has in Postgres. (f24fddae7) Rollback: clients keep their local database and unsent mutations when moving between 1.11 and 1.12 in either direction. One exception: a client group that registered an aggregate, groupBy, distinct or JSON path query is reset when zero-cache is rolled back to 1.11, and its unsent mutations are lost. (8b716b3fe, dbf677638)", "headings": [ { "text": "Installation", @@ -6117,7 +6117,7 @@ "sectionTitle": "Overview", "sectionId": "overview", "url": "/docs/release-notes/1.12", - "content": "Zero 1.12 syncs aggregates like count() and sum(), groups rows with groupBy() and distinct(), and can filter on values inside JSON columns. zero-cache hydrates queries and applies updates up to 1.8x faster in our benchmarks, React Native apps start about 3x faster, and each replication-manager now gets its own replication slot. This release also fixes eleven security vulnerabilities, along with a lot of other fixes, especially in replication and connection handling.", + "content": "Zero 1.12 syncs aggregates like count() and sum(), groups rows with groupBy() and distinct(), and can filter on values inside JSON columns. zero-cache hydrates queries and applies updates up to 1.8x faster in our benchmarks, React Native apps start about 3x faster, and each replication-manager now gets its own replication slot. This release also fixes twelve security vulnerabilities, along with a lot of other fixes, especially in replication and connection handling.", "kind": "section" }, { @@ -6127,7 +6127,7 @@ "sectionTitle": "Security", "sectionId": "security", "url": "/docs/release-notes/1.12", - "content": "Zero 1.12 includes the fixes for the eleven security vulnerabilities fixed in 1.11.5. See the 1.11.5 release notes for the advisories. We recommend that everyone upgrade. The fix for client-claimed user IDs is off until you turn it on. See Server-Validated User IDs. Some of these fixes change how Zero behaves, so read Breaking Changes before you upgrade, especially the entries on userID and the change-streamer secret.", + "content": "Zero 1.12 includes the fixes for the twelve security vulnerabilities fixed in 1.11.5. See the 1.11.5 release notes for the advisories. We recommend that everyone upgrade. The fix for client-claimed user IDs is off until you turn it on. See Server-Validated User IDs. Some of these fixes change how Zero behaves, so read Breaking Changes before you upgrade, especially the entries on userID and the change-streamer secret.", "kind": "section" }, { @@ -10579,4 +10579,4 @@ "content": "Scalar subqueries are not currently integrated with Zero's planner. You need to manually choose when to use them.", "kind": "section" } -] +] \ No newline at end of file diff --git a/contents/docs/release-notes/1.12.mdx b/contents/docs/release-notes/1.12.mdx index f495bfde..3997030c 100644 --- a/contents/docs/release-notes/1.12.mdx +++ b/contents/docs/release-notes/1.12.mdx @@ -23,11 +23,11 @@ npm install @rocicorp/zero@1.12 ## Overview -Zero 1.12 syncs aggregates like `count()` and `sum()`, groups rows with `groupBy()` and `distinct()`, and can filter on values inside JSON columns. `zero-cache` hydrates queries and applies updates up to 1.8x faster in our benchmarks, React Native apps start about 3x faster, and each replication-manager now gets its own replication slot. This release also fixes eleven security vulnerabilities, along with a lot of other fixes, especially in replication and connection handling. +Zero 1.12 syncs aggregates like `count()` and `sum()`, groups rows with `groupBy()` and `distinct()`, and can filter on values inside JSON columns. `zero-cache` hydrates queries and applies updates up to 1.8x faster in our benchmarks, React Native apps start about 3x faster, and each replication-manager now gets its own replication slot. This release also fixes twelve security vulnerabilities, along with a lot of other fixes, especially in replication and connection handling. ## Security -Zero 1.12 includes the fixes for the eleven security vulnerabilities fixed in 1.11.5. See the [1.11.5 release notes](/docs/release-notes/1.11#1115) for the advisories. We recommend that everyone upgrade. The fix for client-claimed user IDs is off until you turn it on. See [Server-Validated User IDs](#server-validated-user-ids). +Zero 1.12 includes the fixes for the twelve security vulnerabilities fixed in 1.11.5. See the [1.11.5 release notes](/docs/release-notes/1.11#1115) for the advisories. We recommend that everyone upgrade. The fix for client-claimed user IDs is off until you turn it on. See [Server-Validated User IDs](#server-validated-user-ids). Some of these fixes change how Zero behaves, so read [Breaking Changes](#breaking-changes) before you upgrade, especially the entries on `userID` and the change-streamer secret.