Menu

Migrations

Your site's SQLite database is shaped only by migrations: SQL files shipped in the release under private/migrations/. Handler code cannot create or alter tables (tenant SQL is data-only, see Context). Migrations also feed the schema snapshot that generates the table objects of the Database SDK.

Files

private/migrations/
  0001_create_posts.sql
  0002_add_post_status.sql
-- 0001_create_posts.sql
CREATE TABLE posts (
  id INTEGER PRIMARY KEY,
  slug TEXT NOT NULL UNIQUE,
  title TEXT NOT NULL,
  status TEXT NOT NULL CHECK (status IN ('draft', 'published')),
  published_at TEXT
);
CREATE INDEX posts_status ON posts(status);

Seed data belongs in a migration too (an INSERT in a migration runs once), not in request-time "bootstrap" code.

When migrations run

Two separate steps happen on every deploy that includes migrations:

  1. Validation at upload. After the manifest is resolved, the server applies all migrations in order, in one transaction, to a fresh in-memory SQLite database. Any SQL error rejects the upload (invalid migrations: <file name>) and nothing is created. This is also how the schema snapshot is built.
  2. The live migration, during the deploy. After the release record is created and before it becomes ready, the pending migrations (numbers above the site database's current version) are applied to the site's real database. This happens at deploy time, whether or not you activate: a staged release (activate=false) has already changed the live database when the deploy call returns. Activation only switches which release serves requests.

Status: documentation conflict. The design documents and the blog README say migrations run when a release is activated. The deploy code (DeploymentService::deploy_report) runs them during the deploy, before the release is ready, and activation does not run migrations. These pages follow the code. Consequence: the release that is currently active keeps serving against the migrated schema until you activate the new one, so write migrations that the old code can still run against (add columns and tables first, remove or rename later).

How the live migration runs (crates/microhost-cli/src/serve_migrations.rs):

What a migration may contain: CREATE, ALTER and DROP of tables and indexes, data statements (INSERT, UPDATE, DELETE), and the same function allowlist as tenant SQL. The migration authorizer also permits triggers and views. PRAGMA, ATTACH, ANALYZE, virtual tables, temporary objects and tables named _mh_* are refused. Table objects and the schema snapshot only cover tables.

Because the snapshot is built with plain SQLite, SQL that the snapshot accepts can still be refused by the live migration (for example a PRAGMA); in that case the deploy fails at step 2 with a migration error.

The schema snapshot

At upload the server reads the migrated in-memory database and seals db-schema.json into the release (its SHA-256 is recorded in resolved-manifest.meta.json; the file name is reserved). Contents, with formatVersion: 1:

microhost/db generates its table objects from this snapshot, once per release, and caches by release checksum. Consequences:

An administrator can inspect the live schema (GET /admin/api/v1/sites/{id}/database/schema) and run queries in the site database console.

Rollback

Activating an older ready release is a rollback. It is refused unless the database's current version is inside that older release's schemaCompat. Migrations are never undone, so a rollback after a destructive migration runs old code against the new schema. The pre-migration backup files stay in the site's private data/ directory for operator recovery.