`mandu db seed`
Replay deterministic fixture data against the live database. Declarative or imperative seed files, parameter-bound SQL with `quoteIdent()` injection defense, idempotent upsert + tamper detection.
On this page
mandu db seed
Replay deterministic fixture data against the live database. Seeds
live in spec/seeds/*.seed.ts and are versioned separately from
schema migrations (Phase 4c).
mandu db seed # default env=dev
mandu db seed --env=staging
mandu db seed --env=prod # requires MANDU_DB_SEED_PROD_CONFIRM=yes
mandu db seed --file=001_users # prefix filter
mandu db seed --dry-run # print INSERT previews, no execution
mandu db seed --reset # truncate tables before seeding
Seed file shapes
Two shapes are supported — pick whichever fits the use case.
Declarative — simplest path
// spec/seeds/001_admin.seed.ts
export default {
resource: "user",
key: "email", // upsert conflict column
env: ["dev", "staging"], // default = ["dev", "staging"]
data: [
{ email: "admin@example.com", name: "Admin" },
],
};
The runner resolves resource: "user" against your registered
resources, validates each row against the user schema, and issues an
upsert keyed by email. Running the same seed twice is idempotent —
no duplicates, no errors.
Imperative — when you need hashing, CSV, derived data
// spec/seeds/002_fixtures.seed.ts
import { hashPassword } from "@mandujs/core/auth";
export default async function seed(ctx) {
// Passwords: hash before insert — the declarative form can't do this
await ctx.upsert("user", [
{
email: "alice@example.com",
name: "Alice",
passwordHash: await hashPassword("alice-dev-pw"),
},
], { by: "email" });
// CSV import
const csv = await Bun.file("spec/seeds/data/products.csv").text();
const rows = csv.split("\n").slice(1).map((line) => {
const [name, price] = line.split(",");
return { name, price: Number(price) };
});
await ctx.upsert("product", rows, { by: "name" });
}
export const env = ["dev"] as const;
The ctx object gives you:
| Method | Purpose |
|---|---|
ctx.upsert(resource, rows, { by }) |
Validated upsert keyed by column |
ctx.sql |
Raw Bun.SQL instance (parameter-bound only) |
ctx.env |
The current --env value |
ctx.dryRun |
true when --dry-run — no writes happen |
ctx.log |
Structured logger |
Execution order
Seeds run in lexicographic filename order — prefix them with zero-padded integers:
spec/seeds/
├── 001_admin.seed.ts ← runs first
├── 002_fixtures.seed.ts
├── 003_products.seed.ts
└── 999_stress_data.seed.ts
Use numeric prefixes even when your seeds are independent — it makes the order predictable across developer machines.
Env gating
Every seed declares which environments it applies to:
// Declarative
export default {
resource: "user",
key: "email",
env: ["dev", "staging"], // skipped under --env=prod
data: [{ /* ... */ }],
};
// Imperative
export const env = ["dev"] as const;
Defaults: ["dev", "staging"]. A seed without an explicit env never
runs against prod.
To explicitly run against production:
MANDU_DB_SEED_PROD_CONFIRM=yes mandu db seed --env=prod
Without that env var, the runner exits with code 4 (refused). This
is the one destructive path mandu db insists you opt into verbally.
Safety model
| Layer | Mechanism |
|---|---|
| SQL injection | Every identifier (table/column) flows through quoteIdent(). Every value is parameter-bound via Bun.SQL. |
| Schema validation | Rows validated against the resource's Zod schema BEFORE any INSERT. Invalid rows abort the file with a clear error. |
| Transactional | Each seed file runs inside a transaction. Any failure rolls back the whole file — partial seeds are impossible. |
| Tamper detection | If a previously-applied file's bytes change, the runner refuses to replay until --reset. |
| Idempotent | Upserts keyed by { by } — running the same seed twice produces the same state. |
Dry-run output
$ mandu db seed --dry-run
[seed] spec/seeds/001_admin.seed.ts
resource: user
key: email
rows: 1
sql:
INSERT INTO "user" ("email", "name") VALUES ($1, $2)
ON CONFLICT ("email") DO UPDATE SET "name" = EXCLUDED."name"
params: ["admin@example.com", "Admin"]
[seed] spec/seeds/002_fixtures.seed.ts
imperative — skipped in --dry-run (would execute ctx.upsert and other calls)
Dry-run previews the exact parameter-bound SQL that would hit the database. Use it in CI gatekeeper jobs to catch malformed seeds before they reach prod.
Exit codes
| Code | Meaning |
|---|---|
0 |
success (or nothing to seed under current env) |
1 |
I/O, validation, or SQL error |
2 |
usage error |
3 |
tampered seeds (a previously-applied file changed) |
4 |
refused (--env=prod without confirmation) |
File naming
Seeds must be named *.seed.ts and live under spec/seeds/. Any
non-seed file in the directory is ignored. Subdirectories are allowed
for grouping:
spec/seeds/
├── dev/
│ └── 100_stress_data.seed.ts
├── 001_admin.seed.ts
└── 002_fixtures.seed.ts
The runner recursively scans spec/seeds/**/*.seed.ts.
Common errors
CLI_E120: seed resource 'foo' not registered — the declarative
resource field must match a registered resource name. Check
mandu routes list --json for the resource registry.
CLI_E121: row failed schema validation — an imperative seed
passed a row that doesn't match the resource's Zod schema. The error
includes the failing field.
CLI_E122: seed file tampered — previous hash mismatch — you
edited a seed file that was already applied. Two fixes:
- Revert the file to match the recorded hash (safer).
- Run
mandu db seed --resetto truncate tables and replay from scratch (destructive).
CLI_E123: --env=prod refused without MANDU_DB_SEED_PROD_CONFIRM=yes —
set the env var, or don't run against prod.
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/cli/db-seed to my project.
Summary of the page:
`mandu db seed` runs `spec/seeds/*.seed.ts` in lexicographic order. Declarative = `{ resource, key, data }`; imperative = `async function seed(ctx) { ... }`. SQL injection blocked via quoteIdent() + parameter binding. Prod deploys require MANDU_DB_SEED_PROD_CONFIRM=yes.
Required invariants — must hold after your changes:
- Every seed identifier flows through `quoteIdent()` — SQL injection blocked at the driver layer
- Every value is parameter-bound via `Bun.SQL` — never concatenated
- Rows validated against the resource's Zod schema BEFORE any INSERT
- Each seed file runs inside a transaction — any failure rolls the whole file back
- Tamper detection: if a previously-applied file's bytes change, the runner refuses to replay until `--reset`
- `--env=prod` requires `MANDU_DB_SEED_PROD_CONFIRM=yes` in the environment
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Related
- CLI —
upgrade— self-update the Mandu CLI. - CLI —
mcp register— wire Mandu MCP into Claude / Cursor / Continue / Aider. - Deploy — Docker Compose — seed
inside a compose stack via
docker compose exec app bunx mandu db seed.
For Agents
{
"schema": "mandu.db.seed/v0.25",
"command": "mandu db seed",
"seed_dir": "spec/seeds",
"seed_glob": "spec/seeds/**/*.seed.ts",
"execution_order": "lexicographic filename",
"shapes": {
"declarative": "{ resource, key, env?, data }",
"imperative": "async function seed(ctx)"
},
"prod_gate_env": "MANDU_DB_SEED_PROD_CONFIRM=yes",
"safety": [
"quoteIdent() for all identifiers",
"parameter-bound values via Bun.SQL",
"Zod schema validation before INSERT",
"per-file transaction — atomic rollback on failure",
"tamper detection via content hash"
],
"exit_codes": {
"0": "success",
"1": "I/O / validation / SQL error",
"2": "usage error",
"3": "tampered seeds",
"4": "refused (prod without confirmation)"
},
"rules": [
"Prefix seed filenames with zero-padded integers for predictable order",
"Never concatenate user input into raw SQL — use `ctx.sql` parameter binding",
"Use `--dry-run` in CI gatekeeper to preview SQL before prod"
]
}For Agents
`mandu db seed` runs `spec/seeds/*.seed.ts` in lexicographic order. Declarative = `{ resource, key, data }`; imperative = `async function seed(ctx) { ... }`. SQL injection blocked via quoteIdent() + parameter binding. Prod deploys require MANDU_DB_SEED_PROD_CONFIRM=yes.
- Every seed identifier flows through `quoteIdent()` — SQL injection blocked at the driver layer
- Every value is parameter-bound via `Bun.SQL` — never concatenated
- Rows validated against the resource's Zod schema BEFORE any INSERT
- Each seed file runs inside a transaction — any failure rolls the whole file back
- Tamper detection: if a previously-applied file's bytes change, the runner refuses to replay until `--reset`
- `--env=prod` requires `MANDU_DB_SEED_PROD_CONFIRM=yes` in the environment