---
title: "Shipping a widget other people install"
description: "Capabilities, preview images and config migrations: the three things that decide whether a stranger keeps your widget installed."
canonical: https://glasshome.app/blog/ship-a-glasshome-widget
date: 2026-08-26
tags: ["Widgets","Developers"]
---
Parts [1](/blog/build-your-first-glasshome-widget) and [2](/blog/widget-data-over-time) 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:

```tsx
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](/docs/widgets/widget-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:

```tsx
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:

```bash
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](/docs/widgets/widget-previews) 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:

```tsx
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

```bash
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](/docs/widgets/widget-publishing) has the full reference, and the [SDK guide](/docs/widgets/widget-sdk) covers what these three posts skipped.