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.
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.
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 successfullyThe 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.
| Category | For |
|---|---|
required | What every environment needs. Roles, baseline settings, the first invitation. |
dev | Demo content for a laptop or a preview deployment. |
test | Fixtures 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,devOrder
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
| Command | What it does |
|---|---|
questpie seed | Runs pending seeds. |
questpie seed:status | Prints what has run and what has not. |
questpie seed:undo | Runs undo handlers, then clears their tracking rows. |
questpie seed:reset | Clears 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.
Related
- 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.