---
title: "Config Migrations"
description: "Handle breaking widget config changes across versions with configVersion and migrate functions."
canonical: https://glasshome.app/docs/widgets/widget-migrations
section: "Build Widgets"
updated: 2026-08-07
---
# Config Migrations

When you change a widget's config shape, existing users still have the old config saved in their dashboard. Migrations let you transform old configs into the new shape without breaking anything.

## How It Works

  1. Increment the configVersion number in your manifest when you make a breaking config change.
  2. The migrate function receives the old config and the version it was saved at, and returns the new shape.
  3. When the user's saved configVersion is lower than the manifest's, the dashboard calls migrate before passing config to your component.

The `fromConfigVersion` argument passed to your `migrate` function is the integer stored in the user's saved config. If the config was saved before `configVersion` was ever set, the dashboard treats the stored version as **1** (and also treats `manifest.configVersion` as 1 when omitted). In practice: if you add `configVersion: 2` to an existing widget for the first time, `fromConfigVersion` will be `1` for all existing users, so your migration should check `fromVersion < 2`.

## Example

Say your widget originally had a single `entity` string field, and you're changing it to an `entityId` field with an additional `showLabel` boolean.

```tsx
// src/my-widget/index.tsx
import { defineConfig, defineWidget, field, type Infer } from "@glasshome/widget-sdk";

// v2 config (current)
const configSchema = defineConfig({
  entityId: field.entity("light", { title: "Light" }),
  showLabel: field.toggle({ title: "Show label", default: true }),
});

type Config = Infer<typeof configSchema>;

// The shape v1 configs were saved in. A type, not a schema: it costs nothing at
// runtime, and the compiler checks the old field names you reference below
// instead of you having to remember them.
type ConfigV1 = { entity?: string };

function MyWidget(props: { config: Config }) {
  // field.entity is a single-select picker, but it stores an array, so a later
  // switch to field.entities is not another config break.
  return (
    <div>
      <span>{props.config.entityId[0]}</span>
    </div>
  );
}

export default defineWidget<Config>({
  manifest: {
    name: "My Widget",
    sdkVersion: "^1.4.0",
    minSize: { w: 1, h: 1 },
    maxSize: { w: 4, h: 4 },
    configVersion: 2,
  },
  configSchema,
  migrate(oldConfig, fromVersion) {
    if (fromVersion < 2) {
      const old = oldConfig as ConfigV1;
      return {
        entityId: old.entity ? [old.entity] : [],
        showLabel: true,
      };
    }
    return oldConfig;
  },
  component: MyWidget,
});
```

> **Be lenient with old config:** `migrate` receives whatever was saved: possibly hand-edited, possibly written
>   by a version that predates a rule you added later. Read it defensively and map
>   what you recognise. Do not parse it with a strict schema, because a parse that
>   throws means the migration cannot run at all, turning a slightly-off config
>   into a widget that will not load. The dashboard validates your *output*
>   against the current schema anyway, so that is where strictness belongs.

## Testing migrations locally

The safest way to test a `migrate` function is to unit-test it in isolation: call it with example old config objects and assert the output shape. Because `migrate` is a pure function (no side effects, no SDK imports required), a plain test file with `bun test` works without any dashboard involved.

For end-to-end testing, run `bun widget connect <dash-url>` against a Dash instance that has a widget placement with old config. The dashboard calls your `migrate` function live, and you can inspect the result in the widget's settings panel.

## Guidelines

- **Always increment, never reset.** `configVersion` should only go up. When omitted or missing from saved config, it is treated as 1. Bump by 1 for each breaking change.
- **Handle all previous versions.** Your `migrate` function might receive configs from any previous version. Check `fromVersion` and handle each case.
- **Return a complete config.** The returned object should match your current config schema. Missing fields fall back to the defaults declared on your `field.*` helpers.
- **Non-breaking changes don't need a migration.** Adding a new optional field with a default doesn't require bumping `configVersion`. Only bump when you rename, remove, or change the type of an existing field.

> **No migration needed?:** If you're only adding new optional fields with defaults, you don't need `configVersion` or `migrate` at all. The defaults are filled in for you.