Skip to content

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

Bump configVersion

Increment the configVersion number in your manifest when you make a breaking config change.

2

Add a migrate function

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

3

Dashboard runs it automatically

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.

// 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.