Repository: rocicorp/mono
Pull request #4265: feat(zero-cache): Add statz endpoint.
Author: login: aboodman
Created: 2025-04-25T06:34:54Z | Merged: 2025-04-30T01:39:03Z

Also remove script as I don't think we need both.


Diff:
diff --git a/package-lock.json b/package-lock.json
index 1cd2f5aed8..c6c2331d9d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8456,6 +8456,14 @@
         "@babel/types": "^7.3.0"
       }
     },
+    "node_modules/@types/basic-auth": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/@types/basic-auth/-/basic-auth-1.1.8.tgz",
+      "integrity": "sha512-dKcUeixGuZn8pBjcUrf1N7x5K6lWuKuwHHitM2IZ4vwZUDWEhhNtwCWiba8jTA9zn0GQQ+fTFkWpKx8pOU/enw==",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/body-parser": {
       "version": "1.19.5",
       "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -10516,6 +10524,22 @@
         }
       ]
     },
+    "node_modules/basic-auth": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+      "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+      "dependencies": {
+        "safe-buffer": "5.1.2"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/basic-auth/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
     "node_modules/batch": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -27210,6 +27234,8 @@
         "@rocicorp/logger": "^5.4.0",
         "@rocicorp/resolver": "^1.0.2",
         "@rocicorp/zero-sqlite3": "^1.0.4",
+        "@types/basic-auth": "^1.1.8",
+        "basic-auth": "^2.0.1",
         "chalk": "^5.3.0",
         "chalk-template": "^1.1.0",
         "chokidar": "^4.0.1",
@@ -27238,7 +27264,6 @@
       "bin": {
         "analyze-query": "out/zero/src/analyze-query.js",
         "ast-to-zql": "out/zero/src/ast-to-zql.js",
-        "cache-stats": "out/zero/src/cache-stats.js",
         "transform-query": "out/zero/src/transform-query.js",
         "zero-build-schema": "out/zero/src/build-schema.js",
         "zero-cache": "out/zero/src/cli.js",
@@ -27287,6 +27312,8 @@
         "@rocicorp/logger": "^5.4.0",
         "@rocicorp/resolver": "^1.0.2",
         "@rocicorp/zero-sqlite3": "^1.0.4",
+        "@types/basic-auth": "^1.1.8",
+        "basic-auth": "^2.0.1",
         "chalk": "^5.3.0",
         "compare-utf8": "^0.1.1",
         "defu": "^6.1.4",
@@ -32002,7 +32029,9 @@
         "@rocicorp/prettier-config": "^0.3.0",
         "@rocicorp/resolver": "^1.0.2",
         "@rocicorp/zero-sqlite3": "^1.0.4",
+        "@types/basic-auth": "^1.1.8",
         "@vitest/runner": "3.0.8",
+        "basic-auth": "^2.0.1",
         "chalk": "^5.3.0",
         "chalk-template": "^1.1.0",
         "chokidar": "^4.0.1",
@@ -32631,6 +32660,14 @@
         "@babel/types": "^7.3.0"
       }
     },
+    "@types/basic-auth": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/@types/basic-auth/-/basic-auth-1.1.8.tgz",
+      "integrity": "sha512-dKcUeixGuZn8pBjcUrf1N7x5K6lWuKuwHHitM2IZ4vwZUDWEhhNtwCWiba8jTA9zn0GQQ+fTFkWpKx8pOU/enw==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/body-parser": {
       "version": "1.19.5",
       "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -34190,6 +34227,21 @@
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
       "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
     },
+    "basic-auth": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+      "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+      "requires": {
+        "safe-buffer": "5.1.2"
+      },
+      "dependencies": {
+        "safe-buffer": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+        }
+      }
+    },
     "batch": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -45339,11 +45391,13 @@
         "@rocicorp/resolver": "^1.0.2",
         "@rocicorp/zero-sqlite3": "^1.0.4",
         "@testcontainers/postgresql": "^10.9.0",
+        "@types/basic-auth": "^1.1.8",
         "@types/node": "^20.8.4",
         "@types/pg-format": "^1.0.5",
         "@types/strip-ansi": "^3.0.0",
         "@types/ws": "^8.5.12",
         "@vitest/runner": "3.0.8",
+        "basic-auth": "^2.0.1",
         "chalk": "^5.3.0",
         "compare-utf8": "^0.1.1",
         "concurrently": "^8.2.1",
diff --git a/packages/analyze-query/src/bin-cache-stats.ts b/packages/analyze-query/src/bin-cache-stats.ts
deleted file mode 100644
index 29d7de015c..0000000000
--- a/packages/analyze-query/src/bin-cache-stats.ts
+++ /dev/null
@@ -1,348 +0,0 @@
-/* eslint-disable no-console */
-import '@dotenvx/dotenvx/config';
-import {astToZQL} from '../../ast-to-zql/src/ast-to-zql.ts';
-import {createSilentLogContext} from '../../shared/src/logging-test-utils.ts';
-import {parseOptions} from '../../shared/src/options.ts';
-import * as v from '../../shared/src/valita.ts';
-import {
-  appOptions,
-  shardOptions,
-  ZERO_ENV_VAR_PREFIX,
-  zeroOptions,
-} from '../../zero-cache/src/config/zero-config.ts';
-import {pgClient} from '../../zero-cache/src/types/pg.ts';
-import {getShardID, upstreamSchema} from '../../zero-cache/src/types/shards.ts';
-import {BigIntJSON} from '../../zero-cache/src/types/bigint-json.ts';
-import chalk from 'chalk';
-import {Database} from '../../zqlite/src/db.ts';
-import fs from 'fs';
-import os from 'os';
-
-const options = {
-  upstream: {
-    db: v.string(),
-  },
-  cvr: {
-    db: v.string(),
-  },
-  change: {
-    db: v.string(),
-  },
-  app: appOptions,
-  shard: shardOptions,
-  dumpZql: v.boolean().optional(),
-  detailed: v.boolean().optional(),
-  replica: zeroOptions.replica,
-};
-
-const config = parseOptions(
-  options,
-  process.argv.slice(2),
-  ZERO_ENV_VAR_PREFIX,
-);
-
-const lc = createSilentLogContext();
-
-async function upstreamStats() {
-  const schema = upstreamSchema(getShardID(config));
-  const sql = pgClient(lc, config.upstream.db);
-
-  await printPgStats([
-    [
-      'num replicas',
-      sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."replicas"`,
-    ],
-    [
-      'num clients with mutations',
-      sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."clients"`,
-    ],
-    [
-      'num mutations processed',
-      sql`SELECT SUM("lastMutationID") as "c" FROM ${sql(schema)}."clients"`,
-    ],
-  ]);
-
-  await sql.end();
-}
-
-async function cvrStats() {
-  const schema = upstreamSchema(getShardID(config)) + '/cvr';
-  const sql = pgClient(lc, config.cvr.db);
-
-  function numQueriesPerClientGroup(
-    active: boolean,
-  ): ReturnType<ReturnType<typeof pgClient>> {
-    const filter = active
-      ? sql`WHERE "inactivatedAt" IS NULL AND deleted = false`
-      : sql`WHERE "inactivatedAt" IS NOT NULL AND ("inactivatedAt" + "ttl") > NOW()`;
-    return sql`WITH 
-    group_counts AS (
-      SELECT 
-        "clientGroupID",
-        COUNT(*) AS num_queries
-      FROM ${sql(schema)}."desires"
-      ${filter}
-      GROUP BY "clientGroupID"
-    ),
-    -- Count distinct clientIDs per clientGroupID
-    client_per_group_counts AS (
-      SELECT 
-        "clientGroupID",
-        COUNT(DISTINCT "clientID") AS num_clients
-      FROM ${sql(schema)}."desires"
-      ${filter}
-      GROUP BY "clientGroupID"
-    )
-    -- Combine all the information
-    SELECT 
-      g."clientGroupID",
-      cpg.num_clients,
-      g.num_queries
-    FROM group_counts g
-    JOIN client_per_group_counts cpg ON g."clientGroupID" = cpg."clientGroupID"
-    ORDER BY g.num_queries DESC;`;
-  }
-
-  await printPgStats([
-    [
-      'total num queries',
-      sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires"`,
-    ],
-    [
-      'num unique query hashes',
-      sql`SELECT COUNT(DISTINCT "queryHash") as "c" FROM ${sql(
-        schema,
-      )}."desires"`,
-    ],
-    [
-      'num active queries',
-      sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "inactivatedAt" IS NULL AND "deleted" = false`,
-    ],
-    [
-      'num inactive queries',
-      sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "inactivatedAt" IS NOT NULL AND ("inactivatedAt" + "ttl") > NOW()`,
-    ],
-    [
-      'num deleted queries',
-      sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "deleted" = true`,
-    ],
-    [
-      'fresh queries percentiles',
-      sql`WITH client_group_counts AS (
-        -- Count inactive desires per clientGroupID
-        SELECT
-          "clientGroupID",
-          COUNT(*) AS fresh_count
-        FROM ${sql(schema)}."desires"
-        WHERE 
-          ("inactivatedAt" IS NOT NULL 
-          AND ("inactivatedAt" + "ttl") > NOW()) OR ("inactivatedAt" IS NULL
-          AND deleted = false)
-        GROUP BY "clientGroupID"
-      )
-      
-      SELECT
-        percentile_cont(0.50) WITHIN GROUP (ORDER BY fresh_count) AS "p50",
-        percentile_cont(0.75) WITHIN GROUP (ORDER BY fresh_count) AS "p75",
-        percentile_cont(0.90) WITHIN GROUP (ORDER BY fresh_count) AS "p90",
-        percentile_cont(0.95) WITHIN GROUP (ORDER BY fresh_count) AS "p95",
-        percentile_cont(0.99) WITHIN GROUP (ORDER BY fresh_count) AS "p99",
-        MIN(fresh_count) AS "min",
-        MAX(fresh_count) AS "max",
-        AVG(fresh_count) AS "avg"
-      FROM client_group_counts;`,
-    ],
-    [
-      'rows per client group percentiles',
-      sql`WITH client_group_counts AS (
-        -- Count inactive desires per clientGroupID
-        SELECT
-          "clientGroupID",
-          COUNT(*) AS row_count
-        FROM ${sql(schema)}."rows"
-        GROUP BY "clientGroupID"
-      )
-      SELECT
-        percentile_cont(0.50) WITHIN GROUP (ORDER BY row_count) AS "p50",
-        percentile_cont(0.75) WITHIN GROUP (ORDER BY row_count) AS "p75",
-        percentile_cont(0.90) WITHIN GROUP (ORDER BY row_count) AS "p90",
-        percentile_cont(0.95) WITHIN GROUP (ORDER BY row_count) AS "p95",
-        percentile_cont(0.99) WITHIN GROUP (ORDER BY row_count) AS "p99",
-        MIN(row_count) AS "min",
-        MAX(row_count) AS "max",
-        AVG(row_count) AS "avg"
-      FROM client_group_counts;`,
-    ],
-    [
-      // check for AST blowup due to DNF conversion.
-      'ast sizes',
-      sql`SELECT 
-        percentile_cont(0.25) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "25th_percentile",
-        percentile_cont(0.5) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "50th_percentile",
-        percentile_cont(0.75) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "75th_percentile",
-        percentile_cont(0.9) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "90th_percentile",
-        percentile_cont(0.95) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "95th_percentile",
-        percentile_cont(0.99) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "99th_percentile",
-        MIN(length("clientAST"::text)) AS "minimum_length",
-        MAX(length("clientAST"::text)) AS "maximum_length",
-        AVG(length("clientAST"::text))::integer AS "average_length",
-        COUNT(*) AS "total_records"
-      FROM ${sql(schema)}."queries";`,
-    ],
-    [
-      // output the hash of the largest AST
-      'biggest ast hash',
-      sql`SELECT "queryHash", length("clientAST"::text) AS "ast_length"
-      FROM ${sql(schema)}."queries"
-      ORDER BY length("clientAST"::text) DESC
-      LIMIT 1;`,
-    ],
-    ...((config.detailed
-      ? [
-          [
-            'total active queries per client and client group',
-            numQueriesPerClientGroup(true),
-          ],
-          [
-            'total inactive queries per client and client group',
-            numQueriesPerClientGroup(false),
-          ],
-          [
-            'total rows per client group',
-            sql`SELECT "clientGroupID", COUNT(*) as "c" FROM ${sql(
-              schema,
-            )}."rows" GROUP BY "clientGroupID" ORDER BY "c" DESC`,
-          ],
-          [
-            'num rows per query',
-            sql`SELECT 
-        k.key AS "queryHash",
-        COUNT(*) AS row_count
-      FROM ${sql(schema)}."rows" r,
-      LATERAL jsonb_each(r."refCounts") k
-      GROUP BY k.key
-      ORDER BY row_count DESC;`,
-          ],
-        ]
-      : []) satisfies [
-      name: string,
-      query: ReturnType<ReturnType<typeof pgClient>>,
-    ][]),
-  ]);
-
-  if (config.dumpZql) {
-    console.log(chalk.blue.bold('ZQL (without permissions) for each query:'));
-    const queryAsts =
-      await sql`SELECT "queryHash", "clientAST" FROM ${sql(schema)}."queries"`;
-
-    const seenQueries = new Set<string>();
-    const parseFailures: string[] = [];
-    for (const row of queryAsts) {
-      const {queryHash, clientAST} = row;
-      if (seenQueries.has(queryHash)) {
-        continue;
-      }
-      seenQueries.add(queryHash);
-
-      try {
-        const zql = clientAST.table + astToZQL(clientAST);
-        console.log(chalk.red.bold('HASH:'), queryHash);
-        console.log(chalk.red.bold('ZQL:'), zql, '\n');
-      } catch (e) {
-        console.log(e);
-        parseFailures.push(queryHash);
-      }
-    }
-    if (parseFailures.length > 0) {
-      console.log('Failed to parse the following hashes:', parseFailures);
-    }
-  }
-
-  await sql.end();
-}
-
-async function changelogStats() {
-  const schema = upstreamSchema(getShardID(config)) + '/cdc';
-  const sql = pgClient(lc, config.change.db);
-
-  await printPgStats([
-    [
-      'change log size',
-      sql`SELECT COUNT(*) as "change_log_size" FROM ${sql(schema)}."changeLog"`,
-    ],
-  ]);
-  await sql.end();
-}
-
-function replicaStats() {
-  const db = new Database(lc, config.replica.file);
-  printStats('replica', [
-    ['wal checkpoint', pick(first(db.pragma('WAL_CHECKPOINT')))],
-    ['page count', pick(first(db.pragma('PAGE_COUNT')))],
-    ['page size', pick(first(db.pragma('PAGE_SIZE')))],
-    ['journal mode', pick(first(db.pragma('JOURNAL_MODE')))],
-    ['synchronous', pick(first(db.pragma('SYNCHRONOUS')))],
-    ['cache size', pick(first(db.pragma('CACHE_SIZE')))],
-    ['auto vacuum', pick(first(db.pragma('AUTO_VACUUM')))],
-    ['freelist count', pick(first(db.pragma('FREELIST_COUNT')))],
-    ['wal autocheckpoint', pick(first(db.pragma('WAL_AUTOCHECKPOINT')))],
-    ['db file stats', fs.statSync(config.replica.file)],
-  ] as const);
-}
-
-function osStats() {
-  printStats('os', [
-    ['load avg', os.loadavg()],
-    ['uptime', os.uptime()],
-    ['total mem', os.totalmem()],
-    ['free mem', os.freemem()],
-    ['cpus', os.cpus().length],
-    ['platform', os.platform()],
-    ['arch', os.arch()],
-    ['release', os.release()],
-    ['uptime', os.uptime()],
-  ] as const);
-}
-
-async function printPgStats(
-  pendingQueries: [
-    name: string,
-    query: ReturnType<ReturnType<typeof pgClient>>,
-  ][],
-) {
-  const results = await Promise.all(
-    pendingQueries.map(async ([name, query]) => [name, await query]),
-  );
-  for (const result of results) {
-    console.log('\n', chalk.blue.bold(result[0]), '\n');
-    console.log(BigIntJSON.stringify(result[1], null, 2));
-  }
-}
-
-function printStats(
-  group: string,
-  queries: readonly [name: string, result: unknown][],
-) {
-  console.log(chalk.blue.bold(`\n=== ${group} Stats: ===`));
-  for (const [name, result] of queries) {
-    console.log(
-      '\n',
-      chalk.blue.bold(name),
-      BigIntJSON.stringify(result, null, 2),
-    );
-  }
-}
-
-await changelogStats();
-await upstreamStats();
-await cvrStats();
-replicaStats();
-osStats();
-
-function first(x: object[]): object {
-  return x[0];
-}
-
-function pick(x: object): unknown {
-  return Object.values(x)[0];
-}
diff --git a/packages/zero-cache/package.json b/packages/zero-cache/package.json
index b2a5a47a32..ec421a052d 100644
--- a/packages/zero-cache/package.json
+++ b/packages/zero-cache/package.json
@@ -37,6 +37,8 @@
     "@rocicorp/logger": "^5.4.0",
     "@rocicorp/resolver": "^1.0.2",
     "@rocicorp/zero-sqlite3": "^1.0.4",
+    "@types/basic-auth": "^1.1.8",
+    "basic-auth": "^2.0.1",
     "chalk": "^5.3.0",
     "compare-utf8": "^0.1.1",
     "defu": "^6.1.4",
diff --git a/packages/zero-cache/src/config/zero-config.test.ts b/packages/zero-cache/src/config/zero-config.test.ts
index 716628cb64..779d4059a5 100644
--- a/packages/zero-cache/src/config/zero-config.test.ts
+++ b/packages/zero-cache/src/config/zero-config.test.ts
@@ -225,6 +225,11 @@ test('zero-cache --help', () => {
                                                                 clients. This is a heavy-weight operation and can result in user-visible                          
                                                                 slowness or downtime if compute resources are scarce.                                             
                                                                                                                                                                   
+     --admin-password string                                    optional                                                                                          
+       ZERO_ADMIN_PASSWORD env                                                                                                                                    
+                                                                A password used to administer zero-cache server, for example to access the                        
+                                                                /statz endpoint.                                                                                  
+                                                                                                                                                                  
      --litestream-executable string                             optional                                                                                          
        ZERO_LITESTREAM_EXECUTABLE env                                                                                                                             
                                                                 Path to the litestream executable. This option has no effect if                                   
diff --git a/packages/zero-cache/src/config/zero-config.ts b/packages/zero-cache/src/config/zero-config.ts
index c7e18b0b04..fcaa3466cb 100644
--- a/packages/zero-cache/src/config/zero-config.ts
+++ b/packages/zero-cache/src/config/zero-config.ts
@@ -341,6 +341,14 @@ export const zeroOptions = {
     ],
   },
 
+  adminPassword: {
+    type: v.string().optional(),
+    desc: [
+      `A password used to administer zero-cache server, for example to access the`,
+      `/statz endpoint.`,
+    ],
+  },
+
   litestream: {
     executable: {
       type: v.string().optional(),
@@ -532,16 +540,16 @@ export const zeroOptions = {
   },
 };
 
-export type ZeroConfig = Config<typeof zeroOptions>;
+export type ZeroConfig = ReturnType<typeof getZeroConfig>;
 
 export const ZERO_ENV_VAR_PREFIX = 'ZERO_';
 
-let loadedConfig: ZeroConfig | undefined;
+let loadedConfig: Config<typeof zeroOptions> | undefined;
 
 export function getZeroConfig(
   env: NodeJS.ProcessEnv = process.env,
   argv = process.argv.slice(2),
-): ZeroConfig {
+) {
   if (!loadedConfig || singleProcessMode()) {
     loadedConfig = parseOptions(zeroOptions, argv, ZERO_ENV_VAR_PREFIX, env);
 
@@ -550,5 +558,21 @@ export function getZeroConfig(
     }
   }
 
-  return loadedConfig;
+  return normalizeZeroConfig(loadedConfig);
+}
+
+export function normalizeZeroConfig<T extends Config<typeof zeroOptions>>(
+  config: T,
+) {
+  return {
+    ...config,
+    cvr: {
+      ...config.cvr,
+      db: config.cvr.db ?? config.upstream.db,
+    },
+    change: {
+      ...config.change,
+      db: config.change.db ?? config.upstream.db,
+    },
+  };
 }
diff --git a/packages/zero-cache/src/server/change-streamer.ts b/packages/zero-cache/src/server/change-streamer.ts
index 0a4bd31363..1762c54187 100644
--- a/packages/zero-cache/src/server/change-streamer.ts
+++ b/packages/zero-cache/src/server/change-streamer.ts
@@ -103,6 +103,7 @@ export default async function runWorker(
   assert(changeStreamer, `resetting replica did not advance replicaVersion`);
 
   const changeStreamerWebServer = new ChangeStreamerHttpServer(
+    config,
     lc,
     changeStreamer,
     {port},
diff --git a/packages/zero-cache/src/server/main.ts b/packages/zero-cache/src/server/main.ts
index 16e8464fca..c90eb0baef 100644
--- a/packages/zero-cache/src/server/main.ts
+++ b/packages/zero-cache/src/server/main.ts
@@ -203,7 +203,7 @@ export default async function runWorker(
 
   if (numSyncers) {
     mainServices.push(
-      new Dispatcher(lc, taskID, parent, syncers, mutator, {port}),
+      new Dispatcher(config, lc, taskID, parent, syncers, mutator, {port}),
     );
   } else if (changeStreamer && parent) {
     // When running as the replication-manager, the dispatcher process
diff --git a/packages/zero-cache/src/server/runner/config.test.ts b/packages/zero-cache/src/server/runner/config.test.ts
index f9af7ad6b3..5b3ba697e9 100644
--- a/packages/zero-cache/src/server/runner/config.test.ts
+++ b/packages/zero-cache/src/server/runner/config.test.ts
@@ -59,9 +59,11 @@ test('parse options', () => {
         "auth": {},
         "autoReset": true,
         "change": {
+          "db": "foo",
           "maxConns": 5,
         },
         "cvr": {
+          "db": "foo",
           "maxConns": 30,
         },
         "initialSync": {
@@ -478,6 +480,11 @@ test('zero-cache --help', () => {
                                                                 clients. This is a heavy-weight operation and can result in user-visible                          
                                                                 slowness or downtime if compute resources are scarce.                                             
                                                                                                                                                                   
+     --admin-password string                                    optional                                                                                          
+       ZERO_ADMIN_PASSWORD env                                                                                                                                    
+                                                                A password used to administer zero-cache server, for example to access the                        
+                                                                /statz endpoint.                                                                                  
+                                                                                                                                                                  
      --litestream-executable string                             optional                                                                                          
        ZERO_LITESTREAM_EXECUTABLE env                                                                                                                             
                                                                 Path to the litestream executable. This option has no effect if                                   
diff --git a/packages/zero-cache/src/server/runner/config.ts b/packages/zero-cache/src/server/runner/config.ts
index a65f40bc98..c17191f4fe 100644
--- a/packages/zero-cache/src/server/runner/config.ts
+++ b/packages/zero-cache/src/server/runner/config.ts
@@ -1,10 +1,9 @@
 import {
   envSchema,
   parseOptionsAdvanced,
-  type Config,
 } from '../../../../shared/src/options.ts';
 import * as v from '../../../../shared/src/valita.ts';
-import {zeroOptions} from '../../config/zero-config.ts';
+import {normalizeZeroConfig, zeroOptions} from '../../config/zero-config.ts';
 
 const ENV_VAR_PREFIX = 'ZERO_';
 
@@ -107,13 +106,12 @@ const tenantsSchema = v
     return v.ok(val);
   });
 
-export type MultiZeroConfig = v.Infer<typeof tenantsSchema> &
-  Omit<Config<typeof multiConfigSchema>, 'tenantsJSON'>;
+export type MultiZeroConfig = ReturnType<typeof getMultiZeroConfig>;
 
 export function getMultiZeroConfig(
   processEnv: NodeJS.ProcessEnv = process.env,
   argv = process.argv.slice(2),
-): {config: MultiZeroConfig; env: NodeJS.ProcessEnv} {
+) {
   const {
     config: {tenantsJSON, ...config},
     env,
@@ -128,5 +126,5 @@ export function getMultiZeroConfig(
   const tenantsConfig = tenantsJSON
     ? v.parse(JSON.parse(tenantsJSON), tenantsSchema)
     : {tenants: []};
-  return {config: {...config, ...tenantsConfig}, env};
+  return {config: {...normalizeZeroConfig(config), ...tenantsConfig}, env};
 }
diff --git a/packages/zero-cache/src/server/runner/run-worker.ts b/packages/zero-cache/src/server/runner/run-worker.ts
index d73919b64f..b0a5f7efcf 100644
--- a/packages/zero-cache/src/server/runner/run-worker.ts
+++ b/packages/zero-cache/src/server/runner/run-worker.ts
@@ -114,7 +114,7 @@ export async function runWorker(
     await runUntilKilled(
       lc,
       parent ?? process,
-      new ZeroDispatcher(lc, runAsReplicationManager, tenants, {
+      new ZeroDispatcher(config, lc, runAsReplicationManager, tenants, {
         port: runAsReplicationManager ? changeStreamerPort : port,
       }),
     );
diff --git a/packages/zero-cache/src/server/runner/zero-dispatcher.ts b/packages/zero-cache/src/server/runner/zero-dispatcher.ts
index 3d665341e0..33aa74cd90 100644
--- a/packages/zero-cache/src/server/runner/zero-dispatcher.ts
+++ b/packages/zero-cache/src/server/runner/zero-dispatcher.ts
@@ -7,6 +7,7 @@ import {
 import {HttpService, type Options} from '../../services/http-service.ts';
 import type {IncomingMessageSubset} from '../../types/http.ts';
 import type {Worker} from '../../types/processes.ts';
+import type {ZeroConfig} from '../../config/zero-config.ts';
 
 type Tenant = {
   // Note: The empty signifies the sole tenant. This can only be provided
@@ -24,12 +25,13 @@ export class ZeroDispatcher extends HttpService {
   readonly #runAsReplicationManager: boolean;
 
   constructor(
+    config: ZeroConfig,
     lc: LogContext,
     runAsReplicationManager: boolean,
     tenants: Tenant[],
     opts: Options,
   ) {
-    super('zero-dispatcher', lc, opts, fastify => {
+    super('zero-dispatcher', config, lc, opts, fastify => {
       installWebSocketHandoff(lc, this.#handoff, fastify.server);
     });
 
diff --git a/packages/zero-cache/src/services/change-streamer/change-streamer-http.test.ts b/packages/zero-cache/src/services/change-streamer/change-streamer-http.test.ts
index e17de1c578..10782052d6 100644
--- a/packages/zero-cache/src/services/change-streamer/change-streamer-http.test.ts
+++ b/packages/zero-cache/src/services/change-streamer/change-streamer-http.test.ts
@@ -24,6 +24,7 @@ import {
 } from './change-streamer-http.ts';
 import type {Downstream, SubscriberContext} from './change-streamer.ts';
 import {PROTOCOL_VERSION} from './change-streamer.ts';
+import type {ZeroConfig} from '../../config/zero-config.ts';
 
 describe('change-streamer/http', () => {
   let lc: LogContext;
@@ -47,17 +48,26 @@ describe('change-streamer/http', () => {
 
     const [parent, receiver] = inProcChannel();
 
-    dispatcher = new HttpService('dispatcher', lc, {port: 0}, fastify => {
-      installWebSocketHandoff(
-        lc,
-        req => ({payload: getSubscriberContext(req), receiver}),
-        fastify.server,
-      );
-    });
+    const config = {} as unknown as ZeroConfig;
+
+    dispatcher = new HttpService(
+      'dispatcher',
+      config,
+      lc,
+      {port: 0},
+      fastify => {
+        installWebSocketHandoff(
+          lc,
+          req => ({payload: getSubscriberContext(req), receiver}),
+          fastify.server,
+        );
+      },
+    );
 
     // Run the server for real instead of using `injectWS()`, as that has a
     // different behavior for ws.close().
     server = new ChangeStreamerHttpServer(
+      config,
       lc,
       {subscribe: subscribeFn.mockResolvedValue(downstream)},
       {port: 0},
diff --git a/packages/zero-cache/src/services/change-streamer/change-streamer-http.ts b/packages/zero-cache/src/services/change-streamer/change-streamer-http.ts
index e5dc477a9a..bc19340e17 100644
--- a/packages/zero-cache/src/services/change-streamer/change-streamer-http.ts
+++ b/packages/zero-cache/src/services/change-streamer/change-streamer-http.ts
@@ -16,6 +16,7 @@ import {
   type Downstream,
   type SubscriberContext,
 } from './change-streamer.ts';
+import type {ZeroConfig} from '../../config/zero-config.ts';
 
 const MIN_SUPPORTED_PROTOCOL_VERSION = 1;
 
@@ -31,12 +32,13 @@ export class ChangeStreamerHttpServer extends HttpService {
   readonly #delegate: ChangeStreamer;
 
   constructor(
+    config: ZeroConfig,
     lc: LogContext,
     delegate: ChangeStreamer,
     opts: Options,
     parent: Worker,
   ) {
-    super('change-streamer-http-server', lc, opts, async fastify => {
+    super('change-streamer-http-server', config, lc, opts, async fastify => {
       await fastify.register(websocket);
 
       // fastify does not support optional path components, so we just
diff --git a/packages/zero-cache/src/services/dispatcher/dispatcher.ts b/packages/zero-cache/src/services/dispatcher/dispatcher.ts
index 731e3fc906..c1ea2791f7 100644
--- a/packages/zero-cache/src/services/dispatcher/dispatcher.ts
+++ b/packages/zero-cache/src/services/dispatcher/dispatcher.ts
@@ -7,6 +7,7 @@ import {HttpService, type Options} from '../http-service.ts';
 import {getConnectParams} from './connect-params.ts';
 import {installWebSocketHandoff} from './websocket-handoff.ts';
 import {assert} from '../../../../shared/src/asserts.ts';
+import type {ZeroConfig} from '../../config/zero-config.ts';
 
 // The server allows the client to use any /:base/ path to facilitate
 // servicing requests on the same domain as the application.
@@ -19,6 +20,7 @@ export class Dispatcher extends HttpService {
   readonly #mutator: Worker | undefined;
 
   constructor(
+    config: ZeroConfig,
     lc: LogContext,
     taskID: string,
     parent: Worker | null,
@@ -26,7 +28,7 @@ export class Dispatcher extends HttpService {
     mutator: Worker | undefined,
     opts: Options,
   ) {
-    super('dispatcher', lc, opts, fastify => {
+    super('dispatcher', config, lc, opts, fastify => {
       installWebSocketHandoff(lc, req => this.#handoff(req), fastify.server);
     });
 
diff --git a/packages/zero-cache/src/services/http-service.ts b/packages/zero-cache/src/services/http-service.ts
index 505721b929..60e9afac54 100644
--- a/packages/zero-cache/src/services/http-service.ts
+++ b/packages/zero-cache/src/services/http-service.ts
@@ -3,6 +3,8 @@ import Fastify, {type FastifyInstance} from 'fastify';
 import {HeartbeatMonitor} from './life-cycle.ts';
 import {RunningState} from './running-state.ts';
 import type {Service} from './service.ts';
+import {handleStatzRequest} from './statz.ts';
+import {type ZeroConfig} from '../config/zero-config.ts';
 
 export type Options = {
   port: number;
@@ -16,6 +18,7 @@ export type Options = {
 export class HttpService implements Service {
   readonly id: string;
   protected readonly _lc: LogContext;
+  readonly #config: ZeroConfig;
   readonly #fastify: FastifyInstance;
   readonly #port: number;
   readonly #state: RunningState;
@@ -24,12 +27,14 @@ export class HttpService implements Service {
 
   constructor(
     id: string,
+    config: ZeroConfig,
     lc: LogContext,
     opts: Options,
     init: (fastify: FastifyInstance) => void | Promise<void>,
   ) {
     this.id = id;
     this._lc = lc.withContext('component', this.id);
+    this.#config = config;
     this.#fastify = Fastify();
     this.#port = opts.port;
     this.#init = init;
@@ -41,6 +46,9 @@ export class HttpService implements Service {
   // run() is the lifecycle method called by the ServiceRunner.
   async start(): Promise<string> {
     this.#fastify.get('/', (_req, res) => res.send('OK'));
+    this.#fastify.get('/statz', (req, res) =>
+      handleStatzRequest(this.#config, req, res),
+    );
     this.#fastify.get('/keepalive', ({headers}, res) => {
       this.#heartbeatMonitor.onHeartbeat(headers);
       return res.send('OK');
diff --git a/packages/zero-cache/src/services/statz.ts b/packages/zero-cache/src/services/statz.ts
new file mode 100644
index 0000000000..e060e72f82
--- /dev/null
+++ b/packages/zero-cache/src/services/statz.ts
@@ -0,0 +1,359 @@
+import {astToZQL} from '../../../ast-to-zql/src/ast-to-zql.ts';
+import {createSilentLogContext} from '../../../shared/src/logging-test-utils.ts';
+import {type ZeroConfig} from '../config/zero-config.ts';
+import {getShardID, upstreamSchema} from '../types/shards.ts';
+import {pgClient} from '../types/pg.ts';
+import type {Writable} from 'stream';
+import {BigIntJSON} from '../types/bigint-json.ts';
+import os from 'os';
+import fs from 'fs';
+import type {FastifyReply, FastifyRequest} from 'fastify';
+import {Database} from '../../../zqlite/src/db.ts';
+import auth from 'basic-auth';
+
+const lc = createSilentLogContext();
+
+async function upstreamStats(config: ZeroConfig, out: Writable) {
+  const schema = upstreamSchema(getShardID(config));
+  const sql = pgClient(lc, config.upstream.db);
+
+  out.write(header('Upstream'));
+
+  await printPgStats(
+    [
+      [
+        'num replicas',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."replicas"`,
+      ],
+      [
+        'num clients with mutations',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."clients"`,
+      ],
+      [
+        'num mutations processed',
+        sql`SELECT SUM("lastMutationID") as "c" FROM ${sql(schema)}."clients"`,
+      ],
+    ],
+    out,
+  );
+
+  await sql.end();
+}
+
+async function cvrStats(config: ZeroConfig, out: Writable) {
+  out.write(header('CVR'));
+
+  const schema = upstreamSchema(getShardID(config)) + '/cvr';
+  const sql = pgClient(lc, config.cvr.db);
+
+  function numQueriesPerClientGroup(
+    active: boolean,
+  ): ReturnType<ReturnType<typeof pgClient>> {
+    const filter = active
+      ? sql`WHERE "inactivatedAt" IS NULL AND deleted = false`
+      : sql`WHERE "inactivatedAt" IS NOT NULL AND ("inactivatedAt" + "ttl") > NOW()`;
+    return sql`WITH
+    group_counts AS (
+      SELECT
+        "clientGroupID",
+        COUNT(*) AS num_queries
+      FROM ${sql(schema)}."desires"
+      ${filter}
+      GROUP BY "clientGroupID"
+    ),
+    -- Count distinct clientIDs per clientGroupID
+    client_per_group_counts AS (
+      SELECT
+        "clientGroupID",
+        COUNT(DISTINCT "clientID") AS num_clients
+      FROM ${sql(schema)}."desires"
+      ${filter}
+      GROUP BY "clientGroupID"
+    )
+    -- Combine all the information
+    SELECT
+      g."clientGroupID",
+      cpg.num_clients,
+      g.num_queries
+    FROM group_counts g
+    JOIN client_per_group_counts cpg ON g."clientGroupID" = cpg."clientGroupID"
+    ORDER BY g.num_queries DESC;`;
+  }
+
+  await printPgStats(
+    [
+      [
+        'total num queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires"`,
+      ],
+      [
+        'num unique query hashes',
+        sql`SELECT COUNT(DISTINCT "queryHash") as "c" FROM ${sql(
+          schema,
+        )}."desires"`,
+      ],
+      [
+        'num active queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "inactivatedAt" IS NULL AND "deleted" = false`,
+      ],
+      [
+        'num inactive queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "inactivatedAt" IS NOT NULL AND ("inactivatedAt" + "ttl") > NOW()`,
+      ],
+      [
+        'num deleted queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "deleted" = true`,
+      ],
+      [
+        'fresh queries percentiles',
+        sql`WITH client_group_counts AS (
+        -- Count inactive desires per clientGroupID
+        SELECT
+          "clientGroupID",
+          COUNT(*) AS fresh_count
+        FROM ${sql(schema)}."desires"
+        WHERE
+          ("inactivatedAt" IS NOT NULL
+          AND ("inactivatedAt" + "ttl") > NOW()) OR ("inactivatedAt" IS NULL
+          AND deleted = false)
+        GROUP BY "clientGroupID"
+      )
+
+      SELECT
+        percentile_cont(0.50) WITHIN GROUP (ORDER BY fresh_count) AS "p50",
+        percentile_cont(0.75) WITHIN GROUP (ORDER BY fresh_count) AS "p75",
+        percentile_cont(0.90) WITHIN GROUP (ORDER BY fresh_count) AS "p90",
+        percentile_cont(0.95) WITHIN GROUP (ORDER BY fresh_count) AS "p95",
+        percentile_cont(0.99) WITHIN GROUP (ORDER BY fresh_count) AS "p99",
+        MIN(fresh_count) AS "min",
+        MAX(fresh_count) AS "max",
+        AVG(fresh_count) AS "avg"
+      FROM client_group_counts;`,
+      ],
+      [
+        'rows per client group percentiles',
+        sql`WITH client_group_counts AS (
+        -- Count inactive desires per clientGroupID
+        SELECT
+          "clientGroupID",
+          COUNT(*) AS row_count
+        FROM ${sql(schema)}."rows"
+        GROUP BY "clientGroupID"
+      )
+      SELECT
+        percentile_cont(0.50) WITHIN GROUP (ORDER BY row_count) AS "p50",
+        percentile_cont(0.75) WITHIN GROUP (ORDER BY row_count) AS "p75",
+        percentile_cont(0.90) WITHIN GROUP (ORDER BY row_count) AS "p90",
+        percentile_cont(0.95) WITHIN GROUP (ORDER BY row_count) AS "p95",
+        percentile_cont(0.99) WITHIN GROUP (ORDER BY row_count) AS "p99",
+        MIN(row_count) AS "min",
+        MAX(row_count) AS "max",
+        AVG(row_count) AS "avg"
+      FROM client_group_counts;`,
+      ],
+      [
+        // check for AST blowup due to DNF conversion.
+        'ast sizes',
+        sql`SELECT
+        percentile_cont(0.25) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "25th_percentile",
+        percentile_cont(0.5) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "50th_percentile",
+        percentile_cont(0.75) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "75th_percentile",
+        percentile_cont(0.9) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "90th_percentile",
+        percentile_cont(0.95) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "95th_percentile",
+        percentile_cont(0.99) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "99th_percentile",
+        MIN(length("clientAST"::text)) AS "minimum_length",
+        MAX(length("clientAST"::text)) AS "maximum_length",
+        AVG(length("clientAST"::text))::integer AS "average_length",
+        COUNT(*) AS "total_records"
+      FROM ${sql(schema)}."queries";`,
+      ],
+      [
+        // output the hash of the largest AST
+        'biggest ast hash',
+        sql`SELECT "queryHash", length("clientAST"::text) AS "ast_length"
+      FROM ${sql(schema)}."queries"
+      ORDER BY length("clientAST"::text) DESC
+      LIMIT 1;`,
+      ],
+      [
+        'total active queries per client and client group',
+        numQueriesPerClientGroup(true),
+      ],
+      [
+        'total inactive queries per client and client group',
+        numQueriesPerClientGroup(false),
+      ],
+      [
+        'total rows per client group',
+        sql`SELECT "clientGroupID", COUNT(*) as "c" FROM ${sql(
+          schema,
+        )}."rows" GROUP BY "clientGroupID" ORDER BY "c" DESC`,
+      ],
+      [
+        'num rows per query',
+        sql`SELECT
+        k.key AS "queryHash",
+        COUNT(*) AS row_count
+      FROM ${sql(schema)}."rows" r,
+      LATERAL jsonb_each(r."refCounts") k
+      GROUP BY k.key
+      ORDER BY row_count DESC;`,
+      ],
+    ] satisfies [
+      name: string,
+      query: ReturnType<ReturnType<typeof pgClient>>,
+    ][],
+    out,
+  );
+
+  out.write('ZQL (without permissions) for each query:');
+  const queryAsts =
+    await sql`SELECT "queryHash", "clientAST" FROM ${sql(schema)}."queries"`;
+
+  const seenQueries = new Set<string>();
+  const parseFailures: string[] = [];
+  for (const row of queryAsts) {
+    const {queryHash, clientAST} = row;
+    if (seenQueries.has(queryHash)) {
+      continue;
+    }
+    seenQueries.add(queryHash);
+
+    try {
+      const zql = clientAST.table + astToZQL(clientAST);
+      out.write('HASH:' + queryHash);
+      out.write('ZQL:' + zql + '\n');
+    } catch (e) {
+      parseFailures.push(queryHash);
+    }
+  }
+  if (parseFailures.length > 0) {
+    out.write('Failed to parse the following hashes: ' + parseFailures);
+  }
+
+  await sql.end();
+}
+
+async function changelogStats(config: ZeroConfig, out: Writable) {
+  out.write(header('Change DB'));
+  const schema = upstreamSchema(getShardID(config)) + '/cdc';
+  const sql = pgClient(lc, config.change.db);
+
+  await printPgStats(
+    [
+      [
+        'change log size',
+        sql`SELECT COUNT(*) as "change_log_size" FROM ${sql(schema)}."changeLog"`,
+      ],
+    ],
+    out,
+  );
+  await sql.end();
+}
+
+function replicaStats(config: ZeroConfig, out: Writable) {
+  out.write(header('Replica'));
+  const db = new Database(lc, config.replica.file);
+  printStats(
+    'replica',
+    [
+      ['wal checkpoint', pick(first(db.pragma('WAL_CHECKPOINT')))],
+      ['page count', pick(first(db.pragma('PAGE_COUNT')))],
+      ['page size', pick(first(db.pragma('PAGE_SIZE')))],
+      ['journal mode', pick(first(db.pragma('JOURNAL_MODE')))],
+      ['synchronous', pick(first(db.pragma('SYNCHRONOUS')))],
+      ['cache size', pick(first(db.pragma('CACHE_SIZE')))],
+      ['auto vacuum', pick(first(db.pragma('AUTO_VACUUM')))],
+      ['freelist count', pick(first(db.pragma('FREELIST_COUNT')))],
+      ['wal autocheckpoint', pick(first(db.pragma('WAL_AUTOCHECKPOINT')))],
+      ['db file stats', fs.statSync(config.replica.file)],
+    ] as const,
+    out,
+  );
+}
+
+function osStats(out: Writable) {
+  printStats(
+    'os',
+    [
+      ['load avg', os.loadavg()],
+      ['uptime', os.uptime()],
+      ['total mem', os.totalmem()],
+      ['free mem', os.freemem()],
+      ['cpus', os.cpus().length],
+      ['platform', os.platform()],
+      ['arch', os.arch()],
+      ['release', os.release()],
+      ['uptime', os.uptime()],
+    ] as const,
+    out,
+  );
+}
+
+async function printPgStats(
+  pendingQueries: [
+    name: string,
+    query: ReturnType<ReturnType<typeof pgClient>>,
+  ][],
+  out: Writable,
+) {
+  const results = await Promise.all(
+    pendingQueries.map(async ([name, query]) => [name, await query]),
+  );
+  for (const [name, data] of results) {
+    out.write('\n');
+    out.write(name);
+    out.write('\n');
+    out.write(BigIntJSON.stringify(data, null, 2));
+  }
+}
+
+function printStats(
+  group: string,
+  queries: readonly [name: string, result: unknown][],
+  out: Writable,
+) {
+  out.write('\n' + header(group));
+  for (const [name, result] of queries) {
+    out.write('\n' + name + BigIntJSON.stringify(result, null, 2));
+  }
+}
+
+export async function handleStatzRequest(
+  config: ZeroConfig,
+  req: FastifyRequest,
+  res: FastifyReply,
+) {
+  const credentials = auth(req);
+  const expectedPassword = config.adminPassword;
+  if (!expectedPassword || credentials?.pass !== expectedPassword) {
+    void res
+      .code(401)
+      .header('WWW-Authenticate', 'Basic realm="Statz Protected Area"')
+      .send('Unauthorized');
+    return;
+  }
+
+  await upstreamStats(config, res.raw);
+  res.raw.write('\n\n');
+  await cvrStats(config, res.raw);
+  res.raw.write('\n\n');
+  await changelogStats(config, res.raw);
+  res.raw.write('\n\n');
+  replicaStats(config, res.raw);
+  res.raw.write('\n\n');
+  osStats(res.raw);
+  res.raw.end();
+}
+
+function first(x: object[]): object {
+  return x[0];
+}
+
+function pick(x: object): unknown {
+  return Object.values(x)[0];
+}
+
+function header(name: string): string {
+  return `=== ${name} ===\n`;
+}
diff --git a/packages/zero/package.json b/packages/zero/package.json
index d7119a3894..73b081d8ba 100644
--- a/packages/zero/package.json
+++ b/packages/zero/package.json
@@ -16,7 +16,7 @@
   "scripts": {
     "build": "rm -rf out && npm run build-server && npm run build-client",
     "build-client": "tsc -p tsconfig.client.json && npx tsx tool/build.js",
-    "build-server": "tsc -p tsconfig.server.json && chmod +x out/zero/src/cli.js out/zero/src/build-schema.js out/zero/src/deploy-permissions.js out/zero/src/zero-cache-dev.js out/zero/src/ast-to-zql.js out/zero/src/analyze-query.js out/zero/src/transform-query.js out/zero/src/cache-stats.js",
+    "build-server": "tsc -p tsconfig.server.json && chmod +x out/zero/src/cli.js out/zero/src/build-schema.js out/zero/src/deploy-permissions.js out/zero/src/zero-cache-dev.js out/zero/src/ast-to-zql.js out/zero/src/analyze-query.js out/zero/src/transform-query.js",
     "check-types": "tsc",
     "check-types:watch": "tsc --watch",
     "format": "prettier --write .",
@@ -47,6 +47,8 @@
     "@rocicorp/logger": "^5.4.0",
     "@rocicorp/resolver": "^1.0.2",
     "@rocicorp/zero-sqlite3": "^1.0.4",
+    "@types/basic-auth": "^1.1.8",
+    "basic-auth": "^2.0.1",
     "chalk": "^5.3.0",
     "chalk-template": "^1.1.0",
     "chokidar": "^4.0.1",
@@ -122,8 +124,7 @@
     "zero-deploy-permissions": "./out/zero/src/deploy-permissions.js",
     "ast-to-zql": "./out/zero/src/ast-to-zql.js",
     "analyze-query": "./out/zero/src/analyze-query.js",
-    "transform-query": "./out/zero/src/transform-query.js",
-    "cache-stats": "./out/zero/src/cache-stats.js"
+    "transform-query": "./out/zero/src/transform-query.js"
   },
   "engines": {
     "node": ">=20"
diff --git a/packages/zero/src/cache-stats.ts b/packages/zero/src/cache-stats.ts
deleted file mode 100644
index e4582c5026..0000000000
--- a/packages/zero/src/cache-stats.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/usr/bin/env node
-import '../../analyze-query/src/bin-cache-stats.ts';
diff --git a/packages/zero/tool/build.ts b/packages/zero/tool/build.ts
index 8706e31096..7ac2296700 100644
--- a/packages/zero/tool/build.ts
+++ b/packages/zero/tool/build.ts
@@ -93,8 +93,8 @@ async function verifyDependencies(external: Iterable<string>) {
   const {dependencies} = JSON.parse(packageJSON);
   const actualDeps = new Set(Object.keys(dependencies));
   assert.deepEqual(
-    expectedDeps,
     actualDeps,
+    expectedDeps,
     'zero/package.json dependencies do not match the dependencies of the internal packages',
   );
 }
diff --git a/packages/zero/tsconfig.server.json b/packages/zero/tsconfig.server.json
index b39ae6bfb4..38e1306bfc 100644
--- a/packages/zero/tsconfig.server.json
+++ b/packages/zero/tsconfig.server.json
@@ -13,7 +13,6 @@
     "src/pg.ts",
     "src/ast-to-zql.ts",
     "src/analyze-query.ts",
-    "src/transform-query.ts",
-    "src/cache-stats.ts"
+    "src/transform-query.ts"
   ]
 }


Reviews:
user: login: tantaman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T14:38:17Z
user: login: aboodman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T01:40:08Z

Review comments:
diff_hunk: @@ -0,0 +1,359 @@
+import {astToZQL} from '../../../ast-to-zql/src/ast-to-zql.ts';
+import {createSilentLogContext} from '../../../shared/src/logging-test-utils.ts';
+import {type ZeroConfig} from '../config/zero-config.ts';
+import {getShardID, upstreamSchema} from '../types/shards.ts';
+import {pgClient} from '../types/pg.ts';
+import type {Writable} from 'stream';
+import {BigIntJSON} from '../types/bigint-json.ts';
+import os from 'os';
+import fs from 'fs';
+import type {FastifyReply, FastifyRequest} from 'fastify';
+import {Database} from '../../../zqlite/src/db.ts';
+import auth from 'basic-auth';
+
+const lc = createSilentLogContext();
+
+async function upstreamStats(config: ZeroConfig, out: Writable) {
+  const schema = upstreamSchema(getShardID(config));
+  const sql = pgClient(lc, config.upstream.db);
+
+  out.write(header('Upstream'));
+
+  await printPgStats(
+    [
+      [
+        'num replicas',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."replicas"`,
+      ],
+      [
+        'num clients with mutations',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."clients"`,
+      ],
+      [
+        'num mutations processed',
+        sql`SELECT SUM("lastMutationID") as "c" FROM ${sql(schema)}."clients"`,
+      ],
+    ],
+    out,
+  );
+
+  await sql.end();
+}
+
+async function cvrStats(config: ZeroConfig, out: Writable) {
+  out.write(header('CVR'));
+
+  const schema = upstreamSchema(getShardID(config)) + '/cvr';
+  const sql = pgClient(lc, config.cvr.db);
+
+  function numQueriesPerClientGroup(
+    active: boolean,
+  ): ReturnType<ReturnType<typeof pgClient>> {
+    const filter = active
+      ? sql`WHERE "inactivatedAt" IS NULL AND deleted = false`
+      : sql`WHERE "inactivatedAt" IS NOT NULL AND ("inactivatedAt" + "ttl") > NOW()`;
+    return sql`WITH
+    group_counts AS (
+      SELECT
+        "clientGroupID",
+        COUNT(*) AS num_queries
+      FROM ${sql(schema)}."desires"
+      ${filter}
+      GROUP BY "clientGroupID"
+    ),
+    -- Count distinct clientIDs per clientGroupID
+    client_per_group_counts AS (
+      SELECT
+        "clientGroupID",
+        COUNT(DISTINCT "clientID") AS num_clients
+      FROM ${sql(schema)}."desires"
+      ${filter}
+      GROUP BY "clientGroupID"
+    )
+    -- Combine all the information
+    SELECT
+      g."clientGroupID",
+      cpg.num_clients,
+      g.num_queries
+    FROM group_counts g
+    JOIN client_per_group_counts cpg ON g."clientGroupID" = cpg."clientGroupID"
+    ORDER BY g.num_queries DESC;`;
+  }
+
+  await printPgStats(
+    [
+      [
+        'total num queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires"`,
+      ],
+      [
+        'num unique query hashes',
+        sql`SELECT COUNT(DISTINCT "queryHash") as "c" FROM ${sql(
+          schema,
+        )}."desires"`,
+      ],
+      [
+        'num active queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "inactivatedAt" IS NULL AND "deleted" = false`,
+      ],
+      [
+        'num inactive queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "inactivatedAt" IS NOT NULL AND ("inactivatedAt" + "ttl") > NOW()`,
+      ],
+      [
+        'num deleted queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "deleted" = true`,
+      ],
+      [
+        'fresh queries percentiles',
+        sql`WITH client_group_counts AS (
+        -- Count inactive desires per clientGroupID
+        SELECT
+          "clientGroupID",
+          COUNT(*) AS fresh_count
+        FROM ${sql(schema)}."desires"
+        WHERE
+          ("inactivatedAt" IS NOT NULL
+          AND ("inactivatedAt" + "ttl") > NOW()) OR ("inactivatedAt" IS NULL
+          AND deleted = false)
+        GROUP BY "clientGroupID"
+      )
+
+      SELECT
+        percentile_cont(0.50) WITHIN GROUP (ORDER BY fresh_count) AS "p50",
+        percentile_cont(0.75) WITHIN GROUP (ORDER BY fresh_count) AS "p75",
+        percentile_cont(0.90) WITHIN GROUP (ORDER BY fresh_count) AS "p90",
+        percentile_cont(0.95) WITHIN GROUP (ORDER BY fresh_count) AS "p95",
+        percentile_cont(0.99) WITHIN GROUP (ORDER BY fresh_count) AS "p99",
+        MIN(fresh_count) AS "min",
+        MAX(fresh_count) AS "max",
+        AVG(fresh_count) AS "avg"
+      FROM client_group_counts;`,
+      ],
+      [
+        'rows per client group percentiles',
+        sql`WITH client_group_counts AS (
+        -- Count inactive desires per clientGroupID
+        SELECT
+          "clientGroupID",
+          COUNT(*) AS row_count
+        FROM ${sql(schema)}."rows"
+        GROUP BY "clientGroupID"
+      )
+      SELECT
+        percentile_cont(0.50) WITHIN GROUP (ORDER BY row_count) AS "p50",
+        percentile_cont(0.75) WITHIN GROUP (ORDER BY row_count) AS "p75",
+        percentile_cont(0.90) WITHIN GROUP (ORDER BY row_count) AS "p90",
+        percentile_cont(0.95) WITHIN GROUP (ORDER BY row_count) AS "p95",
+        percentile_cont(0.99) WITHIN GROUP (ORDER BY row_count) AS "p99",
+        MIN(row_count) AS "min",
+        MAX(row_count) AS "max",
+        AVG(row_count) AS "avg"
+      FROM client_group_counts;`,
+      ],
+      [
+        // check for AST blowup due to DNF conversion.
+        'ast sizes',
+        sql`SELECT
+        percentile_cont(0.25) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "25th_percentile",
+        percentile_cont(0.5) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "50th_percentile",
+        percentile_cont(0.75) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "75th_percentile",
+        percentile_cont(0.9) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "90th_percentile",
+        percentile_cont(0.95) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "95th_percentile",
+        percentile_cont(0.99) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "99th_percentile",
+        MIN(length("clientAST"::text)) AS "minimum_length",
+        MAX(length("clientAST"::text)) AS "maximum_length",
+        AVG(length("clientAST"::text))::integer AS "average_length",
+        COUNT(*) AS "total_records"
+      FROM ${sql(schema)}."queries";`,
+      ],
+      [
+        // output the hash of the largest AST
+        'biggest ast hash',
+        sql`SELECT "queryHash", length("clientAST"::text) AS "ast_length"
+      FROM ${sql(schema)}."queries"
+      ORDER BY length("clientAST"::text) DESC
+      LIMIT 1;`,
+      ],
+      [
+        'total active queries per client and client group',
+        numQueriesPerClientGroup(true),
+      ],
+      [
+        'total inactive queries per client and client group',
+        numQueriesPerClientGroup(false),
+      ],
+      [
+        'total rows per client group',
+        sql`SELECT "clientGroupID", COUNT(*) as "c" FROM ${sql(
+          schema,
+        )}."rows" GROUP BY "clientGroupID" ORDER BY "c" DESC`,
+      ],
+      [
+        'num rows per query',
+        sql`SELECT
+        k.key AS "queryHash",
+        COUNT(*) AS row_count
+      FROM ${sql(schema)}."rows" r,
+      LATERAL jsonb_each(r."refCounts") k
+      GROUP BY k.key
+      ORDER BY row_count DESC;`,
+      ],
+    ] satisfies [
+      name: string,
+      query: ReturnType<ReturnType<typeof pgClient>>,
+    ][],
+    out,
+  );
+
+  out.write('ZQL (without permissions) for each query:');
+  const queryAsts =
+    await sql`SELECT "queryHash", "clientAST" FROM ${sql(schema)}."queries"`;
+
+  const seenQueries = new Set<string>();
+  const parseFailures: string[] = [];
+  for (const row of queryAsts) {
+    const {queryHash, clientAST} = row;
+    if (seenQueries.has(queryHash)) {
+      continue;
+    }
+    seenQueries.add(queryHash);
+
+    try {
+      const zql = clientAST.table + astToZQL(clientAST);
+      out.write('HASH:' + queryHash);
+      out.write('ZQL:' + zql + '\n');
+    } catch (e) {
+      parseFailures.push(queryHash);
+    }
path: packages/zero-cache/src/services/statz.ts
user: login: tantaman
body: Not sure we should dump these. It could be quite a lot of data.
created_at: 2025-04-25T14:38:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,359 @@
+import {astToZQL} from '../../../ast-to-zql/src/ast-to-zql.ts';
+import {createSilentLogContext} from '../../../shared/src/logging-test-utils.ts';
+import {type ZeroConfig} from '../config/zero-config.ts';
+import {getShardID, upstreamSchema} from '../types/shards.ts';
+import {pgClient} from '../types/pg.ts';
+import type {Writable} from 'stream';
+import {BigIntJSON} from '../types/bigint-json.ts';
+import os from 'os';
+import fs from 'fs';
+import type {FastifyReply, FastifyRequest} from 'fastify';
+import {Database} from '../../../zqlite/src/db.ts';
+import auth from 'basic-auth';
+
+const lc = createSilentLogContext();
+
+async function upstreamStats(config: ZeroConfig, out: Writable) {
+  const schema = upstreamSchema(getShardID(config));
+  const sql = pgClient(lc, config.upstream.db);
+
+  out.write(header('Upstream'));
+
+  await printPgStats(
+    [
+      [
+        'num replicas',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."replicas"`,
+      ],
+      [
+        'num clients with mutations',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."clients"`,
+      ],
+      [
+        'num mutations processed',
+        sql`SELECT SUM("lastMutationID") as "c" FROM ${sql(schema)}."clients"`,
+      ],
+    ],
+    out,
+  );
+
+  await sql.end();
+}
+
+async function cvrStats(config: ZeroConfig, out: Writable) {
+  out.write(header('CVR'));
+
+  const schema = upstreamSchema(getShardID(config)) + '/cvr';
+  const sql = pgClient(lc, config.cvr.db);
+
+  function numQueriesPerClientGroup(
+    active: boolean,
+  ): ReturnType<ReturnType<typeof pgClient>> {
+    const filter = active
+      ? sql`WHERE "inactivatedAt" IS NULL AND deleted = false`
+      : sql`WHERE "inactivatedAt" IS NOT NULL AND ("inactivatedAt" + "ttl") > NOW()`;
+    return sql`WITH
+    group_counts AS (
+      SELECT
+        "clientGroupID",
+        COUNT(*) AS num_queries
+      FROM ${sql(schema)}."desires"
+      ${filter}
+      GROUP BY "clientGroupID"
+    ),
+    -- Count distinct clientIDs per clientGroupID
+    client_per_group_counts AS (
+      SELECT
+        "clientGroupID",
+        COUNT(DISTINCT "clientID") AS num_clients
+      FROM ${sql(schema)}."desires"
+      ${filter}
+      GROUP BY "clientGroupID"
+    )
+    -- Combine all the information
+    SELECT
+      g."clientGroupID",
+      cpg.num_clients,
+      g.num_queries
+    FROM group_counts g
+    JOIN client_per_group_counts cpg ON g."clientGroupID" = cpg."clientGroupID"
+    ORDER BY g.num_queries DESC;`;
+  }
+
+  await printPgStats(
+    [
+      [
+        'total num queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires"`,
+      ],
+      [
+        'num unique query hashes',
+        sql`SELECT COUNT(DISTINCT "queryHash") as "c" FROM ${sql(
+          schema,
+        )}."desires"`,
+      ],
+      [
+        'num active queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "inactivatedAt" IS NULL AND "deleted" = false`,
+      ],
+      [
+        'num inactive queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "inactivatedAt" IS NOT NULL AND ("inactivatedAt" + "ttl") > NOW()`,
+      ],
+      [
+        'num deleted queries',
+        sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "deleted" = true`,
+      ],
+      [
+        'fresh queries percentiles',
+        sql`WITH client_group_counts AS (
+        -- Count inactive desires per clientGroupID
+        SELECT
+          "clientGroupID",
+          COUNT(*) AS fresh_count
+        FROM ${sql(schema)}."desires"
+        WHERE
+          ("inactivatedAt" IS NOT NULL
+          AND ("inactivatedAt" + "ttl") > NOW()) OR ("inactivatedAt" IS NULL
+          AND deleted = false)
+        GROUP BY "clientGroupID"
+      )
+
+      SELECT
+        percentile_cont(0.50) WITHIN GROUP (ORDER BY fresh_count) AS "p50",
+        percentile_cont(0.75) WITHIN GROUP (ORDER BY fresh_count) AS "p75",
+        percentile_cont(0.90) WITHIN GROUP (ORDER BY fresh_count) AS "p90",
+        percentile_cont(0.95) WITHIN GROUP (ORDER BY fresh_count) AS "p95",
+        percentile_cont(0.99) WITHIN GROUP (ORDER BY fresh_count) AS "p99",
+        MIN(fresh_count) AS "min",
+        MAX(fresh_count) AS "max",
+        AVG(fresh_count) AS "avg"
+      FROM client_group_counts;`,
+      ],
+      [
+        'rows per client group percentiles',
+        sql`WITH client_group_counts AS (
+        -- Count inactive desires per clientGroupID
+        SELECT
+          "clientGroupID",
+          COUNT(*) AS row_count
+        FROM ${sql(schema)}."rows"
+        GROUP BY "clientGroupID"
+      )
+      SELECT
+        percentile_cont(0.50) WITHIN GROUP (ORDER BY row_count) AS "p50",
+        percentile_cont(0.75) WITHIN GROUP (ORDER BY row_count) AS "p75",
+        percentile_cont(0.90) WITHIN GROUP (ORDER BY row_count) AS "p90",
+        percentile_cont(0.95) WITHIN GROUP (ORDER BY row_count) AS "p95",
+        percentile_cont(0.99) WITHIN GROUP (ORDER BY row_count) AS "p99",
+        MIN(row_count) AS "min",
+        MAX(row_count) AS "max",
+        AVG(row_count) AS "avg"
+      FROM client_group_counts;`,
+      ],
+      [
+        // check for AST blowup due to DNF conversion.
+        'ast sizes',
+        sql`SELECT
+        percentile_cont(0.25) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "25th_percentile",
+        percentile_cont(0.5) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "50th_percentile",
+        percentile_cont(0.75) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "75th_percentile",
+        percentile_cont(0.9) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "90th_percentile",
+        percentile_cont(0.95) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "95th_percentile",
+        percentile_cont(0.99) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "99th_percentile",
+        MIN(length("clientAST"::text)) AS "minimum_length",
+        MAX(length("clientAST"::text)) AS "maximum_length",
+        AVG(length("clientAST"::text))::integer AS "average_length",
+        COUNT(*) AS "total_records"
+      FROM ${sql(schema)}."queries";`,
+      ],
+      [
+        // output the hash of the largest AST
+        'biggest ast hash',
+        sql`SELECT "queryHash", length("clientAST"::text) AS "ast_length"
+      FROM ${sql(schema)}."queries"
+      ORDER BY length("clientAST"::text) DESC
+      LIMIT 1;`,
+      ],
+      [
+        'total active queries per client and client group',
+        numQueriesPerClientGroup(true),
+      ],
+      [
+        'total inactive queries per client and client group',
+        numQueriesPerClientGroup(false),
+      ],
+      [
+        'total rows per client group',
+        sql`SELECT "clientGroupID", COUNT(*) as "c" FROM ${sql(
+          schema,
+        )}."rows" GROUP BY "clientGroupID" ORDER BY "c" DESC`,
+      ],
+      [
+        'num rows per query',
+        sql`SELECT
+        k.key AS "queryHash",
+        COUNT(*) AS row_count
+      FROM ${sql(schema)}."rows" r,
+      LATERAL jsonb_each(r."refCounts") k
+      GROUP BY k.key
+      ORDER BY row_count DESC;`,
+      ],
+    ] satisfies [
+      name: string,
+      query: ReturnType<ReturnType<typeof pgClient>>,
+    ][],
+    out,
+  );
+
+  out.write('ZQL (without permissions) for each query:');
+  const queryAsts =
+    await sql`SELECT "queryHash", "clientAST" FROM ${sql(schema)}."queries"`;
+
+  const seenQueries = new Set<string>();
+  const parseFailures: string[] = [];
+  for (const row of queryAsts) {
+    const {queryHash, clientAST} = row;
+    if (seenQueries.has(queryHash)) {
+      continue;
+    }
+    seenQueries.add(queryHash);
+
+    try {
+      const zql = clientAST.table + astToZQL(clientAST);
+      out.write('HASH:' + queryHash);
+      out.write('ZQL:' + zql + '\n');
+    } catch (e) {
+      parseFailures.push(queryHash);
+    }
path: packages/zero-cache/src/services/statz.ts
user: login: aboodman
body: Yeah :-/. Let's see how it goes. Seems useful.
created_at: 2025-04-30T01:40:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-25T06:34:58Z
body: [vc]: #vLjmCIFpbR6J3quvOA0J626WIsaIqRzGJfo067hnvxA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJyZXBsaWNhY2hlLWRvY3MiLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcm9jaWNvcnAvcmVwbGljYWNoZS1kb2NzLzVKRzJpNHJZaHBva3dOUTMzY3plczZpZTk4Q1QiLCJwcmV2aWV3VXJsIjoicmVwbGljYWNoZS1kb2NzLWdpdC1hYS1zdGF0ei1yb2NpY29ycC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InJlcGxpY2FjaGUtZG9jcy1naXQtYWEtc3RhdHotcm9jaWNvcnAudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJ6YnVncyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3pidWdzIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3JvY2ljb3JwL3pidWdzL0pBVVY2cVNwM2NremdtdEduRGY3SGVhZFV0OUsiLCJwcmV2aWV3VXJsIjoiemJ1Z3MtZ2l0LWFhLXN0YXR6LXJvY2ljb3JwLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiemJ1Z3MtZ2l0LWFhLXN0YXR6LXJvY2ljb3JwLnZlcmNlbC5hcHAifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **replicache-docs** | ✅ Ready ([Inspect](https://vercel.com/rocicorp/replicache-docs/5JG2i4rYhpokwNQ33czes6ie98CT)) | [Visit Preview](https://replicache-docs-git-aa-statz-rocicorp.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/replicache-docs-git-aa-statz-rocicorp.vercel.app?via=pr-comment-feedback-link) | Apr 30, 2025 1:32am |
| **zbugs** | ✅ Ready ([Inspect](https://vercel.com/rocicorp/zbugs/JAUV6qSp3ckzgmtGnDf7HeadUt9K)) | [Visit Preview](https://zbugs-git-aa-statz-rocicorp.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/zbugs-git-aa-statz-rocicorp.vercel.app?via=pr-comment-feedback-link) | Apr 30, 2025 1:32am |



author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-25T06:36:03Z
body: <h2><a href="https://bencher.dev/perf/zero/reports/f695569f-8843-442c-9792-c477cf81d73e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero"><img src="https://bencher.dev/favicon.svg" width="24" height="24" alt="🐰" /> Bencher Report</a></h2><table><tr><td>Branch</td><td><a href="https://bencher.dev/perf/zero/branches/aa-statz?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">aa/statz</a></td></tr><tr><td>Testbed</td><td><a href="https://bencher.dev/perf/zero/testbeds/linux?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">Linux</a></td></tr></table><details><summary>Click to view all benchmark results</summary><br /><table><thead><tr><th>Benchmark</th><th><a href="https://bencher.dev/perf/zero/measures/throughput?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">Throughput</a></th><th>Benchmark Result<br />operations / second (ops/s)<br />(Result Δ%)</th><th>Lower Boundary<br />operations / second (ops/s)<br />(Limit %)</th></tr></thead><tbody><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-custom-bench-ts-big-schema?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/custom.bench.ts > big schema</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=3bc39a14-bf3e-44a4-822e-45ab870cb165&heads=48e0dfaa-a0e7-4850-918a-345fd217ee74&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=da25d28a-0b6b-4baa-8550-fb143072856d&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1743384751000&end_time=1745976751000&report=f695569f-8843-442c-9792-c477cf81d73e&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/4508a057-171d-4efb-9581-6651b51b10b9?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>0.00 ops/s<br /><details><summary>(+72.98%)</summary>Baseline: 0.00 ops/s</details></td><td>0.00 ops/s<br />(19.62%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-zero-bench-ts-basics-all-1000-rows-x-10-colu-rypc9f4e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/zero.bench.ts > basics > All 1000 rows x 10 columns (numbers)</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=3bc39a14-bf3e-44a4-822e-45ab870cb165&heads=48e0dfaa-a0e7-4850-918a-345fd217ee74&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=8ff168e6-e598-4cc2-a9c7-e5d09c4aefec&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1743384751000&end_time=1745976751000&report=f695569f-8843-442c-9792-c477cf81d73e&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/4508a057-171d-4efb-9581-6651b51b10b9?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>39.70 ops/s<br /><details><summary>(-0.34%)</summary>Baseline: 39.84 ops/s</details></td><td>37.16 ops/s<br />(93.60%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-zero-bench-ts-with-filter-lower-rows-500-x-1-d9xjs9n1?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/zero.bench.ts > with filter > Lower rows 500 x 10 columns (numbers)</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=3bc39a14-bf3e-44a4-822e-45ab870cb165&heads=48e0dfaa-a0e7-4850-918a-345fd217ee74&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=ce024352-2a47-400d-b489-b9951bf428ac&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1743384751000&end_time=1745976751000&report=f695569f-8843-442c-9792-c477cf81d73e&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/4508a057-171d-4efb-9581-6651b51b10b9?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>36.03 ops/s<br /><details><summary>(-8.39%)</summary>Baseline: 39.33 ops/s</details></td><td>35.72 ops/s<br />(99.16%)</td></tr></tbody></table></details><a href="https://bencher.dev/perf/zero/reports/f695569f-8843-442c-9792-c477cf81d73e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">🐰 View full continuous benchmarking report in Bencher</a><div id="bencher.dev/projects/zero/id/perf"></div>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-30T01:33:37Z
body: <h2><a href="https://bencher.dev/perf/zero/reports/8186436b-d40f-472c-85a6-437830d9f3fd?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero"><img src="https://bencher.dev/favicon.svg" width="24" height="24" alt="🐰" /> Bencher Report</a></h2><table><tr><td>Branch</td><td><a href="https://bencher.dev/perf/zero/branches/aa-statz?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">aa/statz</a></td></tr><tr><td>Testbed</td><td><a href="https://bencher.dev/perf/zero/testbeds/linux?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">Linux</a></td></tr></table><details><summary>Click to view all benchmark results</summary><br /><table><thead><tr><th>Benchmark</th><th><a href="https://bencher.dev/perf/zero/measures/file-size?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">File Size</a></th><th>Benchmark Result<br />kilobytes (KB)<br />(Result Δ%)</th><th>Upper Boundary<br />kilobytes (KB)<br />(Limit %)</th></tr></thead><tbody><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-package-tgz?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero-package.tgz</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=3bc39a14-bf3e-44a4-822e-45ab870cb165&heads=4529c9a4-0eec-4681-bf8f-2d186a5c8246&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=2bcdd867-a170-440b-8c18-ee15b6476b90&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1743384816000&end_time=1745976816000&report=8186436b-d40f-472c-85a6-437830d9f3fd&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/0ec002cf-c104-47ea-bb6e-3b707a47c8c1?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>1,150.70 KB<br /><details><summary>(+0.07%)</summary>Baseline: 1,149.89 KB</details></td><td>1,172.89 KB<br />(98.11%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-js?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero.js</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=3bc39a14-bf3e-44a4-822e-45ab870cb165&heads=4529c9a4-0eec-4681-bf8f-2d186a5c8246&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=4f172dbf-1d31-4809-9c2d-12c79ed4c9d4&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1743384816000&end_time=1745976816000&report=8186436b-d40f-472c-85a6-437830d9f3fd&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/0ec002cf-c104-47ea-bb6e-3b707a47c8c1?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>194.46 KB<br /><details><summary>(0.00%)</summary>Baseline: 194.46 KB</details></td><td>198.34 KB<br />(98.04%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-js-br?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero.js.br</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=3bc39a14-bf3e-44a4-822e-45ab870cb165&heads=4529c9a4-0eec-4681-bf8f-2d186a5c8246&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=f9d12b7c-f48c-41fc-a662-5019d902c9f9&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1743384816000&end_time=1745976816000&report=8186436b-d40f-472c-85a6-437830d9f3fd&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/0ec002cf-c104-47ea-bb6e-3b707a47c8c1?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>54.42 KB<br /><details><summary>(0.00%)</summary>Baseline: 54.42 KB</details></td><td>55.51 KB<br />(98.04%)</td></tr></tbody></table></details><a href="https://bencher.dev/perf/zero/reports/8186436b-d40f-472c-85a6-437830d9f3fd?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">🐰 View full continuous benchmarking report in Bencher</a><div id="bencher.dev/projects/zero/id/size"></div>
author_association: NONE
user: login: aboodman
created_at: 2025-04-30T01:40:27Z
body: @darkgnotic can you review the config changes?
author_association: CONTRIBUTOR