Repository: ponder-sh/ponder (ts)

Issue:
Cannot integrate Ponder Client with Next.js app
### Version

0.9.22

### Current behavior

1. Creating ponder client with `createClient` (`import { createClient } from '@ponder/client';`) requires providing ponder schema, for example, `createClient('https://example.com/sql', { schema })`.
2. Importing ponder schema requires importing Ponder DB schema functions (`import { index, onchainTable, relations } from "ponder";`).
3. Ponder uses `node:*` imports which Next.js cannot resolve and fails the build

### Expected behavior

1. Ponder schema can be imported into a client-side project without a need for bringing `node:*` dependencies into the build process.

### Steps to reproduce

1. Checkout the repo on `feat/ensadmin-next` branch
2. `cd apps/ensadmin-next`, and then `pnpm i`, and finally `pnpm build`

### Link to repository

https://github.com/namehash/ensnode/tree/feat/ensadmin-next/apps/ensadmin-next

### Anything else?

Related discussion:
- https://github.com/vercel/next.js/issues/28774#issuecomment-1628360375

<details><summary>Error log</summary>

```
pnpm build

> ensadmin-next@0.1.0 build /Users/tko/dev/github/namehash/ensnode/apps/ensadmin-next
> next build

   ▲ Next.js 15.1.7
   - Environments: .env.local

   Creating an optimized production build ...
(node:53740) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
 ⚠ Compiled with warnings

../../../../../../node_modules/@whatwg-node/fetch/dist/node-ponyfill.js
Critical dependency: the request of a dependency is an expression

Import trace for requested module:
../../../../../../node_modules/@whatwg-node/fetch/dist/node-ponyfill.js
../../../../../../node_modules/graphql-yoga/esm/server.js
../../../../../../node_modules/graphql-yoga/esm/index.js
../../../../../../node_modules/@ponder/core/dist/chunk-Y6HWK5B3.js
../../../../../../node_modules/@ponder/core/dist/index.js
./src/components/ponder-client/examples.tsx

(node:53792) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
Failed to compile.

node:fs
Module build failed: UnhandledSchemeError: Reading from "node:fs" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.
    at /Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:406935
    at Hook.eval [as callAsync] (eval at create (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:13:9218), <anonymous>:6:1)
    at Object.processResource (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:406860)
    at processResource (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/loader-runner/LoaderRunner.js:1:5308)
    at iteratePitchingLoaders (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/loader-runner/LoaderRunner.js:1:4667)
    at runLoaders (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/loader-runner/LoaderRunner.js:1:8590)
    at NormalModule._doBuild (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:406722)
    at NormalModule.build (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:408735)
    at /Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:82434
    at NormalModule.needBuild (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:412676)

Import trace for requested module:
node:fs
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/indexing/addStackTrace.js
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/indexing/index.js
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/indexing-store/cache.js
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/graphql/index.js
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/graphql/middleware.js
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/index.js
../../packages/ponder-schema/src/ponder.schema.ts
./src/components/ponder-client/hooks.ts
./src/components/ponder-client/examples.tsx

node:stream/promises
Module build failed: UnhandledSchemeError: Reading from "node:stream/promises" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.
    at /Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:406935
    at Hook.eval [as callAsync] (eval at create (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:13:9218), <anonymous>:6:1)
    at Object.processResource (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:406860)
    at processResource (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/loader-runner/LoaderRunner.js:1:5308)
    at iteratePitchingLoaders (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/loader-runner/LoaderRunner.js:1:4667)
    at runLoaders (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/loader-runner/LoaderRunner.js:1:8590)
    at NormalModule._doBuild (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:406722)
    at NormalModule.build (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:408735)
    at /Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:82434
    at NormalModule.needBuild (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:412676)

Import trace for requested module:
node:stream/promises
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/indexing-store/cache.js
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/graphql/index.js
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/graphql/middleware.js
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/index.js
../../packages/ponder-schema/src/ponder.schema.ts
./src/components/ponder-client/hooks.ts
./src/components/ponder-client/examples.tsx

node:stream
Module build failed: UnhandledSchemeError: Reading from "node:stream" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.
    at /Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:406935
    at Hook.eval [as callAsync] (eval at create (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:13:9218), <anonymous>:6:1)
    at Hook.CALL_ASYNC_DELEGATE [as _callAsync] (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:13:6372)
    at Object.processResource (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:406860)
    at processResource (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/loader-runner/LoaderRunner.js:1:5308)
    at iteratePitchingLoaders (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/loader-runner/LoaderRunner.js:1:4667)
    at runLoaders (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/loader-runner/LoaderRunner.js:1:8590)
    at NormalModule._doBuild (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:406722)
    at NormalModule.build (/Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:408735)
    at /Users/tko/dev/github/namehash/ensnode/node_modules/.pnpm/next@15.1.7_@opentelemetry+api@1.7.0_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/bundle5.js:28:82434

Import trace for requested module:
node:stream
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/indexing-store/cache.js
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/graphql/index.js
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/graphql/middleware.js
../../node_modules/.pnpm/ponder@0.9.22_@opentelemetry+api@1.7.0_@types+node@22.13.5_@types+react@19.0.10_hono@4.6.17_l_6ppbntg43wu54obuw5fk6km63m/node_modules/ponder/dist/esm/index.js
../../packages/ponder-schema/src/ponder.schema.ts
./src/components/ponder-client/hooks.ts
./src/components/ponder-client/examples.tsx

../../node_modules/.pnpm/@headlessui+react@1.7.19_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/@headlessui/react/dist/utils/stable-collection.js
Attempted import error: '__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' is not exported from 'react' (imported as 'r').

Import trace for requested module:
../../node_modules/.pnpm/@headlessui+react@1.7.19_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/@headlessui/react/dist/utils/stable-collection.js
../../node_modules/.pnpm/@headlessui+react@1.7.19_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/@headlessui/react/dist/components/tabs/tabs.js
../../node_modules/.pnpm/@headlessui+react@1.7.19_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/@headlessui/react/dist/headlessui.esm.js
../../node_modules/.pnpm/@graphiql+react@0.28.2_@types+node@22.13.5_@types+react-dom@19.0.4_@types+react@19.0.10__@typ_yypt63qrselewseonxlwy655py/node_modules/@graphiql/react/dist/index.mjs
../../node_modules/.pnpm/graphiql@3.8.3_@types+node@22.13.5_@types+react-dom@19.0.4_@types+react@19.0.10__@types+react_e4vzxlcx24fjwgjxupszm5qb6m/node_modules/graphiql/esm/index.js
./src/components/graphiql/graphiql-wrapper.tsx
./src/app/gql/ponder/page.tsx
```
</details>

Pull request:
fix: `ponder` entrypoint includes node.js imports
Fixes #1559, see changeset

Patch:
diff --git a/.changeset/twelve-planes-perform.md b/.changeset/twelve-planes-perform.md
new file mode 100644
index 00000000..6c2ba0d9
--- /dev/null
+++ b/.changeset/twelve-planes-perform.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Fixed a bug introduced in `0.9.20` where the `ponder` entrypoint included Node.js-only imports like `"node:path"`, breaking some workflows that use `@ponder/client` in browser environments.
diff --git a/packages/core/src/graphql/index.ts b/packages/core/src/graphql/index.ts
index 314f43fe..4452db2e 100644
--- a/packages/core/src/graphql/index.ts
+++ b/packages/core/src/graphql/index.ts
@@ -1,6 +1,6 @@
 import type { Database } from "@/database/index.js";
 import type { OnchainTable } from "@/drizzle/onchain.js";
-import { normalizeColumn } from "@/indexing-store/cache.js";
+import { normalizeColumn } from "@/indexing-store/utils.js";
 import type { Schema } from "@/internal/types.js";
 import type { Drizzle, ReadonlyDrizzle } from "@/types/db.js";
 import { never } from "@/utils/never.js";
diff --git a/packages/core/src/indexing-store/cache.ts b/packages/core/src/indexing-store/cache.ts
index 55dbc275..9234eb70 100644
--- a/packages/core/src/indexing-store/cache.ts
+++ b/packages/core/src/indexing-store/cache.ts
@@ -2,14 +2,9 @@ import { Readable } from "node:stream";
 import { pipeline } from "node:stream/promises";
 import { getPrimaryKeyColumns } from "@/drizzle/index.js";
 import { getColumnCasing } from "@/drizzle/kit/index.js";
-import { addErrorMeta } from "@/indexing/index.js";
-import { toErrorMeta } from "@/indexing/index.js";
+import { addErrorMeta, toErrorMeta } from "@/indexing/index.js";
 import type { Common } from "@/internal/common.js";
-import {
-  BigIntSerializationError,
-  FlushError,
-  NotNullConstraintError,
-} from "@/internal/errors.js";
+import { FlushError } from "@/internal/errors.js";
 import type { Event, Schema, SchemaBuild } from "@/internal/types.js";
 import type { Drizzle } from "@/types/db.js";
 import { ZERO_CHECKPOINT_STRING } from "@/utils/checkpoint.js";
@@ -18,18 +13,13 @@ import { startClock } from "@/utils/timer.js";
 import { PGlite } from "@electric-sql/pglite";
 import {
   type Column,
-  type SQL,
-  type SQLWrapper,
   type Table,
   type TableConfig,
-  and,
-  eq,
   getTableColumns,
   getTableName,
   is,
 } from "drizzle-orm";
 import {
-  PgArray,
   PgTable,
   type PgTableWithColumns,
   getTableConfig,
@@ -37,6 +27,7 @@ import {
 import type { PoolClient } from "pg";
 import copy from "pg-copy-streams";
 import { parseSqlError } from "./index.js";
+import { getCacheKey, getWhereCondition, normalizeRow } from "./utils.js";
 
 export type IndexingCache = {
   /**
@@ -125,127 +116,6 @@ type Buffer = Map<
   >
 >;
 
-/**
- * Returns true if the column has a "default" value that is used when no value is passed.
- * Handles `.default`, `.$defaultFn()`, `.$onUpdateFn()`.
- */
-export const hasEmptyValue = (column: Column) => {
-  return column.hasDefault;
-};
-
-/** Returns the "default" value for `column`. */
-export const getEmptyValue = (column: Column, isUpdate: boolean) => {
-  if (isUpdate && column.onUpdateFn) {
-    return column.onUpdateFn();
-  }
-  if (column.default !== undefined) return column.default;
-  if (column.defaultFn !== undefined) return column.defaultFn();
-  if (column.onUpdateFn !== undefined) return column.onUpdateFn();
-
-  // TODO(kyle) is it an invariant that it doesn't get here
-
-  return undefined;
-};
-
-export const normalizeColumn = (
-  column: Column,
-  value: unknown,
-  isUpdate: boolean,
-  // @ts-ignore
-): unknown => {
-  if (value === undefined) {
-    if (hasEmptyValue(column)) return getEmptyValue(column, isUpdate);
-    return null;
-  }
-  if (value === null) return null;
-  if (column.mapToDriverValue === undefined) return value;
-  try {
-    if (Array.isArray(value) && column instanceof PgArray) {
-      return value.map((v) =>
-        column.baseColumn.mapFromDriverValue(
-          column.baseColumn.mapToDriverValue(v),
-        ),
-      );
-    }
-
-    return column.mapFromDriverValue(column.mapToDriverValue(value));
-  } catch (e) {
-    if (
-      (e as Error)?.message?.includes("Do not know how to serialize a BigInt")
-    ) {
-      const error = new BigIntSerializationError((e as Error).message);
-      error.meta.push(
-        "Hint:\n  The JSON column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/utilities/replace-bigints",
-      );
-      throw error;
-    }
-  }
-};
-
-export const normalizeRow = (
-  table: Table,
-  row: { [key: string]: unknown },
-  isUpdate: boolean,
-) => {
-  for (const [columnName, column] of Object.entries(getTableColumns(table))) {
-    // not-null constraint
-    if (
-      isUpdate === false &&
-      (row[columnName] === undefined || row[columnName] === null) &&
-      column.notNull &&
-      hasEmptyValue(column) === false
-    ) {
-      const error = new NotNullConstraintError(
-        `Column '${getTableName(
-          table,
-        )}.${columnName}' violates not-null constraint.`,
-      );
-      error.meta.push(`db.insert arguments:\n${prettyPrint(row)}`);
-      throw error;
-    }
-
-    row[columnName] = normalizeColumn(column, row[columnName], isUpdate);
-  }
-
-  return row;
-};
-
-export const getCacheKey = (
-  table: Table,
-  key: object,
-  cache?: Map<Table, [string, Column][]>,
-): string => {
-  if (cache) {
-    const primaryKeys = cache.get(table)!;
-    return (
-      primaryKeys
-        // @ts-ignore
-        .map(([pk, col]) => normalizeColumn(col, key[pk]))
-        .join("_")
-    );
-  }
-
-  const primaryKeys = getPrimaryKeyColumns(table);
-  return (
-    primaryKeys
-      // @ts-ignore
-      .map((pk) => normalizeColumn(table[pk.js], key[pk.js]))
-      .join("_")
-  );
-};
-
-/** Returns an sql where condition for `table` with `key`. */
-export const getWhereCondition = (table: Table, key: Object): SQL<unknown> => {
-  const conditions: SQLWrapper[] = [];
-
-  for (const { js } of getPrimaryKeyColumns(table)) {
-    // @ts-ignore
-    conditions.push(eq(table[js]!, key[js]));
-  }
-
-  return and(...conditions)!;
-};
-
 const getBytes = (value: unknown) => {
   // size of metadata
   let size = 13;
diff --git a/packages/core/src/indexing-store/realtime.ts b/packages/core/src/indexing-store/realtime.ts
index 4733f2c6..8c4f9960 100644
--- a/packages/core/src/indexing-store/realtime.ts
+++ b/packages/core/src/indexing-store/realtime.ts
@@ -7,12 +7,12 @@ import { prettyPrint } from "@/utils/print.js";
 import { startClock } from "@/utils/timer.js";
 import { type QueryWithTypings, type Table, getTableName } from "drizzle-orm";
 import { drizzle } from "drizzle-orm/pg-proxy";
-import { getCacheKey, getWhereCondition } from "./cache.js";
 import {
   type IndexingStore,
   checkOnchainTable,
   parseSqlError,
 } from "./index.js";
+import { getCacheKey, getWhereCondition } from "./utils.js";
 
 export const createRealtimeIndexingStore = ({
   common,
diff --git a/packages/core/src/indexing-store/utils.ts b/packages/core/src/indexing-store/utils.ts
new file mode 100644
index 00000000..753744a4
--- /dev/null
+++ b/packages/core/src/indexing-store/utils.ts
@@ -0,0 +1,138 @@
+import { getPrimaryKeyColumns } from "@/drizzle/index.js";
+import {
+  BigIntSerializationError,
+  NotNullConstraintError,
+} from "@/internal/errors.js";
+import { prettyPrint } from "@/utils/print.js";
+import {
+  type Column,
+  type SQL,
+  type SQLWrapper,
+  type Table,
+  and,
+  eq,
+  getTableColumns,
+  getTableName,
+} from "drizzle-orm";
+import { PgArray } from "drizzle-orm/pg-core";
+
+/**
+ * Returns true if the column has a "default" value that is used when no value is passed.
+ * Handles `.default`, `.$defaultFn()`, `.$onUpdateFn()`.
+ */
+export const hasEmptyValue = (column: Column) => {
+  return column.hasDefault;
+};
+
+/** Returns the "default" value for `column`. */
+export const getEmptyValue = (column: Column, isUpdate: boolean) => {
+  if (isUpdate && column.onUpdateFn) {
+    return column.onUpdateFn();
+  }
+  if (column.default !== undefined) return column.default;
+  if (column.defaultFn !== undefined) return column.defaultFn();
+  if (column.onUpdateFn !== undefined) return column.onUpdateFn();
+
+  // TODO(kyle) is it an invariant that it doesn't get here
+
+  return undefined;
+};
+
+export const normalizeColumn = (
+  column: Column,
+  value: unknown,
+  isUpdate: boolean,
+  // @ts-ignore
+): unknown => {
+  if (value === undefined) {
+    if (hasEmptyValue(column)) return getEmptyValue(column, isUpdate);
+    return null;
+  }
+  if (value === null) return null;
+  if (column.mapToDriverValue === undefined) return value;
+  try {
+    if (Array.isArray(value) && column instanceof PgArray) {
+      return value.map((v) =>
+        column.baseColumn.mapFromDriverValue(
+          column.baseColumn.mapToDriverValue(v),
+        ),
+      );
+    }
+
+    return column.mapFromDriverValue(column.mapToDriverValue(value));
+  } catch (e) {
+    if (
+      (e as Error)?.message?.includes("Do not know how to serialize a BigInt")
+    ) {
+      const error = new BigIntSerializationError((e as Error).message);
+      error.meta.push(
+        "Hint:\n  The JSON column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/utilities/replace-bigints",
+      );
+      throw error;
+    }
+  }
+};
+
+export const normalizeRow = (
+  table: Table,
+  row: { [key: string]: unknown },
+  isUpdate: boolean,
+) => {
+  for (const [columnName, column] of Object.entries(getTableColumns(table))) {
+    // not-null constraint
+    if (
+      isUpdate === false &&
+      (row[columnName] === undefined || row[columnName] === null) &&
+      column.notNull &&
+      hasEmptyValue(column) === false
+    ) {
+      const error = new NotNullConstraintError(
+        `Column '${getTableName(
+          table,
+        )}.${columnName}' violates not-null constraint.`,
+      );
+      error.meta.push(`db.insert arguments:\n${prettyPrint(row)}`);
+      throw error;
+    }
+
+    row[columnName] = normalizeColumn(column, row[columnName], isUpdate);
+  }
+
+  return row;
+};
+
+export const getCacheKey = (
+  table: Table,
+  key: object,
+  cache?: Map<Table, [string, Column][]>,
+): string => {
+  if (cache) {
+    const primaryKeys = cache.get(table)!;
+    return (
+      primaryKeys
+        // @ts-ignore
+        .map(([pk, col]) => normalizeColumn(col, key[pk]))
+        .join("_")
+    );
+  }
+
+  const primaryKeys = getPrimaryKeyColumns(table);
+  return (
+    primaryKeys
+      // @ts-ignore
+      .map((pk) => normalizeColumn(table[pk.js], key[pk.js]))
+      .join("_")
+  );
+};
+
+/** Returns an sql where condition for `table` with `key`. */
+export const getWhereCondition = (table: Table, key: Object): SQL<unknown> => {
+  const conditions: SQLWrapper[] = [];
+
+  for (const { js } of getPrimaryKeyColumns(table)) {
+    // @ts-ignore
+    conditions.push(eq(table[js]!, key[js]));
+  }
+
+  return and(...conditions)!;
+};


Test patch:
diff --git a/packages/core/src/index.test.ts b/packages/core/src/index.test.ts
new file mode 100644
index 00000000..4e835cdc
--- /dev/null
+++ b/packages/core/src/index.test.ts
@@ -0,0 +1,45 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { build } from "vite";
+import { expect, test } from "vitest";
+
+let __dirname = fileURLToPath(new URL(".", import.meta.url));
+__dirname = path.resolve(__dirname, "..");
+
+const packageJson = JSON.parse(
+  readFileSync(path.resolve(__dirname, "./package.json"), "utf-8"),
+);
+const dependencies = Object.keys(packageJson.dependencies).filter(
+  (dep) => !["@ponder/client", "@ponder/utils"].includes(dep),
+);
+
+test("should bundle the entry file for the browser without throwing", async () => {
+  await expect(
+    build({
+      resolve: {
+        alias: {
+          "@": path.resolve(__dirname, "./src"),
+          "@ponder/client": path.resolve(__dirname, "../client/src"),
+          "@ponder/utils": path.resolve(__dirname, "../utils/src"),
+        },
+      },
+      // Mock build settings
+      logLevel: "error",
+      build: {
+        lib: {
+          entry: path.resolve(__dirname, "./src/index.ts"),
+          name: "ponder",
+          formats: ["es"],
+        },
+        // Speed up the build
+        write: false,
+        minify: false,
+        reportCompressedSize: false,
+        sourcemap: false,
+        // Exclude all dependencies
+        rollupOptions: { external: dependencies },
+      },
+    }),
+  ).resolves.toBeDefined();
+});