Found by the HotCRM single-database multi-tenant acceptance added in objectstack-ai/cloud#1350. Measured on a real objectstack serve boot of apps/objectos-ee, OS_TENANCY_POSTURE=isolated, licensed, OS_CLOUD_URL=off, with the real 342-row HotCRM artifact installed through POST /api/v1/marketplace/install-local.
What happens
- The install succeeds:
{"success":true,...,"seeded":{"mode":"inline","inserted":342,"errors":0}}. The installer's own organization gets a full copy.
- A customer then founds an organization. The per-organization replay logs, once per organization:
WARN [org-scoping] per-org seed: datasets present but no replayer registered {"organizationId":"..."}
- That organization holds zero rows of the app that was just installed. Every object, every dataset.
The probe is not blind: the same GET /api/v1/data/crm_account returns the full copy for the installer's organization in the same run.
Mechanism
MarketplaceInstallLocalPlugin.applySideEffects calls mergeSeedDatasetsIntoKernel, which correctly appends the package's data blocks onto the shared seed-datasets service (this is what #3453 fixed). It never calls registerSeedReplayerOnce.
Its own docblock states the assumption that then fails:
the per-org replayer (AppPlugin) replays the whole union on the next sys_organization insert
That holds only when an AppPlugin app with seed data is ALSO mounted on the kernel — registerSeedReplayerOnce lives in AppPlugin's seeder path. On a runtime whose host app declares no data of its own — which is precisely the shape a marketplace install targets, and exactly what apps/objectos-ee is (objects: [], no data) — seed-datasets is populated and seed-replayer is absent. The middleware finds datasets, finds no replayer, and does nothing.
So the merge landed and the consumer never did.
Why this matters
This is the designed distribution path for marketplace apps into a customer's own enterprise runtime. On a walled (isolated / group) deployment it means: install the app, then every customer organization created afterwards signs up to an app with no data at all. The installer's own organization is the only one that ever gets a copy, and it gets it by the install-time inline seed rather than by the replay.
Contrast, same artifact, same boot shape
The composed-artifact boot (OS_COMPOSED_ARTIFACT_URL, objectstack-ai/cloud#1347) mounts the app through createDefaultHostConfig and therefore through AppPlugin, which DOES register the replayer. On that path both tenant organizations receive their own private copy. Same runtime, same artifact, same posture — only the arrival path differs.
Suggested shape of the fix
applySideEffects should register the replayer alongside the merge, using the same registerSeedReplayerOnce helper AppPlugin uses (it is register-once, so a host that already has one is unaffected — that is what the helper is for).
Pinned
apps/objectos-ee/test/hotcrm-install-local.acceptance.ts in objectstack-ai/cloud pins the defect as measured, with the datasets present but no replayer registered line as the tripwire. When this is fixed that test goes RED and must be promoted into a full per-organization assertion, not repaired.
Related: #3453 (the merge half of this, fixed).
Generated by Claude Code
Found by the HotCRM single-database multi-tenant acceptance added in objectstack-ai/cloud#1350. Measured on a real
objectstack serveboot ofapps/objectos-ee,OS_TENANCY_POSTURE=isolated, licensed,OS_CLOUD_URL=off, with the real 342-row HotCRM artifact installed throughPOST /api/v1/marketplace/install-local.What happens
{"success":true,...,"seeded":{"mode":"inline","inserted":342,"errors":0}}. The installer's own organization gets a full copy.The probe is not blind: the same
GET /api/v1/data/crm_accountreturns the full copy for the installer's organization in the same run.Mechanism
MarketplaceInstallLocalPlugin.applySideEffectscallsmergeSeedDatasetsIntoKernel, which correctly appends the package'sdatablocks onto the sharedseed-datasetsservice (this is what #3453 fixed). It never callsregisterSeedReplayerOnce.Its own docblock states the assumption that then fails:
That holds only when an
AppPluginapp with seed data is ALSO mounted on the kernel —registerSeedReplayerOncelives inAppPlugin's seeder path. On a runtime whose host app declares no data of its own — which is precisely the shape a marketplace install targets, and exactly whatapps/objectos-eeis (objects: [], nodata) —seed-datasetsis populated andseed-replayeris absent. The middleware finds datasets, finds no replayer, and does nothing.So the merge landed and the consumer never did.
Why this matters
This is the designed distribution path for marketplace apps into a customer's own enterprise runtime. On a walled (
isolated/group) deployment it means: install the app, then every customer organization created afterwards signs up to an app with no data at all. The installer's own organization is the only one that ever gets a copy, and it gets it by the install-time inline seed rather than by the replay.Contrast, same artifact, same boot shape
The composed-artifact boot (
OS_COMPOSED_ARTIFACT_URL, objectstack-ai/cloud#1347) mounts the app throughcreateDefaultHostConfigand therefore throughAppPlugin, which DOES register the replayer. On that path both tenant organizations receive their own private copy. Same runtime, same artifact, same posture — only the arrival path differs.Suggested shape of the fix
applySideEffectsshould register the replayer alongside the merge, using the sameregisterSeedReplayerOncehelperAppPluginuses (it is register-once, so a host that already has one is unaffected — that is what the helper is for).Pinned
apps/objectos-ee/test/hotcrm-install-local.acceptance.tsin objectstack-ai/cloud pins the defect as measured, with thedatasets present but no replayer registeredline as the tripwire. When this is fixed that test goes RED and must be promoted into a full per-organization assertion, not repaired.Related: #3453 (the merge half of this, fixed).
Generated by Claude Code