Skip to main content
tauri-plugin-configurate ships two utility exports that complement the main Configurate class. configDiff() is a standalone function that computes a structural diff between any two config-shaped objects — useful for logging, auditing, or selectively reacting to changes. MigrationStep is the interface you implement to upgrade stored configs from an older schema version to a newer one.

configDiff(oldData, newData)

Computes a structural diff between two config objects and returns an array of change descriptors. Nested objects are compared recursively, and each change is reported at its deepest differing path using dot-separated notation.
function configDiff(
  oldData: Record<string, unknown>,
  newData: Record<string, unknown>,
): DiffEntry[]
oldData
Record<string, unknown>
required
The baseline config object — the “before” state.
newData
Record<string, unknown>
required
The updated config object — the “after” state.
returns
DiffEntry[]
An array of DiffEntry objects, one for each key that was added, removed, or changed. The array is empty when the two objects are deeply equal. Order follows the iteration order of the union of both objects’ keys.
import { configDiff } from "tauri-plugin-configurate-api";

const changes = configDiff(
  { theme: "light", fontSize: 14 },
  { theme: "dark", fontSize: 14, lang: "en" },
);
// [
//   { path: "theme", type: "changed", oldValue: "light", newValue: "dark" },
//   { path: "lang", type: "added", newValue: "en" },
// ]
Nested objects are walked recursively and paths are dot-separated:
const changes = configDiff(
  { database: { host: "localhost", port: 5432 } },
  { database: { host: "db.example.com", port: 5432 } },
);
// [
//   { path: "database.host", type: "changed", oldValue: "localhost", newValue: "db.example.com" },
// ]
Recursion depth is capped at 64 levels. If either object contains nesting deeper than 64 levels, configDiff() throws "configDiff: maximum nesting depth exceeded". In practice, config objects are never this deep.

Using configDiff with onChange

A common pattern is to call configDiff inside an onChange listener to react only to specific fields:
import { Configurate, configDiff } from "tauri-plugin-configurate-api";

let previousData = (await config.load().run()).data;

const unlisten = await config.onChange(async () => {
  const nextData = (await config.load().run()).data;
  const changes = configDiff(
    previousData as Record<string, unknown>,
    nextData as Record<string, unknown>,
  );

  for (const change of changes) {
    if (change.path === "theme") {
      applyTheme(change.newValue as string);
    }
  }

  previousData = nextData;
});

DiffEntry

Each element in the array returned by configDiff() conforms to this interface.
interface DiffEntry {
  path: string;
  type: "added" | "removed" | "changed";
  oldValue?: unknown;
  newValue?: unknown;
}
path
string
Dot-separated path to the changed key, relative to the root of the config object. For example, a change to database.host produces "database.host".
type
"added" | "removed" | "changed"
The kind of change:
  • "added" — the key is present in newData but absent in oldData.
  • "removed" — the key is present in oldData but absent in newData.
  • "changed" — the key is present in both but the values differ.
oldValue
unknown
The previous value. Present for "removed" and "changed" entries; absent for "added".
newValue
unknown
The new value. Present for "added" and "changed" entries; absent for "removed".
const changes = configDiff(
  { theme: "light", fontSize: 14, lang: "en" },
  { theme: "dark", fontSize: 14 },
);
// [
//   { path: "theme",  type: "changed", oldValue: "light", newValue: "dark" },
//   { path: "lang",   type: "removed", oldValue: "en" },
// ]

MigrationStep

MigrationStep is the interface you implement to define how config data should be transformed when the stored version is older than the current version declared in the Configurate constructor. Migrations are applied automatically during load(); if any step runs, the migrated result is auto-saved back to storage.
interface MigrationStep<
  TData extends Record<string, unknown> = Record<string, unknown>
> {
  version: number;
  up: (data: TData) => TData;
}
version
number
The schema version this step upgrades from. A step with version: 1 transforms data that was saved at version 1 into version 2.
up
(data: TData) => TData
A pure transform function. Receives the config data at the declared version and must return the data shaped for the next version. Do not mutate the input object — return a new one.

How versioning works

When you set version: N in the constructor, the plugin stores __configurate_version__: N alongside your data on every write. On load(), it reads the stored version and runs every MigrationStep whose version is less than the stored version, in ascending order, until the data reaches the current version.
import {
  Configurate,
  defineConfig,
  JsonProvider,
} from "tauri-plugin-configurate-api";
import { BaseDirectory } from "@tauri-apps/api/path";

const schema = defineConfig({
  theme: String,
  fontSize: Number,
  // "lang" was added in v1
  // "legacyFlag" was removed in v2
});

const config = new Configurate({
  schema,
  fileName: "app.json",
  baseDir: BaseDirectory.AppConfig,
  provider: JsonProvider(),
  version: 2,
  migrations: [
    {
      // Upgrade from version 0 → 1: add the "lang" field
      version: 0,
      up: (data) => ({ ...data, lang: "en" }),
    },
    {
      // Upgrade from version 1 → 2: remove the deprecated "legacyFlag" field
      version: 1,
      up: (data) => {
        const { legacyFlag, ...rest } = data as typeof data & { legacyFlag?: unknown };
        return rest as typeof data;
      },
    },
  ],
});
Write each up function as a pure transform — copy the input with the spread operator and add or omit fields rather than mutating in place. This makes migration logic easier to unit-test and reason about.
If a migrated config cannot be auto-saved back (for example, because the disk is full), the failure is logged as a warning but does not surface to your code. The migrated data is still returned in memory for the current session.