Skip to content
Ihsen4 min readPart 3 of 3Build a GlassHome widget
  • Widgets
  • Developers
GlassHome dashboards for Home Assistant across themes, on a monitor and phone

Shipping a widget other people install

Capabilities, preview images and config migrations: the three things that decide whether a stranger keeps your widget installed.

Parts 1 and 2 built widgets. This one covers the gap between “works on my dashboard” and “a stranger installs it and still has a working tile after your next release”. Three things decide that, and none of them is your component.

Your install prompt is your first impression

A widget declares what it needs to touch:

capabilities: [{ domain: "light", access: "control" }],

The first time somebody adds your widget, Dash shows a card listing what you asked for and waits. Approve, and the widget takes the slot. That’s the trust model, and it has consequences.

An empty list means no prompt. A widget that only reads entities and never calls a service declares capabilities: [] and installs with no friction. The prompt exists for the bridge service calls cross.

read and control are different asks. A weather widget wants read, a light switch wants control. Asking for control when you only display things is a torch app wanting your contacts, and the card shows the difference.

Widening capabilities later re-prompts. An update asking for more than the user granted queues for approval; updates that widen nothing install quietly. So a narrow list isn’t a cage, it’s renegotiable in public.

So ask for the smallest set that works today. Capabilities has the enforcement details.

Previews: the screenshot is the storefront

Nobody installs a tile they haven’t seen. Declare examples and Hub renders each one, light and dark:

examples: [
  {
    label: "Room group",
    size: { w: 3, h: 2 },
    config: { title: "Living Room", entityIds: ["light.living_room_main", "light.kitchen_counter"] },
  },
  { label: "Single light", size: { w: 2, h: 2 }, config: { title: "Desk", entityIds: ["light.desk_rgb"] } },
],

The configs reference GlassHome’s demo home, around fifty fixed entities, so your widget renders with plausible data. light.studio_rgb and light.desk_rgb are colour-capable.

Render them locally before you publish:

bun widget preview

Images land in preview/, rendered exactly as the published ones are. Iterating there is free; iterating by publishing costs a version number.

Pick examples that show the widget doing its job: a group of lights beats one bulb. And check you didn’t ship the empty state, which is what an example with entities the demo home lacks puts on the catalogue page. The previews guide lists them.

Config that survives your own updates

Config is stored per instance. Change its shape and every existing install is holding the old one. Say v1 stored a single entity and v2 stores a list:

export default defineWidget<Config>({
  manifest: {
    // ...
    configVersion: 2,
  },
  configSchema,
  migrate: (config, fromVersion) => {
    if (fromVersion < 2 && typeof config.entityId === "string") {
      return { ...config, entityIds: [config.entityId], entityId: undefined };
    }
    return config;
  },
  component: MyWidget,
});

Dash runs this on every mount:

  1. Read the stored config and its configVersion, defaulting to 1 if it has none.
  2. If that is older than your manifest’s configVersion and you shipped a migrate, call it with the stored config and the version it came from.
  3. Validate the result against your schema.
  4. Stamp the current version on it.

If step 2 throws or step 3 fails, the widget renders defaults and the stored config stays untouched on disk. Nothing is lost, but the user sees a tile that forgot their settings.

The guard that catches this at build time

Change your config’s shape without bumping configVersion and the build fails: the SDK hashes the schema and compares it with the last one it saw.

Two habits make migrations boring: keep them pure, and never delete a field in the same release you stop reading it.

Publish

bun widget publish

It validates, logs into Hub if needed, then asks which widget, scope and bump. Hub applies three fixed gates:

  • Bundle at most 2 MB. It downloads onto phones on home wifi.
  • The version must be new. Versions are immutable; re-publishing one is a 409, so --bump patch|minor|major.
  • The scope must be yours. Your scope is your username, decoupled from your display name, so renaming yourself doesn’t strand @you/my-widget. Organizations get their own; @glasshome is reserved.

There is no review queue: the capability prompt puts that decision with the user, at install time.

Publishing has the full reference, and the SDK guide covers what these three posts skipped.