Upgrading the engine
How to move a project to a new engine version, covering engine.version, the template contract and check --contract-report, migrations as an explicit deploy step, and the order to run things in.
Upgrading Meridian means running a new binary (or image) against your project and database. Your project files do not change unless the release notes say so. Three things tie a project to an engine: the engine major version it targets, the template contract its templates are written against, and the database migrations the engine has applied.
What ties a project to an engine
| Where it is | What checks it | |
|---|---|---|
| The engine major version | engine.version in config/engine.yaml, such as "1.x" | engine-dmo check and every boot |
| The template contract | template.contract in config/engine.yaml, 1 when unset | engine-dmo check and every boot |
| The engine’s own tables | The database, recorded per migration | engine-dmo migrate status, every boot, the readiness check |
engine-dmo version prints the binary’s version and the template contract it implements:
meridian v0.8.0 (vips) go1.27.3
...
template contract 1 engine.version
engine:
version: "1.x" engine.version names the engine major version the project is written for, as a major number followed by .x. Any other form is an error. A release binary from 1.0.0 on also refuses a project whose major differs from its own, with the error at this line and a hint to run the matching engine or upgrade the project and change the value. Releases before 1.0.0 check only the form, so on a 0.x engine the starter’s "1.x" is accepted and the value protects nothing yet; it starts to matter at 1.0.0.
The template contract
The template contract is the versioned surface your templates use: the context, the globals, the functions and the filters. template.contract in engine.yaml selects it; it is 1 when unset, and 1 is the only contract this engine supports, so any other value is an error:
config/engine.yaml:23:13: template.contract 2 is not supported by this engine; supported: 1 Before an engine supports a new contract, anything the next contract removes is reported as deprecated. List every such use in your templates, each with the form to write instead:
engine-dmo check --no-db --contract-report contract report: nothing the next contract version removes Work through the list before you raise template.contract, then run engine-dmo check again.
Migrations are a deploy step
A new engine version usually brings migrations: changes to the engine’s own tables, separate from your schema. They are numbered per stream (core, and each module the binary compiles), they only move forward, and they run when you say so:
engine-dmo migrate status # applied and pending; exits 2 if any are pending
engine-dmo migrate up # apply every pending migration
engine-dmo migrate to 12 # apply up to and including core version 12
engine-dmo migrate to crm/3 # or up to a module's version 3 By default the server refuses to start while a migration is pending. Two settings change that, and neither is recommended in production because they hide the migration inside a restart:
| Setting | Effect |
|---|---|
engine-dmo serve --migrate | Apply pending migrations, then serve, for this run |
migrate.on_boot: apply in engine.yaml | Apply pending migrations on every boot. The default is refuse |
migrate up refuses a database whose encoding is not UTF8, and refuses to continue if an applied migration’s file has changed since it ran. Migrations have no down step: to go back after one, restore the database backup you took before it.
engine-dmo health --readiness exits 0 only when startup is complete, the database answers and every migration the binary knows is applied, so a load balancer or deploy step never sends traffic to a process whose tables are not ready. --wait 60s keeps trying for that long.
Schema drift at boot
At boot the engine also compares your configuration with the schema in the database. What it does with a difference is schema.on_drift:
| Value | Effect |
|---|---|
refuse | Refuse to start and print the plan. The default |
apply | Apply the plan at boot; a destructive plan still needs its approval file |
warn | Log the difference and start without applying it. Refused in production |
Run engine-dmo schema apply as its own step, after the migrations, so a schema change is planned and reviewed like any other.
Upgrade a project
The order matters: the new binary checks the project before anything changes, the backup comes before the migrations, and the new version serves only once its tables and schema are ready.
flowchart LR notes["Read the<br/>release notes"] --> check["check --no-db<br/>with the new binary"] check --> backup["Back up the<br/>database and media"] backup --> migrate["migrate up"] migrate --> schema["schema plan<br/>and apply"] schema --> start["Start serve and wait<br/>for health --readiness"] start --> refresh["Refresh editor schemas,<br/>AGENTS.md and skills"]
Read the release notes
They list the migrations the release brings and anything you must change.
Check the project against the new binary
engine-dmo version
engine-dmo check --no-db --contract-report Fix every error before going further. Run it in CI with the new image, so a project that does not pass never reaches a server.
Back up the database
Take a backup you have restored before. Migrations cannot be undone any other way. The engine has no backup command: back up the database with pg_dump and, with the filesystem storage driver, copy the storage directory beside it (Backups and restore).
Migrate, then apply the schema
engine-dmo migrate up
engine-dmo schema plan
engine-dmo schema apply Run these with the new binary, as one-off steps, before the new version starts serving.
Start the new version
Start engine-dmo serve with the new binary and wait for engine-dmo health --readiness.
Refresh the generated files
engine-dmo config schema --write # the editor schemas in .meridian/schema
engine-dmo guide --out AGENTS.md # the project guide, which names the engine version
engine-dmo docs skill --out .claude/skills/meridian/SKILL.md The meridian skill is the handbook’s agents page from the binary that wrote it, so an agent reading it follows the engine you now run. Any engine-dmo dev verb also rewrites the agent files a project has.
In a container deployment, run the migration and schema steps as one-off containers of the new image before the engine container starts. Single-node deployment has a Compose file that does exactly this, Upgrades covers rolling upgrades and rolling back, and Backups and restore covers backups and restores.
Extensions
An extension is a Go plugin, and it opens only in an engine built with the same Go toolchain, platform, build flags and meridian-extend version; the engine refuses a mismatch and names it. engine-dmo version prints what the engine was built with on its extensions: line. Rebuild your own extensions for each engine upgrade; the first-party extensions ship inside the -vips image and always match it. See Extending.
The development loop
The commands for day-to-day work on a Meridian project (dev up, serve --dev, the dev authoring verbs, check, schema plan and apply, migrate, seed, describe, render, template tests, guide and docs serve), what each needs and what it exits with.
Concepts
The ideas every Meridian project rests on (sites, locales, routes, documents and publishing, modules and bundles, stable ids, users and permissions) and how they fit together.