Cross-repo seam card, filed by the domain:ui execution seat (session session_012u2pRjcqAYtoEjgr3wwhnK, objectui seat post objectui#5560) while working objectui#5668 at the maintainer's explicit direction. Filed here rather than fixed there because the repair is in packages/spec and objectui cannot make it — see Why objectui cannot fix this.
Named reader: the domain:spec seat. This is not a report of a change objectui wants; it is a boundary the published package does not currently declare.
What is broken, in one sentence
@objectstack/spec@17.1.0 declares pg-connection-string as a hard runtime dependency and imports it at the top level of its browser-facing entry bundles, with no browser export condition anywhere in the package. Any bundler building a client graph that reaches @objectstack/spec must therefore resolve fs, and fails.
objectui's docs site (Next.js 16.3.1 / Turbopack) is red on main on exactly this:
./node_modules/.pnpm/pg-connection-string@2.14.0/node_modules/pg-connection-string/index.js:88:70
Error: Module not found: Can't resolve 'fs'
87 | // Only try to load fs if we expect to read from the disk
> 88 | const fs = config.sslcert || config.sslkey || config.sslrootcert ? require('fs') : null
Import chain, identical in the Client Component Browser and Client Component SSR traces:
pg-connection-string@2.14.0
<- @objectstack/spec@17.1.0/dist/index.mjs
<- @objectstack/formula@17.1.0/dist/index.mjs
<- packages/core/dist/evaluator/fieldRules.js
<- packages/components/dist/index.js
<- apps/site/app/components/InteractiveDemo.tsx [Client Component]
Measured: it arrives with 17.1.0, single-commit isolated
A one-legged bisect in objectui, both legs built with pnpm turbo run build --filter='@object-ui/site' (the exact command objectui's Build Docs job runs), each after pnpm install --frozen-lockfile:
| objectui commit |
@objectstack/spec resolved |
site build |
aa3b81062 (parent of the bump) |
17.0.0 |
GREEN — Tasks: 29 successful, 29 total, turbo exit 0 |
2d36552d2 "Pin @objectstack/* to 17.1.0" (objectui#5529) |
17.1.0 |
RED — Tasks: 28 successful, 29 total, Failed: @object-ui/site#build, turbo exit 1 |
6ff0eb1e7 (objectui origin/main today) |
17.1.0 |
RED — identical trace and tally |
--frozen-lockfile on both legs is load-bearing: every objectui manifest declares ^17.0.0, so a non-frozen install re-resolves to 17.1.0 and would have turned the green leg red for the wrong reason.
The lockfile diff of that one commit introduces the package rather than bumping it — there is no removal counterpart anywhere in the diff:
- '@objectstack/spec@17.0.0(ai@7.0.65(zod@4.4.3))':
+ '@objectstack/spec@17.1.0(ai@7.0.65(zod@4.4.3))':
dependencies:
+ pg-connection-string: 2.14.0
zod: 4.4.3
And byte-level, from the two installed trees side by side:
@objectstack+spec@17.0.0.../dist/index.mjs pg-connection-string in first 3 lines: 0
@objectstack+spec@17.1.0.../dist/index.mjs pg-connection-string in first 3 lines: 1
Exactly one objectui commit in the whole breakage window touches pnpm-lock.yaml or any package.json; the other five are source-only.
The mechanism, measured from the published 17.1.0 tarball
1. The dependency is hard, not optional. package.json: "dependencies": { "pg-connection-string": "^2.14.0", "zod": "^4.4.3" }.
2. The exports map declares no browser condition. Counted on the published package and again on this repo's origin/main (3e26359, packages/spec/package.json still version: 17.1.0):
browser conditions in exports: 0
browser field: undefined
Every entry is { import: { types, default }, require: { types, default } }. There is no condition a browser bundler could select, so a browser consumer and a Node consumer resolve the same file by construction.
3. It is line 2 of the main entry. dist/index.mjs:
import { z } from 'zod';
import { parse } from 'pg-connection-string';
Top-level, static, unconditional — not a lazy import(), not behind a runtime guard.
4. Six of the sixteen entry points carry it. Classified by whether the top of each entry bundle imports pg-connection-string:
POISONED . clean ./ai clean ./integration
POISONED ./cloud clean ./api clean ./qa
POISONED ./data clean ./automation clean ./security
POISONED ./kernel clean ./contracts clean ./studio
POISONED ./shared clean ./identity clean ./ui
POISONED ./system
./shared, ./data and ./kernel being on that list is what makes this not routable around: they are the entries a browser consumer of field, filter and expression schemas actually needs.
5. There is exactly one call site. In dist/index.mjs the only use of the import is parse(value) inside pgParseableUrl(), a Zod superRefine that refuses a postgres datasource config.url that pg could not open. Source: packages/spec/src/data/driver/postgres.zod.ts:16 (import { parse as parsePostgresUrl } from 'pg-connection-string';), reachable from ./data via packages/spec/src/data/index.ts:176 (export * from './driver/index';) and from the root barrel through the same chain.
That refinement is a publish-time, server-side authoring check. It is unreachable in a browser docs page by construction, so every byte of it in a client bundle is dead weight.
6. pg-connection-string itself declares no browser story either. 2.14.0 has no browser field and no browser export condition; its ESM entry is a four-line wrapper importing the CJS index.js, so the require('fs') on line 88 — runtime-guarded, bundler-static — is an unconditional resolution request in any client graph.
How objectui reaches it (and why a subpath swap does not help)
@objectstack/formula@17.1.0 is the bridge. Its dist/index.mjs imports the root barrel for one identity helper:
import { createEvalUser } from "@objectstack/spec"; // src/stdlib.ts:17
createEvalUser lives in src/identity/eval-user.zod.ts, and dist/identity/index.mjs is clean. So "have formula import from @objectstack/spec/identity" looks like a one-line fix — and it is not sufficient, measured: the same dist/index.mjs also imports @objectstack/spec/data (lines 1607–1608, for nextUtcCalendarDay, utcInstantMs, asciiCaseInsensitiveContains, matchesLikePattern), and ./data is one of the six poisoned entries. Any fix that only re-points the root-barrel import leaves the breakage in place.
Why objectui cannot fix this
objectui's triage put a hard prohibition on the obvious local move, and the measurement above makes it more binding, not less:
Don't paper over it with a Turbopack fs shim/alias without knowing which — a shim over a real boundary regression ships pg-connection-string to browsers as accepted cargo.
A resolveAlias: { fs: false }, a webpack resolve.fallback, or a browser override in objectui would make the build green and would work at runtime, because the require('fs') branch is only taken for ?sslcert= / ?sslkey= / ?sslrootcert= — which this same schema refuses one step earlier. That is precisely what makes the shim dangerous rather than safe: it succeeds, so nothing would ever prompt anyone to look again, and a Postgres connection-string parser becomes permanent accepted cargo in every ObjectUI browser bundle. Fixing a producer's boundary with a consumer-side tolerance is also what objectui's AGENTS.md #0.1 (contract-first) forbids by name.
The other local option, reverting objectui#5529's pin to 17.0.0, is not a lockfile-only revert and is reported on objectui#5668 rather than taken: #5529 also adapted objectui to the 17.1.0 contract (the new icontains operator wired into a table keyed by ViewFilterOperator, which fails to compile without it; five new record:* blocks pinned in the console registry parity; a re-baselined eager-closure ceiling raised under the maintainer's ruling on objectui#5531). Reverting trades a red Build Docs for a red Type Check and undoes a ruling.
Options, for the domain:spec seat to grade
Stated as options rather than a demand — the cost model here belongs to this repo.
- A. Declare the boundary in the
exports map. Add a browser condition for the affected entries pointing at a build with the driver-config schemas excluded (or with the postgres URL refinement degraded to the shape-only checks it already performs before parse). Long-term soundness: highest — declared = enforced, and a browser consumer can no longer accidentally receive server-only publish-time validators. Cost: a second tsup output and a rule about what may live behind it.
- B. Move the DSN refusal out of the statically-reachable graph. Keep one build, but make
parse reachable only through a server-only entry (for example @objectstack/spec/publish) that the data / shared / kernel / root barrels do not re-export. Long-term soundness: high, and it is the smaller diff. Risk: it moves a public export, so it is a spec surface change.
- C. Drop the dependency and hand-roll the parse. Rejected on evidence, recorded so it is not re-proposed:
packages/spec/src/data/driver/postgres.zod.ts's own docblock and the objectstack#8082 / objectstack#8337 rulings turn on using the parser pg itself uses; a hand-rolled parser would agree with pg until it did not, which is the failure mode the refusal exists to prevent.
- D. Do nothing upstream; let each browser consumer alias
fs. Rejected above. Named only so the option set is complete.
Recommendation: A, with B as the smaller interim if A cannot land quickly. Four axes:
- Real business need — measured, not speculative: it is red on objectui
main right now and holds an ACCEPTed PR. The consumer is real and named.
- Long-term soundness for this project — a schema package is imported by browsers by design; it having no browser condition at all is the actual defect, and this dependency is only the first one to make it visible. Fixing the instance without declaring the boundary leaves the next server-only dependency free to do the same thing.
- Making AI-written code hard to get wrong — an author or an agent adding a Node-only import to a
*.zod.ts file today gets no signal at all; the cost lands in a downstream repo's bundler, hours later, attributed to whichever PR happened to touch a docs path. A declared browser condition (plus a gate over it) turns that into a refusal at the producer.
- Startup scope discipline — argues for the smallest change that declares the boundary, not a bundling redesign. Both A and B are bounded; neither expands the spec's capability surface. It also argues against C, which is new code for a solved problem.
Reproduction, ready to run
In objectui, at origin/main:
git worktree add ../objectui-pgcs -b probe/pgcs origin/main && cd ../objectui-pgcs
pnpm install --frozen-lockfile
pnpm turbo run build --filter='@object-ui/site' # exits 1, "Can't resolve 'fs'"
Against the installed package alone, no build needed:
SPEC=$(ls -d node_modules/.pnpm/@objectstack+spec@17.1.0*/node_modules/@objectstack/spec)
head -3 "$SPEC/dist/index.mjs" # line 2 is the pg-connection-string import
for f in "$SPEC"/dist/index.mjs "$SPEC"/dist/*/index.mjs; do
head -3 "$f" | grep -q pg-connection-string && echo "POISONED $f" || echo "clean $f"
done
Dependency edge
This card waits on nothing. objectui#5668 waits on it: a released @objectstack/spec whose browser-facing entries do not statically import pg-connection-string unblocks objectui's Build Docs on a plain lockfile refresh, with no objectui source change. Following the objectstack#10812 convention, the machine-readable Blocked-by: line lives on the waiting card.
Adjacent, not duplicate
- objectstack#8133 — the same
exports map and tsup pipeline, from the cross-entry-identity and no-shared-chunks angle. It is pm:on-hold with restart-when: any PR touches the packages/spec build/publish pipeline (tsup config, or the exports map in packages/spec/package.json) — options A and B both touch exactly that, so whoever takes this should read objectstack#8133 first and decide whether the two land together.
- objectstack#9659 —
@objectstack/formula among the four packages still reaching the Console bundle from objectui's lockfile. Different problem (publish ordering), same edge.
- objectui#5668 — the consumer-side card carrying the CI evidence and the merge impact.
Generated by Claude Code
Cross-repo seam card, filed by the
domain:uiexecution seat (sessionsession_012u2pRjcqAYtoEjgr3wwhnK, objectui seat post objectui#5560) while working objectui#5668 at the maintainer's explicit direction. Filed here rather than fixed there because the repair is inpackages/specand objectui cannot make it — see Why objectui cannot fix this.Named reader: the
domain:specseat. This is not a report of a change objectui wants; it is a boundary the published package does not currently declare.What is broken, in one sentence
@objectstack/spec@17.1.0declarespg-connection-stringas a hard runtime dependency and imports it at the top level of its browser-facing entry bundles, with nobrowserexport condition anywhere in the package. Any bundler building a client graph that reaches@objectstack/specmust therefore resolvefs, and fails.objectui's docs site (Next.js 16.3.1 / Turbopack) is red on
mainon exactly this:Import chain, identical in the Client Component Browser and Client Component SSR traces:
Measured: it arrives with 17.1.0, single-commit isolated
A one-legged bisect in objectui, both legs built with
pnpm turbo run build --filter='@object-ui/site'(the exact command objectui'sBuild Docsjob runs), each afterpnpm install --frozen-lockfile:@objectstack/specresolvedaa3b81062(parent of the bump)Tasks: 29 successful, 29 total, turbo exit02d36552d2"Pin @objectstack/* to 17.1.0" (objectui#5529)Tasks: 28 successful, 29 total,Failed: @object-ui/site#build, turbo exit16ff0eb1e7(objectuiorigin/maintoday)--frozen-lockfileon both legs is load-bearing: every objectui manifest declares^17.0.0, so a non-frozen install re-resolves to 17.1.0 and would have turned the green leg red for the wrong reason.The lockfile diff of that one commit introduces the package rather than bumping it — there is no removal counterpart anywhere in the diff:
And byte-level, from the two installed trees side by side:
Exactly one objectui commit in the whole breakage window touches
pnpm-lock.yamlor anypackage.json; the other five are source-only.The mechanism, measured from the published 17.1.0 tarball
1. The dependency is hard, not optional.
package.json:"dependencies": { "pg-connection-string": "^2.14.0", "zod": "^4.4.3" }.2. The
exportsmap declares no browser condition. Counted on the published package and again on this repo'sorigin/main(3e26359,packages/spec/package.jsonstillversion: 17.1.0):Every entry is
{ import: { types, default }, require: { types, default } }. There is no condition a browser bundler could select, so a browser consumer and a Node consumer resolve the same file by construction.3. It is line 2 of the main entry.
dist/index.mjs:Top-level, static, unconditional — not a lazy
import(), not behind a runtime guard.4. Six of the sixteen entry points carry it. Classified by whether the top of each entry bundle imports
pg-connection-string:./shared,./dataand./kernelbeing on that list is what makes this not routable around: they are the entries a browser consumer of field, filter and expression schemas actually needs.5. There is exactly one call site. In
dist/index.mjsthe only use of the import isparse(value)insidepgParseableUrl(), a ZodsuperRefinethat refuses a postgres datasourceconfig.urlthatpgcould not open. Source:packages/spec/src/data/driver/postgres.zod.ts:16(import { parse as parsePostgresUrl } from 'pg-connection-string';), reachable from./dataviapackages/spec/src/data/index.ts:176(export * from './driver/index';) and from the root barrel through the same chain.That refinement is a publish-time, server-side authoring check. It is unreachable in a browser docs page by construction, so every byte of it in a client bundle is dead weight.
6.
pg-connection-stringitself declares no browser story either.2.14.0has nobrowserfield and nobrowserexport condition; its ESM entry is a four-line wrapper importing the CJSindex.js, so therequire('fs')on line 88 — runtime-guarded, bundler-static — is an unconditional resolution request in any client graph.How objectui reaches it (and why a subpath swap does not help)
@objectstack/formula@17.1.0is the bridge. Itsdist/index.mjsimports the root barrel for one identity helper:createEvalUserlives insrc/identity/eval-user.zod.ts, anddist/identity/index.mjsis clean. So "have formula import from@objectstack/spec/identity" looks like a one-line fix — and it is not sufficient, measured: the samedist/index.mjsalso imports@objectstack/spec/data(lines 1607–1608, fornextUtcCalendarDay,utcInstantMs,asciiCaseInsensitiveContains,matchesLikePattern), and./datais one of the six poisoned entries. Any fix that only re-points the root-barrel import leaves the breakage in place.Why objectui cannot fix this
objectui's triage put a hard prohibition on the obvious local move, and the measurement above makes it more binding, not less:
A
resolveAlias: { fs: false }, a webpackresolve.fallback, or abrowseroverride in objectui would make the build green and would work at runtime, because therequire('fs')branch is only taken for?sslcert=/?sslkey=/?sslrootcert=— which this same schema refuses one step earlier. That is precisely what makes the shim dangerous rather than safe: it succeeds, so nothing would ever prompt anyone to look again, and a Postgres connection-string parser becomes permanent accepted cargo in every ObjectUI browser bundle. Fixing a producer's boundary with a consumer-side tolerance is also what objectui's AGENTS.md #0.1 (contract-first) forbids by name.The other local option, reverting objectui#5529's pin to 17.0.0, is not a lockfile-only revert and is reported on objectui#5668 rather than taken: #5529 also adapted objectui to the 17.1.0 contract (the new
icontainsoperator wired into a table keyed byViewFilterOperator, which fails to compile without it; five newrecord:*blocks pinned in the console registry parity; a re-baselined eager-closure ceiling raised under the maintainer's ruling on objectui#5531). Reverting trades a redBuild Docsfor a redType Checkand undoes a ruling.Options, for the
domain:specseat to gradeStated as options rather than a demand — the cost model here belongs to this repo.
exportsmap. Add abrowsercondition for the affected entries pointing at a build with the driver-config schemas excluded (or with the postgres URL refinement degraded to the shape-only checks it already performs beforeparse). Long-term soundness: highest —declared = enforced, and a browser consumer can no longer accidentally receive server-only publish-time validators. Cost: a second tsup output and a rule about what may live behind it.parsereachable only through a server-only entry (for example@objectstack/spec/publish) that thedata/shared/kernel/ root barrels do not re-export. Long-term soundness: high, and it is the smaller diff. Risk: it moves a public export, so it is a spec surface change.packages/spec/src/data/driver/postgres.zod.ts's own docblock and the objectstack#8082 / objectstack#8337 rulings turn on using the parserpgitself uses; a hand-rolled parser would agree withpguntil it did not, which is the failure mode the refusal exists to prevent.fs. Rejected above. Named only so the option set is complete.Recommendation: A, with B as the smaller interim if A cannot land quickly. Four axes:
mainright now and holds an ACCEPTed PR. The consumer is real and named.*.zod.tsfile today gets no signal at all; the cost lands in a downstream repo's bundler, hours later, attributed to whichever PR happened to touch a docs path. A declaredbrowsercondition (plus a gate over it) turns that into a refusal at the producer.Reproduction, ready to run
In objectui, at
origin/main:Against the installed package alone, no build needed:
Dependency edge
This card waits on nothing. objectui#5668 waits on it: a released
@objectstack/specwhose browser-facing entries do not statically importpg-connection-stringunblocks objectui'sBuild Docson a plain lockfile refresh, with no objectui source change. Following the objectstack#10812 convention, the machine-readableBlocked-by:line lives on the waiting card.Adjacent, not duplicate
exportsmap and tsup pipeline, from the cross-entry-identity and no-shared-chunks angle. It ispm:on-holdwith restart-when: any PR touches the packages/spec build/publish pipeline (tsup config, or the exports map in packages/spec/package.json) — options A and B both touch exactly that, so whoever takes this should read objectstack#8133 first and decide whether the two land together.@objectstack/formulaamong the four packages still reaching the Console bundle from objectui's lockfile. Different problem (publish ordering), same edge.Generated by Claude Code