Skip to main content
View as Markdown

Migrating a Database

Sovrium normally migrates your database as part of starting the server. sovrium migrate separates the two: it brings the schema forward and exits, without starting a server or binding a port.

>_ terminal
sovrium migrate

That separation buys you two things. A platform can run migrations as a release phase instead of inside the web process. And when a deploy will not boot, you still have a route to its own database — the command constructs no application runtime, so it stays available on databases where sovrium start cannot complete.

All it needs is the connection: DATABASE_URL for PostgreSQL, or nothing at all for the embedded SQLite. A config path is accepted, and auto-discovered when omitted, exactly as start does it.

Three modes

Command Question it answers Exit
sovrium migrate [config] Bring this database forward. 0 applied, 1 failed
sovrium migrate --dry-run What would change? 0 unless refused
sovrium migrate --check Is this database safe to upgrade? 0 safe, 1 unsafe

--dry-run and --check both write nothing, and cannot be combined — they ask different questions, and running them together would blur which answer you got.

What it migrates

"Migration" names two independent systems in Sovrium, and this command owns both.

Shipped migrations Config tables
Source The migration files bundled with the binary tables in your configuration
Covers Authentication, system and internal tables Your own tables, views, indexes
Decided by The migration journal recorded in your database A checksum of your table definitions

The shipped migrations run first, always. A user field emits a real foreign key into the authentication tables, so your own tables cannot be created before the migrations that build them. Running only half would leave sovrium start doing schema work inside the web process — the coupling this command exists to break.

Applying migrations

>_ terminal
sovrium migrate app.yaml
code
  Dialect: sqlite
  Migrations: /srv/app/drizzle/sqlite

  ✓ Applied 14 pending migrations. The journal is at 14 of 14.
    0000_mute_cassandra_nova
    0001_famous_leper_queen

    0013_jazzy_karma

  ✓ Config tables reconciled: notes.

Each migration is named, and the journal position is reported as a fraction, so "applied nothing" and "applied fourteen" never look alike from outside.

The command is idempotent. Run it over a database that is already current and it applies nothing and exits 0:

code
  ✓ No pending migrations. The journal is at 14 of 14.

  ✓ Config tables reconciled: notes.

That is what makes it safe as a deploy hook: every deploy can call it, and only the ones with work to do will do any.

Previewing with --dry-run

>_ terminal
sovrium migrate app.yaml --dry-run

Names every pending migration and every statement it would run against your own tables, and writes nothing:

code
  ⚠ Dry run — nothing was written.

  Dialect: sqlite
  Migrations: /srv/app/drizzle/sqlite

  would apply 14 pending migration(s)
    0000_mute_cassandra_nova

    0013_jazzy_karma

  would create table notes
    CREATE TABLE IF NOT EXISTS notes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
  updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
  deleted_at TEXT,
  title TEXT
)

  Re-run without --dry-run to apply this plan.

A few changes rebuild a table and copy its rows across. The exact statements for those depend on the state of the table at the moment they run, so they cannot be rendered in advance. They are named and labelled as unsimulated rather than left out — a preview that quietly under-reports is worse than none, because you use it to decide whether the change needs a maintenance window.

Pre-flight with --check

>_ terminal
sovrium migrate app.yaml --check

Reports where the database stands — which engine, which migration folder, and how much of the journal it has applied — then gives a verdict:

code
  Dialect: sqlite
  Migrations: /srv/app/drizzle/sqlite
  Applied: 0 of 14
  Pending: 14

  Pending migrations:
    0000_mute_cassandra_nova

    0013_jazzy_karma

  ✓ Safe to migrate.

On a database already at the current schema the verdict reads No pending migrations. This database is at the current schema. — also exit 0.

Exit 1 means the upgrade would abort part-way through, for one of three reasons:

  • Duplicate account identities — two authentication rows that a later migration's uniqueness constraint cannot both keep.
  • Duplicate OAuth client ids — the same collision on the OAuth server's clients.
  • A rewritten released migration — a migration file whose stored checksum no longer matches the file this build ships.

Each is reported with the offending rows named. Nothing is repaired automatically: deleting one of two colliding authentication rows would sever somebody's login, so the command names them and stops, and you decide which one survives.

In a deploy pipeline

Because the command is idempotent and needs only the connection, it fits a platform's release phase:

>_ terminal
sovrium migrate app.yaml && sovrium start app.yaml

Splitting them keeps schema work out of the web process, and gives a failed migration its own exit code instead of a boot that dies with no explanation. Pair it with --check in CI, ahead of the deploy, to learn about a blocked upgrade before you take traffic down.

What it does not do

It does not roll back. Released migrations are forward-only and are never rewritten. Recovering from a bad upgrade means restoring a backup — see Upgrade & rollback.

It does not write any data. Migrations shape the schema; rows come from sovrium seed or from the app itself. It also skips the best-effort work that boot does after the schema is in place, so that a command named migrate has no side effects its name does not promise.

It does not replace a correct boot. sovrium start still migrates on its own. This command makes a broken upgrade recoverable and a deploy pipeline explicit; it is a route to the database, not a repair of the boot sequence.

Last updated September 1, 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