---
title: Migrations
description: An app's data model and its data, versioned and run in order, as Flyway runs them.
section: Data
order: 13
---

# Migrations

<p class="lead">An app's <code>migrations/</code> folder holds versioned changes to its schema and data, one file per version, Flyway-style. Each runs once, in order, at startup: new installs get all of them, existing ones only what's pending.</p>

```text title="migrations/"
V1__init.json          # collections, same format as schema.json
V2__sample_data.json   # records (omit the file for no sample data)
V3__school_year.js     # generate or transform data in JavaScript
V4__trips_price.json   # schema change, as a patch
V5__tidy.sql           # plain SQL
R__views.sql           # repeatable: re-runs whenever it changes
```

## Versions

Files are named `V<version>__<description>`: `V1`, `V2`, `V2_1` (between `V2` and `V3`). Versions compare numerically, with `_` or `.` as separators and leading zeros ignored: `V2_1` = `V2.01`, and `V2_2` sorts before `V2_10`. Applied migrations are recorded with a checksum. **Never edit a migration that already ran**: the server stops before running anything after it and names the file. Add a new version instead. `R__` files run after versioned ones, and again whenever their contents change.

`migrations/` goes in the app folder, or next to it to share it between apps in one repository (e.g. an app and its `reports/`); it then runs once for all of them.

## Kinds of file

**`.json`** is an import document, same format as `schema.json`: `collections`, then `records`. Records are inserted in dependency order, so referenced rows exist first.

It can also hold non-collection data: `settings`, `conversations` with messages, `pages` with blocks and rows, social `feeds` and their `posts`. A post needs `id`, `feed`, `author` and `body`, and can have `reply_to` (a reply, in the thread's feed), `created`, `likes` (user ids) and `pinned`. Posts are inserted through the same path as in-app posts, so counts, `#tags` and search stay consistent. Existing pages are left untouched unless the document sets `"merge": true`, which updates folder settings, properties, template, navigation, sign-ups and look while keeping content and rows.

```json title="migrations/V5__news.json"
{ "posts": [
    { "id": "welcome", "feed": "school", "author": "principal0001", "body": "Welcome back! #backtoschool", "pinned": true },
    { "id": "welcome-1", "reply_to": "welcome", "author": "parent0000001", "body": "Thank you!" }
] }
```

**A patch** changes the schema by name. It's a JSON merge patch: objects add or modify, `null` deletes (along with its data). It's the only migration type that removes anything, and only what it names.

```json title="migrations/V4__trips_price.json"
{ "patch": {
    "trips": { "fields": { "price": { "type": "number" }, "old_note": null } },
    "invoices": null
} }
```

A file can contain both: the patch runs first, then collections, then records, so schema and data for one version travel together.

**`.js` or `.ts`** files export a function that runs like a [job](/docs/jobs), e.g. `ctx.collection("trips").update(…)`. It can also return a document (`{ records: … }` or a `patch`), imported like a `.json` migration; that's the way to generate lots of data:

```js title="migrations/V3__school_year.js"
export default function () {
  const grades = pupils.flatMap((p) => marksFor(p));
  return { records: { grades } };
}
```

**`.sql`** runs as a single script in one transaction.

## Running them

Migrations run when `sluurp serve` starts, before the app's agents. To preview or run them without serving:

```sh title="Terminal"
sluurp migrate ./app --plan
sluurp migrate ./app
```

A database that already has what some of the files make, because it was set up by hand or before migrations existed, can say so. `--baseline` records every file up to that version as run, without running it. Later files then run as usual:

```sh title="Terminal"
sluurp migrate ./app --baseline V14
```

## All or nothing

Before running pending migrations, Sluurp snapshots the database to `snapshots/<timestamp>-migration/`. If any migration fails, the snapshot is restored (schema, data and migration history) and the error names the file.

Nothing is left half-applied: all writes from the failing file and from earlier migrations in the same run are rolled back. An app gets all of its pending migrations or none. Fix the file and restart.

The snapshot is kept either way as a pre-upgrade backup, listed under **Backups** in the admin UI.

Apps with only a `schema.json` work as before: on each start collections are added or changed, never removed.
