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 { 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.
configVersionshould 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
migratefunction might receive configs from any previous version. CheckfromVersionand 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.