09 — App Ecosystem¶
1. Model¶
An app is a Docker Compose project plus an Onyx manifest that describes how the app integrates with the OS (permissions, volumes, ports, env, health, UI extras). Apps are the OS's only extension mechanism — there is no "plugin API" separate from the app store; the SDK exposes the same surfaces apps use.
jellyfin/
├── onyx.yaml # manifest (required)
├── compose.yaml # standard Docker Compose
├── icon.svg # 256px
├── README.md # rendered in the store
└── screenshots/
2. Manifest (onyx.yaml)¶
id: jellyfin
name: Jellyfin
version: 10.10.0
category: media
license: GPL-2.0
homepage: https://jellyfin.org
description: Free software media system.
icon: icon.svg
compose: compose.yaml
resources:
memory_min: 512Mi
memory_max: 4Gi
cpu: "2"
permissions: # what the app may ask for
volumes:
- /config # maps to @apps/jellyfin/config
- /media # user grants a share at install
ports:
- 8096 # published to host
env: # secrets stored in keyring, injected as refs
- JELLYFIN_ADMIN_PIN
network: bridge # bridge | host (host = flagged high-risk)
devices: [] # e.g. /dev/dri for transcoding
capabilities:
- media
healthcheck: http://localhost:8096/health
backup:
paths: [/config]
databases: []
integrations:
- webui: { path: "/", port: 8096 }
- share: { name: "Media", mount: "/media" } # optional auto-share
store:
sources: ["official", "community", "custom"]
3. App store¶
- Catalog tiers:
- Official — curated by the Onyx project: reviewed manifests, pinned + signed image digests, tested on target hardware, security-responded. (Jellyfin, Immich, Nextcloud, Pi-hole, Syncthing, Home Assistant, Gitea, Paperless-ngx, qBittorrent, Vaultwarden, …)
- Community — community-maintained entries; signed by their maintainers; shown with a "community" badge and a trust prompt at install.
- Custom — any user-supplied manifest/compose (paste YAML or point at a git repo).
- Store protocol: a static, signed JSON index (
apps.jsonper store) with per-app manifest URLs; indexes can be self-hosted. Updates to an app = new manifest + digests, pulled on schedule or on demand. - Search & discoverability: search by name/description, filter by category and capabilities, "Works well on Raspberry Pi" filter, install counts (anonymous, local).
- Reviews are not in v1 (defer; discussion lives in the forum) — but installs show the app's upstream health (last release, open CVEs).
4. Install UX (the 30-second promise)¶
- Browse the store, preview screenshots + README, see resource estimate and disk footprint ("~1.2 GB + media").
- Install → the wizard shows exactly what it will do, human-readable: "Jellyfin will: create its own storage (2 GB), publish port 8096, and request access to your Media share."
- One click →
onyx-appdpulls pinned images (progress), writes compose to/var/lib/onyx/apps/<id>/, creates the app subvolume, injects secrets, starts, runs the healthcheck. - Done screen: "Open Jellyfin" (opens the web UI via the gateway's app proxy), links to settings, and a one-click "Add a backup for this app".
5. App lifecycle and supervision¶
- Status:
installed → starting → running | degraded | stopped | updating | error. Health from the manifest healthcheck + container state; degraded apps show in the dashboard Health card with one-click logs. - Start/stop/restart/update from the UI or
onyx appCLI; updates are config-preserving (data volume untouched) with automatic pre-update snapshot of the app subvolume and one-click rollback to the previous image. - Logs: unified viewer with follow + filter;
--jsonfor scripting. - Uninstall: typed confirmation; app subvolume is snapshotted first
(
app-uninstall-<id>-<ts>), then removed; user can delete the snapshot later. - Startup policy:
enabled | disabled | on-demand(on-demand = socket/proxy activation where supported, for low-RAM devices).
6. Sandboxing and permissions (recap from §07.6)¶
- All apps: user namespaces, seccomp, AppArmor deny-by-default, memory/pids limits, bridge networking, read-only rootfs where upstream allows.
- Every permission (volume, port, device, host network, privileged) is requested at install and re-confirmed on update if the manifest changed; the UI renders permission diffs ("This update adds: port 8080").
- Apps cannot see: the host unix sockets, other apps' subvolumes, the API token, or the
keyring. Apps reach the API only via an injected, scoped app token with their declared
integrations. - Enforced, not just documented:
onyx-appdrefuses a catalog manifest at startup when any of its services is privileged, shares a host namespace, adds a dangerous capability back, omitsno-new-privileges/cap_drop/pids_limit/mem_limit, or bind-mounts a path that is not a named volume or a location under the storage root. Install-time path settings are confined to the storage root the same way, so an operator's input cannot widen an app's reach. cap_drop: [ALL]is the baseline, and a manifest may add narrow capabilities back. Dropping everything and declaring what is needed is the supported direction; a dangerous addition (SYS_ADMIN,SYS_PTRACE,SYS_MODULE,SYS_RAWIO,DAC_READ_SEARCH,ALL) is refused outright. The narrow additions exist because mainstream images drop privileges in their entrypoint — MariaDB chowns its data directory andsu-execs tomysql, Nextcloud rsyncs and chowns its code before Apache drops towww-data. With everything dropped and nothing added back, that entrypoint fails (failed switching to 'mysql': operation not permitted) and the app crash-loops, so the catalog manifests declare exactly the capabilities those images need (CHOWN,DAC_OVERRIDE,FOWNER,SETGID,SETUID) and nothing more.
7. SDK¶
onyx-sdk (Apache-2.0) provides:
- Go + TypeScript clients for the public API (same contracts as the UI).
- App runtime helpers: health endpoint conventions, graceful-shutdown contract,
optional
ONYX_*env injection (app id, data dir, token) — no SDK dependency required to run, only to integrate deeply. - Manifest tooling:
onyx app validate(lint manifest against the schema),onyx app package(build a store entry + signature), CI templates. - Templates: a
onyx app newscaffolder with ready-to-run examples (web service, worker, media app).
8. App-to-app and share integration¶
- Share mounts: apps declare
share:mounts that the user fulfills by picking a share at install (mounted read-only or rw per the manifest); the Files UI shows "Used by Jellyfin" on such shares. - App proxy: apps with
integrations.webuiare reachable athttps://<nas>/apps/<id>/through the gateway (single TLS cert, no extra ports) — port publishing becomes optional. - Events: apps can subscribe to
onyx-bustopics (e.g., a backup hook aftersnap.created) via the SDK — the beginning of real app automation.
9. Governance and trust¶
- Official store policy: images must be upstream-official or patched only with published source; manifests reviewed in the open; CVEs tracked; apps with sustained unmaintained status get a "maintenance needed" badge and eventual archival.
- Community store: anyone can publish; signing keys are bound to accounts; the UI shows publisher identity and a first-install trust prompt.
- Reproducibility goal: official store builds reproducible where upstream allows; SBOM per app surfaced in the app detail page.
- Removal policy: an app can be delisted but installed copies keep working (no kill switch); the OS never deletes user data on store changes.
10. Roadmap extensions (designed now, built later)¶
- App profiles for low-RAM hardware (e.g., "Jellyfin Lite": transcoding off, lower caches) — v1.5.
- Sidecar apps (e.g., download helper for a share, auto-backup on import) — v1.5.
- App marketplace payments/patreon-style support buttons — v2, only if community asks.
- Nested containers / VM apps — explicitly out of scope; Onyx apps are containers only.