Load seed and test data into an environment from the same Zod schemas and codecs that define your tables and KV stores.
Synopsis
pithy seed [--worker <name>] [--env <name>] [--dry-run] [--redo] [--yes] \
[--confirm-production <phrase>] [--confirm-reset <phrase>] [--json]Flags
| Flag | Default | Purpose |
|---|---|---|
--worker <name> | every Worker | Seed one Worker instead of every Worker under apps/ |
--env <name> | dev | The environment to seed. dev runs locally against Miniflare; anything else runs against the live D1, KV, R2, Images and Stream for that environment |
--json | false | Machine-readable output — the full write plan or run report as one line. Implies non-interactive: pithy seed never prompts when it is set |
--dry-run | false | Compute and print the write plan without touching any backend. Reads media sidecars so it can report upload, skip or reupload accurately; mints nothing |
--redo | false | Destructive. Drop every table and recreate the schema before seeding. See Resetting data, below |
--confirm-reset <phrase> | — | Unlock a non-dev --redo: the exact phrase yes, i really want to reset <env> |
--yes | false | Confirm a non-dev environment. Required for staging and prod; dev never needs it |
--confirm-production <phrase> | — | The non-interactive unlock for prod. See The production exception, below |
What it does
Fixtures are authored with defineSeed — the peer of defineCapability — and composed library-before-app, exactly like the migrations pithy migrate runs. There is no separate fixture format and no hand-written SQL: the schemas that define your tables are the schemas that define your test data.
Seed test data is the authoring model. This page is the command.
Idempotency
Every run is safe to repeat. D1 rows insert with INSERT OR IGNORE, KV entries put by key, and a once media asset uploads on its first run only and skips on every run after. Re-running against an environment that already has the fixtures loaded writes nothing new and changes nothing existing.
This is also why editing a fixture’s values and re-running does nothing: the row already exists, so it is ignored, unchanged. --redo is the answer to that, and it is below.
Fixture size
Make a fixture as big as the thing it has to prove. A paged list needs more rows than a page, and the default page size is 25.
D1 accepts 100 bound parameters in one statement, and an insert binds one per column per row — so a seven-column table fits about fifteen rows per statement. pithy seed writes each group in chunks sized from that table’s own column count, so the limit is never a fixture’s problem and never a number to look up. A 500-row group lands the way a 3-row one does.
A group is not atomic across its chunks, and INSERT OR IGNORE is what makes that safe: a run that dies partway is re-run, the landed rows are ignored, and the rest go in.
The production exception
Every other flag in this CLI follows one rule everywhere: --json means non-interactive, full stop. pithy seed --env prod is the one place a flag additionally gates content rather than just interactivity — because seeding production is rare and should stay rare.
devnever asks for anything.staging, and any other non-dev, non-production environment, needs--yes.prodneeds--yesand the exact phraseyes, i really want to seed production, matched case-insensitively after trimming. Interactively you are prompted for it. Non-interactively there is no prompt — pass it:
pithy seed --env prod --yes --confirm-production "yes, i really want to seed production"Get it wrong, or omit it headlessly, and the run is refused before anything opens:
$ pithy seed --env prod --yes --json
The production confirmation phrase did not match.
Pass --confirm-production "yes, i really want to seed production" to seed production.The flag and the phrase keep the word production deliberately. The environment is named prod, and --env production is refused — but a confirmation phrase is read by a person about to overwrite live data, and yes, i really want to seed prod is a sentence you can type without meaning it.
Underneath both gates is a third, structural one that no flag can bypass: a seed set is only ever composed for prod if it lists prod in its own environments array.
Resetting data with --redo
--redo is for the moment you edited a fixture’s values and want them to actually land. It is not a per-row refresh. It is a full schema reset:
Seeded data is local by default, which is what makes it safe to throw away and rebuild while pithy dev is running.
- Roll every migration back — every
down, not just the latest, in reverse order. - Reapply every migration’s
up, recreating the schema empty. - Seed as normal.
Because every table the migration registry owns comes back empty, step 3’s ordinary non-destructive writes just work. There is also nothing left of what was there before: --redo destroys every row in every table the registry owns, not just the rows a fixture wrote. Data you inserted by hand, in a seeded table, does not survive.
That is the sharp edge. Reach for --redo only when a clean rebuild is what you actually want.
A real reset opens with the banner that names what it just did, then a line per database:
$ pithy seed --env dev --redo
DESTRUCTIVE. Every table in dev was dropped and recreated.
Reset app (DB): 1 migration rolled back and reapplied.
api 0200_leaderboard_demo_board: 12 rows.
Done.--redo carries its own, stricter gate. --yes means yes, this is not dev; it was designed to authorize writing seed rows, which is additive and harmless. A reset drops every table first. Letting one flag authorize both would mean a script — or a hand — that knew only to pass --yes could destroy an environment’s entire dataset.
| Environment | Plain seed | --redo |
|---|---|---|
dev | Free | Free — a local Miniflare store is what reset is for |
staging, a feature environment, anything non-dev | --yes | The exact phrase yes, i really want to reset <env> |
prod, and any name in seed.productionEnvironments | --yes plus the seed confirm phrase | The reset phrase, and it is refused headlessly without it |
The phrase names its environment, so one authorizing a staging reset cannot be pasted into a command targeting another. Automation is preserved — CI passes the flag explicitly — and a reset simply cannot happen by accident.
A non-dev reset is audited: a seed/schema_reset event at critical severity, naming the environment and the databases involved. The outcome is always truthful — recorded as success once the reset completes, or as failure, with the command still failing, if it dies partway. A dev reset records nothing, because auditing covers actions that reach a remote system and a dev run only touches the local store.
--json
One line, the same shape for a plan and for a report, with dryRun telling you which.
$ pithy seed --env dev --dry-run --json
{"command":"seed","env":"dev","dryRun":true,"workers":[{"worker":"api","sets":[{"name":"0200_leaderboard_demo_board","d1":[{"database":"app","table":"boardEntries","rows":12}],"kv":[],"r2":[],"media":[]}],"skippedByEnv":[],"shared":[]}],"devSecrets":null}| Key | Type | Meaning |
|---|---|---|
command | string | "seed" |
env | string | The environment seeded |
dryRun | boolean | Whether the plan was computed and nothing written |
workers | object[] | The per-Worker outcome, in fan-out order |
workers[].worker | string | The Worker’s name |
workers[].sets | object[] | The per-set outcome, in run order, each named by its composed key and carrying its d1, kv, r2 and media writes |
workers[].skippedByEnv | string[] | The sets the registry carries that this environment disallows — surfaced, never silently dropped |
workers[].shared | string[] | The sets an earlier Worker in the fan-out already wrote to the same store. Two Workers sharing a binding share one store, so the fixture runs once and the second says so rather than double-counting |
reset | object[] | Present only with --redo: one entry per physical database whose schema was, or would be, reset |
devSecrets | object or null | What the dev-secrets pass wrote, and which Workers’ .dev.vars it refused. null on a dry run and outside dev, where nothing is written |
Errors
Every refusal here is a gate rather than a fault, and each one names the flag that opens it.
| Condition | Effect |
|---|---|
--env production | Refused at the flag. The environment is prod |
A non-dev environment without --yes | Refused before anything opens |
prod without the confirmation phrase, non-interactively | Refused before anything opens |
A non-dev --redo without --confirm-reset "yes, i really want to reset <env>" | Refused. The phrase names its environment |
A Worker that was supposed to get a generated .dev.vars and did not | The fixtures still run — they are the rest of the run and worth doing — and the exit code is 1. The refusal names the file and offers .dev.vars.local |
Examples
A normal run reports one line per seed set, then Done. Every line is prefixed with the Worker that ran it, because the run fans out over apps/* and a fan-out should read as one list rather than several interleaved ones.
$ pithy seed --env dev
api 0100_auth_test_users: 3 rows, 1 entry.
api 0200_leaderboard_demo_board: 12 rows.
Done.A set with nothing to write still gets a line, so a quiet run is never mistaken for a skipped one:
$ pithy seed --env dev
api 0300_media_avatar: nothing to seed.
Done.A set present in the registry but not allowed for the target environment is reported above the sets that ran, never silently dropped:
$ pithy seed --env dev
api skipped 0210_leaderboard_prod_smoke: not allowed in dev.
api 0200_leaderboard_demo_board: 12 rows.
Done.--dry-run prints the same per-set shape and adds a plain reminder before Done.:
$ pithy seed --env staging --dry-run
api 0200_leaderboard_demo_board: 12 rows.
Dry run. Nothing written.
Done.