Skip to main content
View as Markdown

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.

>_ terminal
sovrium seed

It 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:

code
my-app/
  app.yaml
  config/tables/companies.yaml
  config/tables/contacts.yaml
  seed/companies.yaml
  seed/contacts.yaml

seed/companies.yaml:

app.yaml
records:
  - key: northwind
    fields:
      name: Northwind Trading
      industry: Retail

seed/contacts.yaml:

app.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>:

app.yaml
company: '@companies.northwind'

A many-to-many field takes a list:

app.yaml
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:

app.yaml
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.
>_ terminal
sovrium seed --mode replace

if-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:

app.yaml
mergeOn: [email]
records:
  - key: priya
    fields:
      email: priya@northwind.example
      name: Priya Raman

Without 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:

app.yaml
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.
  • upsert on 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:

code
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:

app.yaml
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:

>_ terminal
sovrium seed --dry-run
code
[dry-run] companies: would create 5 records
[dry-run] contacts: would create 7 records
[dry-run] no changes written

To iterate on one file, restrict the run:

>_ terminal
sovrium seed --table contacts --mode replace

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

Built with Sovrium