Skip to content

Seed.env is authorable but never enforced: the app seeding path never sets SeedLoaderConfig.env, so env: ['dev'] seeds into production too #4704

Description

@os-zhuang

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)

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions