Imported from brandonarbini/arbini.family (
.agents/skills/seed-fixtures/SKILL.md). Install upstream withnpx skills add brandonarbini/arbini.family --skill seed-fixtures. Copyright stays with the author.
Owned by dev-env and rewritten on each apply — extend it with a sibling skill, do not edit.
A seed never runs in production
The seed creates a development dataset and nothing else. Anything production needs to exist —
a system organization, a catalog, reference rows, an initial admin — is a migration (for data
that is part of the schema's meaning) or a separate, idempotent bootstrap script run once and
deliberately. Neither of those is the seed, and there is no seed:prod entry point: a script that
can rewrite production data from a developer's shell is an incident waiting for a pasted
connection string.
Refuse to run unless all three checks pass
The usual guard — checking NODE_ENV — does not catch the way it actually happens: a local
process, in development mode, whose DATABASE_URL points at production because someone pasted a
connection string to debug something an hour ago. Require all of:
NODE_ENVis notproduction. (dev-env's Compose override sets it for the seed command, so a bareNODE_ENVinside the container is not the signal — seedev-environment.)- No deploy-platform variable is set (
VERCEL_ENV,RAILWAY_*, whatever the platform sets) — this catches running inside a deployed environment. - The database URL's host is on an allowlist of known development hosts:
localhost,127.0.0.1, the Compose service name (db), the container hostname.
The third is the one that matters, and the only one that sees the pasted connection string. Put
the guard in its own module (prisma/seed-guard.ts) with its own unit test, so the checks are
readable and provably still wired.
Idempotent, always
A seed you cannot re-run is a seed that gets replaced by a database reset, and then by nothing. Upsert on a natural key — the slug, the email, the external identifier — never insert blindly and never key on a generated id. The test is running the seed twice and getting the same state as one run.
This is worth real effort for ordered or hierarchical fixtures, where the lazy version appends duplicates.
Provide a minimal mode
Seeding a rich dataset is right for most work and wrong for two cases: onboarding and empty
states. Offer --minimal that stops after the first account, which is the state a brand-new real
tenant is actually in — and the only way to test the empty screens, the first-run prompts, and the
onboarding flow at all. Both modes stay idempotent, and either can follow the other.
Copy fixture identifiers from production shapes
The most common seed defect is data that is too tidy. Invent names and you get a dataset where every slug is unique, every list is short, every string is ASCII, and nothing collides — so the UI's hardest cases never appear locally.
Copy real shapes instead: the actual slugs and counts from a live tenant (content anonymized as needed), including the long names, the near-duplicates, and the ones that happen to hash into the same bucket. If any UI derives behaviour from a value — a colour from a hash of a slug, a sort from a name, an avatar from an id — then invented values do not reproduce the collisions that UI has to survive, and the bug ships.
Aim the dataset at the states that are hard to reach by clicking: overdue items, a partially-completed flow, an item assigned to someone else, a period boundary. Fixtures that link to each other are worth the extra care, because a dangling link in a seed reads as a product bug.
Rationale
- Why not fixtures generated from the schema — random data satisfies types and nothing else: it never produces the specific collision, ordering, or empty state you need to see. Generated fixtures are for load testing.
- Why the guard is a module with a test, not an inline
if— the check is the only thing standing between a paste and a data-loss incident, and an inline condition is the kind of thing a refactor moves. A named function with a test cannot quietly stop running.
Contract
These must be true of this repository. pnpm dev-env status reports each one that is not.
- No
package.jsonscript seeds a production database — no script key naming bothseedandprod, and no seed script that setsNODE_ENV=production. — A seed rewrites data and refuses to run outside development; anything production needs is a migration or a separate idempotent bootstrap, so a production seed entry point is a data-loss incident waiting for a paste.