LangENKO

`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.

since v0.25
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 --reset to 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

🤖 Agent Prompt — `mandu db seed`
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.

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

AI hint

`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.

Invariants
  • 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
Guard scope
db-seed