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 three distinct workarounds, all 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 in the same repo. 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 not exposed by default, so parallel install doesn't collide unless exposed manually.
- 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 mapping for 8485 (socat), exposed by default, so parallel install collides by default.
- 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 (WebRTC running on 8555 tcp and udp), exposed by default.
- 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. No port configuration, host network, tries to bind at least port 8097 by default which collide.
- Room Assistant (
mKeRix/hassio-repo) — separate dev slug published alongside stable, same pattern as the other Workaround B examples.
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.
Workaround C: a whole store repository per channel. Each channel is shipped as its own store repo; the same app is then maintained in parallel across multiple repositories under the same in-repo slug. Users add each repository they want access to. From Supervisor's perspective these are still independent apps because the store key is namespaced by repository (<repo>_<slug>).
-
Community Add-ons (hassio-addons), the largest single source of apps in the ecosystem — three repos:
Every app exists in all three. Notably, the beta and edge repositories contain more apps than stable — new apps are commonly iterated on in beta/edge first and only promoted to stable once they're proven (some never reach stable). The patterns Frenck established here have been widely copied, so a number of other smaller stores follow this same model.
Tradeoffs: cleanest from an authoring perspective — each repository has its own release branch, release cadence, and build pipeline. But users must subscribe to multiple stores, and Supervisor sees the same app three times under three different slug keys with no relationship between them, so state doesn't carry over and the user has no way to "switch" — they install, configure, migrate manually.
Why this matters for OHF. App quality depends on getting more users onto pre-release builds before stable cuts. All three 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.
- Workaround C asks users to manually subscribe to multiple stores and re-install the app in each one to migrate between channels.
None of them 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 — the most-installed Apps in the ecosystem (Matter Server, OTBR, ESPHome, Z2M, Frigate, Music Assistant, Room Assistant, plus the entire Community Add-ons catalog) 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).
- Channel variants of the same App living in different store repositories. All variants must live in one repository directory; users should not have to subscribe to multiple stores to access channels (this would contradict the unified-App approach).
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.
The set of recognized channels is fixed by Supervisor — stable, beta, dev (mirroring Home Assistant Core's own update channels). Free-form author-chosen channel names were considered and rejected: a fixed vocabulary keeps the UI consistent, the strings translatable, and prevents the channel mechanism from being repurposed as a generic "app variant" system (e.g. capability splits like full-access vs. minimal, or hardware splits like oldcpu).
Supervisor groups these into a single logical App. The user-visible model:
- The Store lists one App with an
available_channels set. available_channels is not an authored field — Supervisor derives it from which config.<channel>.yaml files exist in the repo, so no new metadata file is needed.
- An installed App carries a persisted
channel. On a fresh install, default = the highest-stability channel the App currently offers (stable > beta > dev). Apps may exist without a stable variant — common during early development, and confirmed in practice by Community Add-ons where new apps iterate purely in beta/edge before promotion. Supervisor handles this naturally because the channel set is ordered.
- 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. Should apps have options to prevent downgrade/force backup restore? We don't really deal with that with Core either (manual downgrade is possible). Apps also don't need to use the channel feature.
- 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 (Workaround B) or three separate store repos (Workaround C) 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 three distinct workarounds, all 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 in the same repo. 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 not exposed by default, so parallel install doesn't collide unless exposed manually.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 mapping for 8485 (socat), exposed by default, so parallel install collides by default.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 (WebRTC running on 8555 tcp and udp), exposed by default.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. No port configuration, host network, tries to bind at least port 8097 by default which collide.mKeRix/hassio-repo) — separate dev slug published alongside stable, same pattern as the other Workaround B examples.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.Workaround C: a whole store repository per channel. Each channel is shipped as its own store repo; the same app is then maintained in parallel across multiple repositories under the same in-repo slug. Users add each repository they want access to. From Supervisor's perspective these are still independent apps because the store key is namespaced by repository (
<repo>_<slug>).Community Add-ons (
hassio-addons), the largest single source of apps in the ecosystem — three repos:Every app exists in all three. Notably, the beta and edge repositories contain more apps than stable — new apps are commonly iterated on in beta/edge first and only promoted to stable once they're proven (some never reach stable). The patterns Frenck established here have been widely copied, so a number of other smaller stores follow this same model.
Tradeoffs: cleanest from an authoring perspective — each repository has its own release branch, release cadence, and build pipeline. But users must subscribe to multiple stores, and Supervisor sees the same app three times under three different slug keys with no relationship between them, so state doesn't carry over and the user has no way to "switch" — they install, configure, migrate manually.
Why this matters for OHF. App quality depends on getting more users onto pre-release builds before stable cuts. All three workarounds make opting-in friction-heavy or risky:
None of them 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 — the most-installed Apps in the ecosystem (Matter Server, OTBR, ESPHome, Z2M, Frigate, Music Assistant, Room Assistant, plus the entire Community Add-ons catalog) 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.The set of recognized channels is fixed by Supervisor —
stable,beta,dev(mirroring Home Assistant Core's own update channels). Free-form author-chosen channel names were considered and rejected: a fixed vocabulary keeps the UI consistent, the strings translatable, and prevents the channel mechanism from being repurposed as a generic "app variant" system (e.g. capability splits like full-access vs. minimal, or hardware splits likeoldcpu).Supervisor groups these into a single logical App. The user-visible model:
available_channelsset.available_channelsis not an authored field — Supervisor derives it from whichconfig.<channel>.yamlfiles exist in the repo, so no new metadata file is needed.channel. On a fresh install, default = the highest-stability channel the App currently offers (stable>beta>dev). Apps may exist without astablevariant — common during early development, and confirmed in practice by Community Add-ons where new apps iterate purely in beta/edge before promotion. Supervisor handles this naturally because the channel set is ordered./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. Should apps have options to prevent downgrade/force backup restore? We don't really deal with that with Core either (manual downgrade is possible). Apps also don't need to use the channel feature.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 (Workaround B) or three separate store repos (Workaround C) 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