Skip to main content
As your application evolves, the shape of your configuration changes: fields get added, renamed, or removed, and nested structures get reorganized. Without a migration strategy, loading an older config file with a newer schema either silently discards data or throws validation errors. tauri-plugin-configurate solves this with built-in schema versioning and ordered migration steps that run automatically on load() — no extra code required at call sites.

Why migrations?

Every time you ship a new version of your app that changes the config schema — adding a required field, splitting one field into two, removing a deprecated key — any existing user config on disk is out of date. Migrations let you describe exactly how to transform version N data into version N+1 data. The plugin applies as many steps as needed in sequence, then saves the result back to disk so subsequent loads are already at the current version.

How it works

1

Set a version on your Configurate instance

Pass version: <number> to the Configurate constructor. This is the current schema version your code expects. The plugin writes this number to a reserved __configurate_version__ key inside every config it saves.
2

Define migration steps

Pass a migrations array of MigrationStep objects. Each step declares the version it upgrades from and an up function that receives data at that version and returns data at the next version.
3

Call load() as normal

On config.load(), the plugin reads the stored version number, compares it to the current version, and runs any needed migration steps in ascending order. You do not call migrations manually.
4

Migrated data is auto-saved

After running migrations, the plugin saves the updated data back to storage automatically so the next load starts at the current version without repeating the migration.

MigrationStep interface

interface MigrationStep<TData> {
  version: number;                    // The version this step upgrades FROM
  up: (data: TData) => TData;         // Transform function: returns data at version+1
}
Each up function receives the full config object at version and must return a new object valid at version + 1. You are responsible for returning a new object (or a mutation of the received one) — do not rely on the plugin merging partial results. If a required migration step is missing (for example, you have data at version 1 but no step with version: 1), the plugin throws an error at load time.

Full example

The following example tracks a config through three versions of a fictional app:
  • v0 → v1: Added a fontSize field that did not exist before.
  • v1 → v2: Replaced fontFamily (a free-form string) with a constrained displayMode enum.
import {
  BaseDirectory,
  Configurate,
  JsonProvider,
  defineConfig,
} from "tauri-plugin-configurate-api";

// Current schema — version 2
const schema = defineConfig({
  theme: String,
  language: String,
  displayMode: String,   // replaces fontFamily from v1
});

const config = new Configurate({
  schema,
  fileName: "app.json",
  baseDir: BaseDirectory.AppConfig,
  provider: JsonProvider(),
  version: 2,
  migrations: [
    {
      // Upgrade v0 → v1: add missing fontSize field
      version: 0,
      up: (data) => ({ ...data, fontSize: 14 }),
    },
    {
      // Upgrade v1 → v2: replace fontFamily with displayMode
      version: 1,
      up: (data) => {
        const { fontFamily, ...rest } = data as Record<string, unknown>;
        return { ...rest, displayMode: "system" };
      },
    },
  ],
});

// Migrations run automatically — no special call needed here
const { data } = await config.load().run();
When a user who has a v0 config on disk loads the app, the plugin runs the version: 0 step, then the version: 1 step, saves the result at version 2, and returns the migrated data — all transparently.

Migration rules

RuleDetail
Steps run in ascending version orderThe plugin sorts steps by version before applying them, regardless of the order you list them in the array.
Each step upgrades exactly one versionA step with version: 1 transforms v1 data into v2 data. There is no support for skipping versions.
All intermediate steps must be presentIf stored data is at v0 and current version is v3, steps for v0, v1, and v2 must all exist.
Migrated data is auto-savedAfter migration the updated data is written back to storage so future loads skip migration.
Already-current data is not re-savedIf __configurate_version__ in the stored data already equals version, no migration or extra write occurs.

The __configurate_version__ key

The plugin stores the current schema version in a reserved key named __configurate_version__ inside your config data. You will see it if you open the raw config file in a text editor. Do not modify or delete this key manually — the plugin uses it to determine whether a migration is needed and which step to start from. If the key is absent, the plugin treats the stored data as version 0.
You do not need to include __configurate_version__ in your schema definition; the plugin manages it transparently.

Handling a failed auto-save

After migrations run, the plugin attempts to write the migrated data back to disk. If that write fails (for example, because the file is read-only or disk space is full), the error is silently swallowed.
A failed auto-save after migration is non-fatal. The migrated data is still returned to your application and is fully usable for the current session. On the next load the migration will simply run again from the stored (un-migrated) version. To surface disk errors proactively, enable validateOnRead in your SchemaValidationOptions — it will not catch write errors directly, but your monitoring can detect repeated migrations as a signal of a persistent write failure.