Fix the generated client's identity to hold across module instances, in every environment - #1548
Conversation
…gistry entry The generated context's "once per process" client guarantee rested on a module-local memo plus a globalForClient global written only outside production. A bundler that compiles .opensaas/context.ts into more than one bundle gives each copy its own module scope, so two copies could each construct their own client and pool — and in production, where the global was never written, nothing spanned them at all. getClient() now publishes the client through core's existing processGlobal registry (the same mechanism the Engine stamp's origin store already uses), unconditionally in every environment. Adds ADR-0070 and a probe proving two on-disk copies of the generated module share one client under NODE_ENV=production. Closes #1201 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TTvKf8g5qdqX9jJaVKSPjv
🦋 Changeset detectedLatest commit: 101d5fa The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Code review (medium-high effort, correctness / reuse-simplification-efficiency)Reviewed the diff against No blocking issues. The fix is well-scoped: One non-blocking observation worth a decision, not a fix requirement:
This is almost certainly out of scope for what #1201 asked to fix (one app, bundler-duplicated), and may be an acceptable, documented limitation rather than something to change here — but it seems worth either a one-line callout in the ADR's Consequences section (in the spirit of the "costs taken knowingly" pattern this repo already uses elsewhere) or a deliberate "not a concern because X" note, so it's a recorded decision rather than an implicit gap. Nothing else stood out on reuse/simplification/efficiency — the duplicated Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com Generated by Claude Code |
…y key change
packages/auth/tests/generated-project.ts reached into the old globalThis
key by its bare string name ('opensaasClient') to close and drop the
generated bundle's cached client between sequentially-generated projects.
That reach-in silently stopped working once the previous commit moved the
key to a Symbol.for(processGlobalKey('client')) slot, so a later project's
getClient() adopted an earlier, already-closed project's client instead of
constructing its own — surfacing in CI as ECONNREFUSED and "database does
not exist" in rate-limit-e2e.test.ts.
Exports processGlobalKey alongside processGlobal from
@opensaas/stack-core/internal so the test harness can address the real
key, and updates both the harness's cleanup and the test asserting the
slot is empty after close(). Also documents the registry key's known
limit (it names the slot, not a particular app/config) in ADR-0070.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TTvKf8g5qdqX9jJaVKSPjv
|
Deployment failed for project stack-docs with the following error: Learn More: https://vercel.com/open-saas?upgradeToPro=build-rate-limit |
|
Thanks for the review. On the non-blocking observation about Fixed in 101d5fa: exported Generated by Claude Code |
|
Note on CI: the Generated by Claude Code |
Coverage Report for Core Package Coverage (./packages/core)
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||
Coverage Report for UI Package Coverage (./packages/ui)
File CoverageNo changed files found. |
Coverage Report for CLI Package Coverage (./packages/cli)
File Coverage
|
||||||||||||||||||||||||||||||||||||||
Coverage Report for Auth Package Coverage (./packages/auth)
File CoverageNo changed files found. |
Coverage Report for Storage Package Coverage (./packages/storage)
File CoverageNo changed files found. |
Coverage Report for RAG Package Coverage (./packages/rag)
File CoverageNo changed files found. |
Coverage Report for Storage S3 Package Coverage (./packages/storage-s3)
File CoverageNo changed files found. |
Coverage Report for Storage Vercel Package Coverage (./packages/storage-vercel)
File CoverageNo changed files found. |
…llision 0070-the-auth-plugins-id-strategy-is-config-not-a-hardcoded-pin.md (#1550) and 0070-the-generated-clients-identity-is-a-process-wide-registry-entry.md (#1548) landed within a minute of each other and both claimed 0070, tripping check:adr-duplicates on every PR against main. Renumbered the id-strategy ADR to 0071 (2 filename-link updates in ADR-0048 and ADR-0060) rather than the client-identity one, which has 7+ scattered bare "(ADR-0070)" comment references across packages/core, packages/cli and packages/auth that would otherwise need updating instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEEWCWcKXFFYY3PXE2zBH6
…llision 0070-the-auth-plugins-id-strategy-is-config-not-a-hardcoded-pin.md (#1550) and 0070-the-generated-clients-identity-is-a-process-wide-registry-entry.md (#1548) landed within a minute of each other and both claimed 0070, tripping check:adr-duplicates on every PR against main. Renumbered the id-strategy ADR to 0071 (2 filename-link updates in ADR-0048 and ADR-0060) rather than the client-identity one, which has 7+ scattered bare "(ADR-0070)" comment references across packages/core, packages/cli and packages/auth that would otherwise need updating instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEEWCWcKXFFYY3PXE2zBH6
…llision (#1555) 0070-the-auth-plugins-id-strategy-is-config-not-a-hardcoded-pin.md (#1550) and 0070-the-generated-clients-identity-is-a-process-wide-registry-entry.md (#1548) landed within a minute of each other and both claimed 0070, tripping check:adr-duplicates on every PR against main. Renumbered the id-strategy ADR to 0071 (2 filename-link updates in ADR-0048 and ADR-0060) rather than the client-identity one, which has 7+ scattered bare "(ADR-0070)" comment references across packages/core, packages/cli and packages/auth that would otherwise need updating instead. Claude-Session: https://claude.ai/code/session_01HEEWCWcKXFFYY3PXE2zBH6 Co-authored-by: Claude <noreply@anthropic.com>
Summary
clientPromisememo plusglobalForClient.opensaasClient, a plainglobalThisproperty written only whenNODE_ENV !== 'production'..opensaas/context.tsinto more than one bundle gives each copy its own module scope. Under the old code, two such copies each construct their own client and pool — and in production, where the global was never written, nothing spanned the copies at all. That's backwards: production is where a duplicated pool against the Dev database's socket-multiplexed session, or against a deployment's connection ceiling, costs the most.getClient()now publishes the client through core's existingprocessGlobalregistry (@opensaas/stack-core/internal) — the sameSymbol.for/globalThismechanism the Engine stamp's origin store and the engine-context face already use — unconditionally, in every environment. TheNODE_ENVbranch is deleted outright rather than widened.isRuntimeClientis a structural guard (mirrorsunsafe.ts'sUnsafeCapableClientshape:orm,sql,raw,transaction) used to verify a value already published under the registry key before adopting it, consistent with howoriginStore's ownischeck works.docs/adr/0070-the-generated-clients-identity-is-a-process-wide-registry-entry.mdrecording the decision and the considered alternatives (a version-namespaced key, a bespoke runtime registry, documenting per-module-instance identity instead).maxConnectionsdoc comment: its headroom was always meant to count distinct processes sharing one sidecar, and this fix is what makes that reading hold for a single app process a bundler happens to duplicate. No change to the constant itself.Test plan
packages/cli/tests/bundle-client-construction.test.ts: imports two on-disk copies of the generatedcontext.tsunderNODE_ENV=productionand asserts the pg-pool factory is called exactly once across both — this fails against the old per-module-instance behavior (verified locally by reverting the generator change and confirming the new test fails with "expected 2 to be 1", then restoring the fix and confirming it passes).packages/cli/src/generator/context.test.tsand its snapshot to assert the newprocessGlobal-based shape and the absence ofNODE_ENV/globalForClient.pnpm buildclean acrosscore,cli,auth,rag,storage,tiptap.pnpm testgreen: core (1757 passed), cli (554 passed), auth (509 passed).pnpm lintclean.pnpm manypkg fix/pnpm formatrun.@opensaas/stack-core+@opensaas/stack-cli, patch).Closes #1201
🤖 Generated with Claude Code
https://claude.ai/code/session_01TTvKf8g5qdqX9jJaVKSPjv
Generated by Claude Code