---
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-09-13
---
# 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



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 shape 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.
- **Bump on any shape change; migrate only on a breaking one.** The build guard compares your config schema against the shape it recorded last build and fails when it changed without a `configVersion` bump. That includes adding a field. A `migrate` function is only needed when the change would break a saved config: a rename, a removal, or a type change.

> **No migration needed?:** Adding new optional fields with defaults still needs the `configVersion` bump the build guard asks for, but no `migrate` function. The defaults are filled in for you.