Found while implementing hotcrm#640 (a demo app needed dev-only seed rows); filed unassigned per Prime Directive #10. Measured against 17.0.0-rc.1.
The declared capability
SeedSchema in packages/spec gives every seed dataset an env axis, with a default:
env: z.ZodDefault< z.ZodArray< z.ZodEnum< { prod, dev, test } > > >
and the parsed-type comment advertises it as always-present:
Parsed/output type — all defaults are applied (env, mode, externalId always present)
Read as authoring guidance, that says: mark a dataset env: ['dev'] and it seeds
on a development install only. That is precisely the affordance an app wants for
demo-only fixtures, and it is the first thing you reach for.
What is enforced
SeedLoaderService.load() filters on the loader config's env, not the dataset's:
filterByEnv(datasets, env) {
if (!env) return datasets;
return datasets.filter((d) => d.env.includes(env));
}
SeedLoaderConfigSchema.env is z.ZodOptional with no default, and the two
places that seed an app never pass it. In AppPlugin (@objectstack/runtime),
the inline seed:
SeedLoaderRequestSchema.parse({
seeds: normalizedDatasets,
config: { defaultMode: 'upsert', multiPass: true, identity: seedIdentity },
})
and the per-org replayer, which adds only organizationId. Neither sets env.
So config.env is undefined on every app-seeding call, filterByEnv returns
all datasets, and dataset.env is never read at all. A dataset marked
env: ['dev'] seeds into production exactly as if it were marked ['prod'].
Why this is worth fixing rather than documenting
This is the declared-vs-enforced shape ADR-0049 is about, and it fails in the
dangerous direction: the author believes they have restricted the blast radius,
and the runtime silently widens it. The keys most likely to carry env: ['dev']
are demo users, fake customers, seeded credentials — the exact rows whose
appearance in a customer tenant is the failure everyone is trying to avoid. A
lint or a doc note cannot cover that, because the metadata is correct; the
consumer just never asks.
In hotcrm#640 the workaround was to keep the demo rows out of the artifact
entirely (a repo script rather than a seed), which is sound for that app but is
not available to an app that legitimately wants environment-scoped fixtures.
Repro
Author any dataset with env: ['dev'], boot with NODE_ENV=production, and
observe the rows land. Or read it statically: nothing in packages/runtime
passes env into SeedLoaderRequestSchema.parse.
The two honest ways out (ADR-0049 enforce-or-remove)
- Enforce. Have the app-seeding path derive
config.env — from NODE_ENV,
or from an explicit OS_SEED_ENV, with a documented default — so
dataset.env becomes the gate it reads like. Worth deciding what the default
is: defaulting to prod changes behaviour for every existing dataset that
left env at its schema default, so the default probably has to be "no
filter unless the host opts in", with the opt-in documented.
- Remove. Drop
env from SeedSchema and the loader's filterByEnv, and
say plainly that seed scoping is the host's business.
Either is fine. What is not fine is the current state, where the key is
authorable, type-checked, defaulted, and inert.
Found while implementing hotcrm#640 (a demo app needed dev-only seed rows); filed unassigned per Prime Directive #10. Measured against 17.0.0-rc.1.
The declared capability
SeedSchemainpackages/specgives every seed dataset anenvaxis, with a default:and the parsed-type comment advertises it as always-present:
Read as authoring guidance, that says: mark a dataset
env: ['dev']and it seedson a development install only. That is precisely the affordance an app wants for
demo-only fixtures, and it is the first thing you reach for.
What is enforced
SeedLoaderService.load()filters on the loader config's env, not the dataset's:SeedLoaderConfigSchema.envisz.ZodOptionalwith no default, and the twoplaces that seed an app never pass it. In
AppPlugin(@objectstack/runtime),the inline seed:
and the per-org replayer, which adds only
organizationId. Neither setsenv.So
config.envisundefinedon every app-seeding call,filterByEnvreturnsall datasets, and
dataset.envis never read at all. A dataset markedenv: ['dev']seeds into production exactly as if it were marked['prod'].Why this is worth fixing rather than documenting
This is the declared-vs-enforced shape ADR-0049 is about, and it fails in the
dangerous direction: the author believes they have restricted the blast radius,
and the runtime silently widens it. The keys most likely to carry
env: ['dev']are demo users, fake customers, seeded credentials — the exact rows whose
appearance in a customer tenant is the failure everyone is trying to avoid. A
lint or a doc note cannot cover that, because the metadata is correct; the
consumer just never asks.
In hotcrm#640 the workaround was to keep the demo rows out of the artifact
entirely (a repo script rather than a seed), which is sound for that app but is
not available to an app that legitimately wants environment-scoped fixtures.
Repro
Author any dataset with
env: ['dev'], boot withNODE_ENV=production, andobserve the rows land. Or read it statically: nothing in
packages/runtimepasses
envintoSeedLoaderRequestSchema.parse.The two honest ways out (ADR-0049 enforce-or-remove)
config.env— fromNODE_ENV,or from an explicit
OS_SEED_ENV, with a documented default — sodataset.envbecomes the gate it reads like. Worth deciding what the defaultis: defaulting to
prodchanges behaviour for every existing dataset thatleft
envat its schema default, so the default probably has to be "nofilter unless the host opts in", with the opt-in documented.
envfromSeedSchemaand the loader'sfilterByEnv, andsay plainly that seed scoping is the host's business.
Either is fine. What is not fine is the current state, where the key is
authorable, type-checked, defaulted, and inert.