Skip to main content
Applications change over time — new settings are added, old ones are renamed or removed. Without a migration strategy, updating your schema can corrupt or lose data in config files written by an older version of your app. tauri-plugin-configurate provides a built-in versioning and migration system that runs automatically on load().

How schema versioning works

When you set a version number on the Configurate constructor, the plugin stores a __configurate_version__ key alongside your data on every write. On load, it compares the stored version to the current version. If the stored version is lower, the plugin runs the applicable migration steps in order, then auto-saves the migrated data back to storage. The process is entirely automatic — you define the steps; the plugin handles when and in what order they run.

The version field

Pass version (a non-negative integer) to the Configurate constructor:
const config = new Configurate({
  schema: appSchema,
  fileName: "app.json",
  baseDir: BaseDirectory.AppConfig,
  provider: JsonProvider(),
  version: 2, // current schema version
});
Every subsequent create, save, or patch call writes __configurate_version__: 2 into the stored file. On load, the plugin reads this value and uses it to determine which migrations, if any, need to run.
Files written before you added version to the constructor are treated as version 0. This means your first set of migrations should use version: 0 as the “from” version.

MigrationStep

A MigrationStep describes a single schema transform:
interface MigrationStep<TData> {
  version: number;              // The version this migration upgrades FROM
  up: (data: TData) => TData;   // Transform function — receives old data, returns new data
}
Pass an array of steps to the migrations option. Steps are applied in ascending order of version whenever the loaded data’s stored version is less than the current version.

Auto-save after migration

After running migrations, the plugin automatically saves the migrated data back to storage. This means the next load() will find already-migrated data and skip the migration steps entirely. If the auto-save fails (for example, due to a permissions error), the failure is logged as a warning but does not surface to the caller — the migrated data is still returned for the current session.

Step-by-step example

Starting state

Your app is at version 0 with the following schema:
// Version 0 schema
const schemaV0 = defineConfig({
  theme: String,
  fontSize: Number,
});

const config = new Configurate({
  schema: schemaV0,
  fileName: "app.json",
  baseDir: BaseDirectory.AppConfig,
  provider: JsonProvider(),
  version: 0,
});
Stored file (app.json):
{ "theme": "dark", "fontSize": 14 }

Migration 1: adding a new field

In version 1, you add a language field. Existing files won’t have it, so the migration provides a default value.
const schemaV1 = defineConfig({
  theme: String,
  fontSize: Number,
  language: String, // new field
});

const config = new Configurate({
  schema: schemaV1,
  fileName: "app.json",
  baseDir: BaseDirectory.AppConfig,
  provider: JsonProvider(),
  version: 1,
  migrations: [
    {
      version: 0, // applies to data stored at version 0
      up: (data) => ({ ...data, language: "en" }),
    },
  ],
});
When a user loads app.json with __configurate_version__: 0 (or no version key), the plugin runs the version: 0 step, producing:
{ "theme": "dark", "fontSize": 14, "language": "en", "__configurate_version__": 1 }
This is then auto-saved back to disk.

Migration 2: renaming a field

In version 2, you rename fontSize to textSize. The migration copies the old value and removes the old key.
const schemaV2 = defineConfig({
  theme: String,
  textSize: Number, // renamed from fontSize
  language: String,
});

const config = new Configurate({
  schema: schemaV2,
  fileName: "app.json",
  baseDir: BaseDirectory.AppConfig,
  provider: JsonProvider(),
  version: 2,
  migrations: [
    {
      version: 0,
      up: (data) => ({ ...data, language: "en" }),
    },
    {
      version: 1,
      up: (data) => {
        const { fontSize, ...rest } = data as typeof data & { fontSize?: number };
        return { ...rest, textSize: fontSize ?? 14 };
      },
    },
  ],
});
A file at version 0 would run both steps in sequence (0 → 1 → 2). A file already at version 1 only runs the second step (1 → 2).

The version key in stored data

The plugin stores the version under the key "__configurate_version__" in the config object. You can read this key directly from raw config data if you need it for debugging or logging:
const locked = await config.load().run();
const storedVersion = (locked.data as Record<string, unknown>)["__configurate_version__"];
console.log(storedVersion); // e.g. 1
This key is managed internally by the plugin. Do not set or delete it manually — it is written automatically on every create, save, patch, or migration auto-save when version is set on the constructor.

Best practices

1

Start versioning early

Add version: 0 to the constructor even before you have any migrations. This ensures the version key is present in all new files from the start, making future migrations easier to reason about.
2

Keep migration functions pure

Each up function receives old data and must return new data. Do not produce side effects (API calls, storage writes) inside a migration step — the plugin manages persistence.
3

Never modify or remove a past migration

Once a migration has shipped to users, treat it as immutable. Existing users may be at any version — removing a step leaves files stranded at an intermediate version. Add new steps for new changes.
4

Test each migration step in isolation

Write a unit test for each up function. Pass a representative sample of old data and assert the output shape matches your new schema.

Build docs developers (and LLMs) love