fix(seed-loader): keep the per-org tenant stamp instead of resolving it as a natural key - #4644
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…it as a natural key The per-organization seed replay landed every row org-less, so a freshly created organization booted with data the tenant wall hides from everyone — including that organization's own owner. The stamp and the reference pass disagreed about what `organization_id` holds. The loader writes `config.organizationId` — the replay target's id. The reference pass then sees a field declared as a lookup to `sys_organization`, treats the value as a natural key, probes `sys_organization.name`, misses, and drops the field; a dropped reference takes the tenant attribution with it. The `id` fallback probe cannot rescue it: under replay every probe is AND-scoped with `organization_id = <target org>`, and `sys_organization` is the tenant table itself and carries no such column. The id SHAPE is what kept this hidden. `looksLikeInternalId` short-circuits UUID and ObjectId, so fixtures minting UUID org ids passed. Every organization better-auth creates — including the default org `ensureDefaultOrganization` bootstraps — is `org_<base36>`, which it does not recognise, so the defect fired on real deployments and nowhere else. The loader now remembers it wrote the stamp and skips resolution for that field only. A seed authoring `organization_id` itself still resolves, so naming an organization by natural key keeps working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TYoxKa8yFLiDDh7tkBqtu
os-zhuang
force-pushed
the
claude/hotcrm-multi-tenant-single-db-61tyos
branch
from
August 2, 2026 13:59
b5c2321 to
ca30220
Compare
os-zhuang
marked this pull request as ready for review
August 2, 2026 14:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was broken
In a multi-org deployment the per-organization seed replay landed every row org-less. A freshly created organization therefore booted with a database full of data that nobody could see: the tenant wall (
organization_id = <active org>) hides a NULL-org row from all members, including that organization's own owner.The seed summary reads clean while this happens — a dropped reference is counted separately from rejected rows — so there is no signal at boot.
Root cause
The stamp and the reference pass disagree about what
organization_idholds.SeedLoaderServicewritesconfig.organizationId— the replay target's id — into the record. The reference pass then sees a field declared as a lookup →sys_organization, treats the value as a natural key, and probessys_organization.namefor it. That misses, and a missed reference is dropped rather than kept, taking the tenant attribution with it.The
idfallback probe cannot rescue it either: under per-tenant replay every probe is AND-scoped withorganization_id = <target org>, andsys_organization— being the tenant table itself — carries no such column, so that probe matches nothing by construction.Why it stayed hidden
The id shape.
looksLikeInternalIdrecognises UUID and Mongo ObjectId and short-circuits resolution for both, so any fixture minting UUID organization ids passed. Every organization better-auth actually creates isorg_<base36>— including the default organizationensureDefaultOrganizationbootstraps on first boot — and that shape is not recognised.So the defect fired on real deployments and on nothing else.
The fix
The loader now remembers that it wrote the stamp itself and skips resolution for that one field. A seed that authors
organization_idexplicitly still goes through resolution, so naming an organization by its natural key keeps working.Narrow on purpose: widening
looksLikeInternalIdto acceptorg_*would be the runtime-side "accept another dialect" fallback the repo's contract-first doctrine rules out. The loader second-guessing a value it just wrote itself is the actual defect, so that is what changed.Verification
packages/objectql/src/seed-loader-org-stamp.test.ts— 3 new cases: the better-auth id shape survives (fails without the fix), the UUID shape still survives, and a seed-authored natural key still resolves.apps/ee-tenant-crm-showcasein the cloud repo (companion PR) boots two organizations over one database through@objectstack/verify. 10/10 pass with this fix; the two seed-attribution cases fail against the unfixed framework — confirmed by rebuilding at the pre-fix commit and re-running.🤖 Generated with Claude Code
https://claude.ai/code/session_019TYoxKa8yFLiDDh7tkBqtu
Generated by Claude Code