QUESTPIE
Schema

Seeds

A seed is a file that writes app data once. The runner wraps it in a transaction, records that it ran, and skips it every time after that.

View markdown

A fresh database has your tables and none of the rows the app needs to work. This page starts from one file in seeds/ and ends with those rows written once, on every machine that runs the app.

The file

A seed lives under seeds/, next to collections/, and the file is the registration. Default-export one seed({...}) call per file.

src/questpie/server/seeds/site-settings.ts
import { seed } from "questpie/services";

export default seed({
	id: "siteSettings",
	description: "Write the default site settings",
	category: "required",
	async run({ globals, log }) {
		await globals.site_settings.update({
			siteName: "QUESTPIE",
			tagline: "Sharp cuts, every time",
		});

		log("Site settings written");
	},
});

id is what the runner records, so it has to be unique across your app and every module in it. category is required, description is not.

run receives the whole app context. The same collections, globals, db, services, queue, email, storage and kv a route or a job gets, plus log for a line of CLI output and createContext for a request context of your own.

questpie add seed site-settings scaffolds the file and runs codegen for you.

It runs as the system

The runner puts every seed in system access mode, which bypasses collection, global and field access rules, so bootstrap data can land before a user or a role exists. HTTP requests are untouched by this and still run in user mode.

Writing one locale at a time

createContext({ locale }) builds a request context you pass as the second argument to a CRUD call. A code that is not in your configured locales falls back to the mapping in locale.fallbacks, then to your default locale.

const sk = await createContext({ locale: "sk" });
await globals.site_settings.update({ tagline: "Ostré strihy, vždy" }, sk);

Running it

questpie generate   # register the new file in the typed app
questpie seed       # run everything that has not run yet
🌱 Found 1 seed(s)

🌱 Running 1 seed(s)...
  🌱 Running seed: siteSettings (Write the default site settings)
    Site settings written
  ✅ Seed completed: siteSettings
✅ All seeds completed successfully

The runner writes one row per finished seed into questpie_seeds, creating that table on first use, and skips anything it finds there. Run the command again and it prints ✅ No pending seeds.

What one run guarantees

A seed({...}) body runs inside a single transaction, and the tracking row is written in that same transaction. A seed that writes three rows and then throws leaves nothing behind and stays pending. The run stops there, so the seeds after it do not run either.

Write the body to be idempotent anyway, so a second entry changes nothing. --force, seed:reset, an undo and a fresh database all send the runner back through run. Look before you insert, or patch a singleton the way the example above does.

Categories

Every seed declares one of three.

CategoryFor
requiredWhat every environment needs. Roles, baseline settings, the first invitation.
devDemo content for a laptop or a preview deployment.
testFixtures an integration suite can count on.

The CLI filter is literal. --category dev does not pull required in the way autoSeed: "dev" does, and an unrecognised name is an error, not an empty run.

questpie seed --category required
questpie seed --category required,dev

Order

dependsOn lists the ids that have to run first. The runner sorts the selected seeds topologically before it runs any of them.

export default seed({
	id: "demoPosts",
	category: "dev",
	dependsOn: ["siteSettings"],
	async run({ collections }) {
		await collections.posts.create({ title: "Hello", slug: "hello" });
	},
});

A dependency outside your filter is pulled in regardless, so --only demoPosts runs siteSettings first. An id that matches no seed logs a warning and the run continues. A cycle throws Circular seed dependency detected at "…".

Taking it back out

undo is optional. questpie seed:undo runs each handler inside a transaction, then deletes the tracking row so the seed is pending again. It walks the registered seeds backwards, and codegen registers them alphabetically by filename, so that is not the reverse of the order they ran. dependsOn does not reorder an undo.

async undo({ collections }) {
	await collections.posts.deleteMany({ where: { slug: { eq: "hello" } } });
},

A seed with no undo handler is left alone and stays recorded as executed. seed:reset is the blunter tool: it clears tracking rows and touches no data, so the next questpie seed treats those seeds as pending.

When one seed is too big to restart

seed({...}) is all-or-nothing, which is the wrong shape for a seed that uploads two hundred files or calls an API you would rather not pay for twice. seed.steps({...}) trades the one transaction for a checkpoint per step.

Checkpointed seeds builds one and covers what resuming costs you.

The commands

CommandWhat it does
questpie seedRuns pending seeds.
questpie seed:statusPrints what has run and what has not.
questpie seed:undoRuns undo handlers, then clears their tracking rows.
questpie seed:resetClears tracking rows and step checkpoints. No data.
questpie seed:status
📊 Seed Status:

Executed: 1
Pending: 1

✅ Executed seeds:
  - siteSettings [required] (2026-08-01T09:12:44.010Z)

⏳ Pending seeds:
  - demoPosts [dev]

Running seeds has the options each command takes, the dry run, the config key that seeds on startup, and the same four operations from code.

  • Migrations for the other half of the split. A migration changes tables and columns, a seed writes rows.
  • Access control for the rules system mode steps over.
  • Modules for shipping seeds in a package. Seed arrays concatenate, which is why ids have to be unique.
  • Codegen for how seeds/ is discovered.

On this page