Problem statement
There is an obvious, ongoing need for Apps (formerly add-ons) to ship pre-release builds alongside their stable release so users can opt into testing without losing their existing setup. The problem is that Supervisor has no first-class concept of an "App channel" — so the ecosystem implements this with two distinct workarounds, both of which have significant downsides:
Workaround A: option flag inside a single App. One slug, one container image that bundles both runtimes; a config option (commonly beta: true) switches behavior at startup.
- OpenThread Border Router (
home-assistant/addons, openthread_border_router) — has a beta: false option in its schema.
- Matter Server (
home-assistant/addons, matter_server) — has beta: false. Its run script inspects the flag and, if true, npm installs matter-server@latest and execs the JavaScript Matter Server; otherwise it runs the Python Matter Server. The stable image therefore ships both the Python runtime and a full Node.js + npm toolchain so beta can be activated by toggling a checkbox. There are also escape-hatch options (matter_server_version, matter_sdk_wheels_version, matter_server_args, matter_server_env_vars) so power users can pin a specific upstream version — each of which is essentially a workaround for not being able to install a different image.
Tradeoffs: same /data and options survive switching, which is the property authors actually want — but the stable image carries a beta runtime, the schema is polluted with channel-control fields, and switching back/forth happens via config edits with no safety net.
Workaround B: separate Apps with distinct slugs. Technically cleaner image separation, allows parallel install in theory, but the App's persistent state is tied to the slug, so users either lose state when migrating or the project has to push state out of /data into shared folders.
- ESPHome (
esphome/home-assistant-addon) — three slugs: esphome, esphome-beta, esphome-dev. URLs differ (esphome.io / beta.esphome.io / next.esphome.io). All three map config:rw (Home Assistant's main config/ folder) and store device configs there rather than in /data — that's how migration works in practice. Default port 6052 is shared, so parallel install collides unless reassigned.
- Zigbee2MQTT (
zigbee2mqtt/hassio-zigbee2mqtt) — zigbee2mqtt (stable) and zigbee2mqtt-edge. Different images (ghcr.io/zigbee2mqtt/zigbee2mqtt-{arch} vs …-edge-{arch}), shared state via share:rw + homeassistant_config:rw. Identical port mappings, so parallel install collides.
- Frigate (
blakeblackshear/frigate-hass-addons) — six variant slugs: frigate, frigate_beta, frigate_fa, frigate_fa_beta, frigate_oldcpu, frigate_proxy. Same image with different tags; many overlapping ports.
- Music Assistant (
music-assistant/home-assistant-addon) — four slugs: music_assistant (stable 2.8.7), music_assistant_beta (2.9.0b13), music_assistant_dev (1.5.2 — a different upstream branch), music_assistant_nightly.
Tradeoffs: clean image and per-channel options schema, no runtime is conflated. But state doesn't carry over: communities steer users to keep config in HA's config/ or share/ (fragmenting backups and Supervisor's data model), and parallel install — the supposed benefit — usually doesn't work without manual port surgery because every variant ships the same defaults.
Why this matters for OHF. App quality depends on getting more users onto pre-release builds before stable cuts. Both workarounds make opting-in friction-heavy or risky:
- Workaround A asks users to flip a hidden checkbox with no rollback, and pushes the stable image to permanently ship a beta runtime.
- Workaround B asks users to reinstall and lose their setup, then re-onboard devices/credentials in the beta variant.
Neither encourages broad testing. The result is fewer testers, fewer pre-release bug reports, and bugs that land in stable.
Community signals
The strongest signal today is the breadth of the workarounds themselves — five of the most-installed Apps in the ecosystem (Matter Server, OTBR, ESPHome, Z2M, Frigate, Music Assistant) have all independently invented a channel mechanism, and none of them are quite the same.
From Supervisor repository:
Scope & Boundaries
In scope
- Allow one App to declare multiple channel variants (stable / beta / dev) from a single repo directory, sharing slug,
/data, options, ingress route, and discovery.
- Supervisor-side channel switching with a safety net (automatic per-App backup before switch).
- API and frontend surface to view and select an installed App's channel.
- Full backward compatibility — existing single-
config.yaml Apps behave exactly as today.
Not in scope
- Cross-channel
/data migration logic — remains the App author's responsibility, like any major-version upgrade.
- Automatic channel upgrades — switching is always user-initiated.
- Replacing the existing standalone pre-release Apps on day one. This is opt-in for App authors; existing duplicated slugs can keep working.
- Parallel install of multiple channels of the same App — explicitly not supported (the whole point is one logical App).
Foreseen solution
App authors place additional manifests in the same directory of their store repo, with a channel suffix derived from the filename:
my-app/
├── config.yaml ← stable
├── config.beta.yaml ← beta
└── config.dev.yaml ← dev (optional)
All variants share the same slug: and (typically) most of the manifest, but declare a different image: and version:. No new manifest field is introduced.
Supervisor groups these into a single logical App. The user-visible model:
- The Store lists one App with an
available_channels set.
- An installed App carries a persisted
channel (defaults to stable).
- Switching channel is a single Supervisor operation: take a partial backup of just this App, swap the image (re-using the existing update path which already handles image-name changes), and start the new variant against the existing
/data and options. On failure, roll back and surface a restore suggestion.
- New endpoint:
POST /addons/{slug}/channel. Info responses gain channel and available_channels.
Conceptually the same as Home Assistant Core's own update channel, but per-App.
Risks & open questions
- Options-schema divergence across channels. If beta tightens or renames an option, stable's stored options can fail validation on switch. Mitigation: validate current options against the target schema before pulling the image; abort early with a suggestion to reset options if invalid.
/data layout differences. Not handled by Supervisor — author responsibility, same as a major version upgrade today. The pre-switch backup is the safety net.
- App author migration cost. Frigate's
frigate_fa (full-access) variant is a capability split, not a channel split — it shouldn't be folded into channels. We should document clearly what channels are and are not for so authors don't try to model unrelated forks (e.g. oldcpu) as channels.
- Frontend UX. How prominent is the channel selector? Always visible per App, or hidden behind "advanced"? How do we signal beta risk without being alarmist?
- Naming. Core already uses "channel" for the Supervisor/OS/Core update stream. Reusing the term for Apps is intuitive but could be confused; consider whether "App channel" is sufficiently distinct in UI copy.
- Discovery/migration for existing duplicated Apps. What is the recommended path for projects with already-deployed
foo + foo-beta slugs to consolidate without breaking existing installs? Likely needs a migration tool or documented one-time backup-restore-into-stable flow.
- Pull-failure mid-switch. The old container is stopped before the new image pulls. On failure, the App stays stopped until the user restores. Needs clear UX.
Appetite
Small–medium.
- Supervisor: contained — store scanner change, one new state field on installed Apps, one new operation that reuses the existing update path, one API endpoint.
- Frontend: per-App selector + an info pill + a confirmation dialog highlighting the auto-backup.
- Docs + author guidance: the longer tail — how to structure repos, schema-compat guidance, recommended migration story for existing duplicated Apps.
Roughly 1–2 cycles of focused work across Supervisor, frontend, and developer docs.
Decision log
Problem statement
There is an obvious, ongoing need for Apps (formerly add-ons) to ship pre-release builds alongside their stable release so users can opt into testing without losing their existing setup. The problem is that Supervisor has no first-class concept of an "App channel" — so the ecosystem implements this with two distinct workarounds, both of which have significant downsides:
Workaround A: option flag inside a single App. One slug, one container image that bundles both runtimes; a config option (commonly
beta: true) switches behavior at startup.home-assistant/addons,openthread_border_router) — has abeta: falseoption in its schema.home-assistant/addons,matter_server) — hasbeta: false. Its run script inspects the flag and, if true,npm installsmatter-server@latestand execs the JavaScript Matter Server; otherwise it runs the Python Matter Server. The stable image therefore ships both the Python runtime and a full Node.js + npm toolchain so beta can be activated by toggling a checkbox. There are also escape-hatch options (matter_server_version,matter_sdk_wheels_version,matter_server_args,matter_server_env_vars) so power users can pin a specific upstream version — each of which is essentially a workaround for not being able to install a different image.Tradeoffs: same
/dataand options survive switching, which is the property authors actually want — but the stable image carries a beta runtime, the schema is polluted with channel-control fields, and switching back/forth happens via config edits with no safety net.Workaround B: separate Apps with distinct slugs. Technically cleaner image separation, allows parallel install in theory, but the App's persistent state is tied to the slug, so users either lose state when migrating or the project has to push state out of
/datainto shared folders.esphome/home-assistant-addon) — three slugs:esphome,esphome-beta,esphome-dev. URLs differ (esphome.io/beta.esphome.io/next.esphome.io). All three mapconfig:rw(Home Assistant's mainconfig/folder) and store device configs there rather than in/data— that's how migration works in practice. Default port6052is shared, so parallel install collides unless reassigned.zigbee2mqtt/hassio-zigbee2mqtt) —zigbee2mqtt(stable) andzigbee2mqtt-edge. Different images (ghcr.io/zigbee2mqtt/zigbee2mqtt-{arch}vs…-edge-{arch}), shared state viashare:rw+homeassistant_config:rw. Identical port mappings, so parallel install collides.blakeblackshear/frigate-hass-addons) — six variant slugs:frigate,frigate_beta,frigate_fa,frigate_fa_beta,frigate_oldcpu,frigate_proxy. Same image with different tags; many overlapping ports.music-assistant/home-assistant-addon) — four slugs:music_assistant(stable 2.8.7),music_assistant_beta(2.9.0b13),music_assistant_dev(1.5.2 — a different upstream branch),music_assistant_nightly.Tradeoffs: clean image and per-channel options schema, no runtime is conflated. But state doesn't carry over: communities steer users to keep config in HA's
config/orshare/(fragmenting backups and Supervisor's data model), and parallel install — the supposed benefit — usually doesn't work without manual port surgery because every variant ships the same defaults.Why this matters for OHF. App quality depends on getting more users onto pre-release builds before stable cuts. Both workarounds make opting-in friction-heavy or risky:
Neither encourages broad testing. The result is fewer testers, fewer pre-release bug reports, and bugs that land in stable.
Community signals
The strongest signal today is the breadth of the workarounds themselves — five of the most-installed Apps in the ecosystem (Matter Server, OTBR, ESPHome, Z2M, Frigate, Music Assistant) have all independently invented a channel mechanism, and none of them are quite the same.
From Supervisor repository:
Scope & Boundaries
In scope
/data, options, ingress route, and discovery.config.yamlApps behave exactly as today.Not in scope
/datamigration logic — remains the App author's responsibility, like any major-version upgrade.Foreseen solution
App authors place additional manifests in the same directory of their store repo, with a channel suffix derived from the filename:
All variants share the same
slug:and (typically) most of the manifest, but declare a differentimage:andversion:. No new manifest field is introduced.Supervisor groups these into a single logical App. The user-visible model:
available_channelsset.channel(defaults tostable)./dataand options. On failure, roll back and surface a restore suggestion.POST /addons/{slug}/channel. Info responses gainchannelandavailable_channels.Conceptually the same as Home Assistant Core's own update channel, but per-App.
Risks & open questions
/datalayout differences. Not handled by Supervisor — author responsibility, same as a major version upgrade today. The pre-switch backup is the safety net.frigate_fa(full-access) variant is a capability split, not a channel split — it shouldn't be folded into channels. We should document clearly what channels are and are not for so authors don't try to model unrelated forks (e.g.oldcpu) as channels.foo+foo-betaslugs to consolidate without breaking existing installs? Likely needs a migration tool or documented one-time backup-restore-into-stable flow.Appetite
Small–medium.
Roughly 1–2 cycles of focused work across Supervisor, frontend, and developer docs.
Decision log