api.seed in the recipe is the authenticated one-shot that mints them: one command you write (or let guard setup draft), run once per run, whose output the whole run reuses:
command: one shell command, run in the repo root.provides: the static declaration of what the seed promises to emit. Credentials each carry aheader, an optionaldescriptionnaming the principal (“org owner”, “regular member”) so authoring picks the right one for a role-sensitive claim, and an optionalsatisfiesnaming the OpenAPI security scheme they fulfill. Fixtures arename → [field, …]. No values: no secret ever reachesrecipe.jsonor the recipe fingerprint. Changingprovidesdoes re-key authoring, since it changes what scenarios can be written against.- A credential name may not be declared in both
api.credentialsandapi.seed.provides.credentials; one name has exactly one source.
The manifest
The runner setsGUARD_SEED_OUT to a temp file path; the command writes its results there as JSON:
value, and every declared fixture field must be present. A gap is a hard seed-failed stop that names what’s missing, never a silent skip. Fixture values keep their native JSON type; a manifest number stays a number.
When it runs: once per run, in the repo root, only when the run has api scenarios: after api.services.up (so migrations and the datastore are ready) and before any server boots. It runs with the server’s environment, so a DATABASE_URL you declared for the server reaches the seed too.
Using it in scenarios
{{cred:owner}}: seeded credentials merge into the same pool as static ones. Credentials resolve in header values only, never in a path, body, or expectation.{{fixture:org.id}}: fixtures are ids and handles, not secrets, so they resolve anywhere: path, query string, header value, request body, and expectation matchers. When a JSON leaf is exactly one placeholder it substitutes the native value ({"orgId": "{{fixture:org.id}}"}sends the number42).- Referencing a fixture or field the seed never provided is a scenario error, not a silent empty string.
«cred:<name>». Fixtures are deliberately not redacted: they’re the ids you want to read in a transcript.
What survives, and what doesn’t
Guard boots one fresh server per scenario. Seeded state therefore survives only when it lives in an external datastore brought up byapi.services.up: a Postgres, a Redis, anything outside the process. If your app keeps state in memory, either give it a real store for guard runs (via api.env) or have each scenario create what it needs through the API itself.
Let guard draft it
truecourse guard setup drafts the seed script for you, but only when it can be honest about it: a database whose schema it actually parsed, a recipe with an api block, and no api.seed already (an existing seed is yours; --refresh replaces it, and asks first). The draft is grounded in your repo: the parsed tables and foreign-key graph, the ORM your files import, the connection env var your server reads, your HTTP route surface, your OpenAPI security schemes, and excerpts of the specs themselves. Neither artifact is written until the engine has proved them by running the script for real and booting the server against the state it left behind. Review and commit both artifacts; the script is real code that writes to your datastore, and reviewing it is the point.
A drafted seed also records api.seed.script, the script file path. It’s optional and the runner ignores it; its one job is staleness: the recipe fingerprint hashes that file’s content, so editing the seed re-authors the flows written against the rows it creates. Add it to a hand-written seed for the same guarantee.
Next steps
External services
Handle the third parties your app calls: fakes, stubs, or real accounts.
Guard generate
Author scenarios against the seeded world.