Migrations
An app's migrations/ 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.
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 changesVersions
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.
{ "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.
{ "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, 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:
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:
sluurp migrate ./app --plan
sluurp migrate ./appA 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:
sluurp migrate ./app --baseline V14All 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.