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

```bash
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

```bash
sovrium migrate app.yaml
```

```text
  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`:

```text
  ✓ 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`

```bash
sovrium migrate app.yaml --dry-run
```

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

```text
  ⚠ 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`

```bash
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:

```text
  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.

:::callout
**`--check` is a pre-flight, not a guarantee.** It reports the conditions it can prove would
block the upgrade. A clean report means none of those were found — not that the migration
will succeed.
:::

## In a deploy pipeline

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

```bash
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](/en/docs/upgrade-rollback).

**It does not write any data.** Migrations shape the schema; rows come from
[`sovrium seed`](/en/docs/cli-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.

## Related Pages

- [Schema Migrations](/en/docs/migrations) — how schema evolution works when the server boots.
- [Upgrade & rollback](/en/docs/upgrade-rollback) — backing up before a version change, and restoring after one.
- [Seeding Data](/en/docs/cli-seed) — filling the schema this command creates.
- [Database Infrastructure](/en/docs/database-infrastructure) — SQLite by default, PostgreSQL via `DATABASE_URL`.
- [CLI Overview](/en/docs/cli) — config resolution and the full command surface.
