---
title: "Config Migrations"
description: "Handle breaking widget config changes across versions with configVersion and migrate functions."
canonical: https://glasshome.app/docs/widget-migrations
section: "Build Widgets"
updated: 2026-06-09
---
# 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

  ### Bump configVersion

Increment the configVersion number in your manifest when you make a breaking config change.
  ### Add a migrate function

The migrate function receives the old config and the version it was saved at, and returns the new shape.
  ### Dashboard runs it automatically

When the user

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 { defineWidget } from "@glasshome/widget-sdk";
import { z } from "zod";

// v1 schema, kept around for safe parsing during migration
const configSchemaV1 = z.object({
  entity: z.string(),
});

// v2 schema (current)
const configSchema = z.object({
  entityId: z.string(),
  showLabel: z.boolean().default(true),
});

type Config = z.infer<typeof configSchema>;

function MyWidget(props: { config: Config }) {
  return (
    <div>
      <span>{props.config.entityId}</span>
    </div>
  );
}

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

## 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 will use Zod defaults if you have them.
- **Non-breaking changes don't need a migration.** Adding a new optional field with a Zod 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 in your Zod schema, you don't need `configVersion` or `migrate` at all. Zod fills in the defaults automatically.