Skip to content

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.json per 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)

  1. Browse the store, preview screenshots + README, see resource estimate and disk footprint ("~1.2 GB + media").
  2. 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."
  3. One click → onyx-appd pulls pinned images (progress), writes compose to /var/lib/onyx/apps/<id>/, creates the app subvolume, injects secrets, starts, runs the healthcheck.
  4. 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 app CLI; 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; --json for 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-appd refuses a catalog manifest at startup when any of its services is privileged, shares a host namespace, adds a dangerous capability back, omits no-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 and su-execs to mysql, Nextcloud rsyncs and chowns its code before Apache drops to www-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 new scaffolder 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.webui are reachable at https://<nas>/apps/<id>/ through the gateway (single TLS cert, no extra ports) — port publishing becomes optional.
  • Events: apps can subscribe to onyx-bus topics (e.g., a backup hook after snap.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.