---
title: "Migrations"
description: "Register schema versions with .schema(), apply them with .start() or migrateToLatest(), and understand what the diff refuses to do."
---

> Documentation Index
> Fetch the complete documentation index at: https://outer.now/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrations

You never write a migration file. You add a new [schema version](/schema/defining-schema), register it, and Outer diffs it against the previous one.

## Register versions

Pass each schema version to `.schema()` on the builder, in order:

```ts
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()`](/schema/defining-schema#extend-a-previous-version):

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

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

```ts
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](/outer/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 nullable
```

Adding 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()`](/schema/defining-schema) — evolve versions with `.extend()`
- [`.resource()`](/outer/resource) — generate CRUD over the migrated tables
- [Deployment](/outer/deployment) — when and where to run migrations

Source: https://outer.now/schema/migrations/index.mdx
