You never write a migration file. You add a new schema version, register it, and Outer diffs it against the previous one.
Register versions
Pass each schema version to .schema() on the builder, in order:
new Outer({ db: pglite() })
.schema(v1_0)
.schema(v1_1) // each call adds a migration step and updates the DB type
.start();Each call also advances the type of context.db to InferDB<T>, so procedures registered after a .schema() call see the new columns.
To avoid restating every table when only one column changed, derive the next version with .extend():
const v1_1 = schema("1.1.0")
.extend(v1_0)
.table("post", (t) => ({ tags: t.text().nullable() }))
.build();Apply migrations
Prefer .start() when you self-host — it builds the server and applies every pending migration, and throws if anything fails:
const server = await new Outer({ db: pglite() }).schema(v1_0).schema(v1_1).start();Use .build() plus the migrator when you need the result object, or when migrations run outside the request path:
const server = new Outer({ db: pglite() }).schema(v1_0).schema(v1_1).build();
const { error, results } = await server.migrator.migrateToLatest();
if (error) throw error;Outer diffs consecutive schema versions with a custom SchemaMigrationProvider. Each schema("x.y.z") call becomes one Kysely migration keyed by its version string.
- Up creates new tables, adds new columns with their indexes, and drops removed columns.
- Down reverses all three.
Run this on startup when you self-host. On serverless, run it from a deploy-time script instead — see Deployment.
Zero-pad version segments past 9
Kysely applies migrations in lexicographic order of their version-string keys, which disagrees with numeric order once a segment reaches double digits: "1.10.0" sorts before "1.2.0".
migrateToLatest() detects the mismatch up front and returns an error telling you to zero-pad the segments, for example "1.02.00". It never applies migrations out of order silently. .start() surfaces the same error by throwing.
Changed columns are refused, not ignored
Editing a column in place — its type, nullability, default, uniqueness, primary key, foreign key, or index — produces no add and no drop, so the diff has nothing to emit.
Rather than migrate to nothing and leave your schema and database disagreeing, migrateToLatest() returns an error naming each offending table and column:
Schema version "2.0.0" changes existing columns, which Outer cannot migrate automatically:
thing
name: became nullableAdding and dropping columns is supported. Altering one is not. Either revert the edit, or write the ALTER yourself with context.db and keep the schema in sync.
Renames destroy data
A rename reads to the diff as a drop plus an add, which it performs happily — and that destroys the old column’s data. Copy the data across yourself before removing the old name.
Next steps
schema()— evolve versions with.extend().resource()— generate CRUD over the migrated tables- Deployment — when and where to run migrations