Seeding Data
A freshly installed app has a schema and no rows. sovrium seed fills it from files
you keep beside your config, so a new checkout opens on a working app instead of an
empty grid.
sovrium seedIt reads seed/<table>.yaml, resolves the links between records, expands any date
tokens, and writes through the same code path the REST API uses. No server needs to
be running.
The seed directory
One file per table, named after the table:
my-app/
app.yaml
config/tables/companies.yaml
config/tables/contacts.yaml
seed/companies.yaml
seed/contacts.yamlseed/companies.yaml:
records:
- key: northwind
fields:
name: Northwind Trading
industry: Retailseed/contacts.yaml:
records:
- key: priya
fields:
name: Priya Raman
email: priya@northwind.example
company: '@companies.northwind'
last_contacted: '{{today-3d}}'Tables are written parents-first. You do not have to order the files yourself — the command reads your relationship fields and works out the order. A genuine cycle is refused by name rather than guessed at.
Linking records with key
key names a record inside your seed files. It is never written to a column and never
appears in your database — it exists so one record can point at another before either
one has an id.
Reference it from another file as @<table>.<key>:
company: '@companies.northwind'A many-to-many field takes a list:
tags: ['@tags.urgent', '@tags.renewal']For a one-to-many relationship the foreign key lives on the child, so you seed the children pointing at the parent — not the parent listing its children.
Dates that stay current
A fixed date ages. A pipeline whose deals all closed last spring reads as abandoned by autumn, and a calendar seeded with fixed days is empty the moment you look at a different month.
Write dates relative to the day the seed runs:
close_date: '{{today+21d}}'
last_contacted: '{{today-3d}}'
due: '{{today}}'Every replay recomputes them, so a demo reset nightly always shows work in progress.
Modes
| Mode | Behaviour |
|---|---|
if-empty |
Default. Seeds a table only when it has no rows. Safe to run repeatedly. |
upsert |
Matches existing rows on a natural key and updates them; creates the rest. |
replace |
Deletes the table's rows, then inserts. For demo environments that reset. |
sovrium seed --mode replaceif-empty counts soft-deleted rows as present, so a table you emptied through the app
is not silently refilled underneath you.
Choosing what upsert matches on
upsert needs to know which column identifies an existing row. Declare it at the top
of the file:
mergeOn: [email]
records:
- key: priya
fields:
email: priya@northwind.example
name: Priya RamanWithout mergeOn, the command uses the table's single unique field. If the table has
none, or more than one, it stops and asks — matching on the wrong column would
overwrite unrelated records, so it will not guess.
Note mergeOn and key are different things. key links records inside your files;
mergeOn names real columns in your database.
Options
| Option | Meaning |
|---|---|
[config] |
Config file. Defaults to ./app.yaml. |
--dir <path> |
Seed directory. Defaults to a seed folder beside the config file. An explicit path is resolved against the working directory. |
--mode <mode> |
if-empty, upsert, or replace. Defaults to if-empty. |
--table <name> |
Seed only this table. Repeat for several. |
--dry-run |
Report what would be written and write nothing. |
The directory default follows the config file rather than your shell, so the same command behaves identically whether you run it from the project root or from a service unit with a different working directory.
Attachments
Put files in seed/assets/ and reference them by name:
logo: '@asset:northwind-logo.avif'The file is uploaded and the stored key is written to the field.
What seeding does not do
It does not run your automations. Records are written directly, so an automation that reacts to record creation will not have fired. If your app's demo value depends on something an automation produces — an activity log, a derived status — seed that too, written the way the automation would have written it.
Some fields are refused rather than half-written, each with a message naming the file and record:
- A relationship pointing at its own table needs two passes and is not supported yet.
- An attachment field on a bucket other than
default. upserton a table whose seed data carries many-to-many links.
Refusing is deliberate. Writing the row and dropping the links would leave you with data that looks complete and is not.
When a record is rejected
The message names the file, the record, the field, the value, and the reason:
companies.yaml (key "northwind"): field "size" — 201 is not one of the declared
options ('1-10', '11-50', '51-200', '201-1000', '1000+') — the submitted value is a
number; quote it in YAML to keep it a string [CHECK constraint failed: check_size_enum]Quote select values that start with a digit
That example is the most common surprise. YAML reads a scalar beginning with a digit as a number, and a trailing range or suffix does not stop it:
| You write | YAML gives you |
|---|---|
size: 201-1000 |
201 |
size: 1000+ |
1000 |
code: 07 |
7 |
qty: 1e3 |
1000 |
rate: 2.0 |
2 |
Quote them, and the value survives:
size: '201-1000'yes, no, on and off are read as strings, so they need no quoting.
Working on the data
--dry-run reports the plan without touching the database:
sovrium seed --dry-run[dry-run] companies: would create 5 records
[dry-run] contacts: would create 7 records
[dry-run] no changes writtenTo iterate on one file, restrict the run:
sovrium seed --table contacts --mode replaceLast updated August 11, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.