ManifestAdapter interface + the coolify/umbrel/casaos adapters (2026-07-17)
Status: DESIGN. Part of the platform-compat program — faithful capture of
docs/handoff/2026-07-16-hel1-reprovision-and-platform-compat.md (§C, §C2,
§C2b, §C5, §E), no new design. Siblings:
2026-07-17-origin-ingest-gate.md (the
pipeline adapters run inside),
2026-07-17-app-metadata-schema.md (the
canonical metadata adapters remap into),
2026-07-17-notes-templating.md (the
notes surface adapters target),
2026-07-17-origin-repos-and-migrations.md
(catalog registration + the adapted→native migration path).
Problem
Section titled “Problem”Goal: run apps packaged for Umbrel / CasaOS / Runtipi / Coolify /
CapRover / Portainer / Cloudron on SHC — gated (opt-in per app,
<origin>-compat mode; native SHC apps untouched).
Each origin has its own manifest format, secret contract, proxy model, and volume layout. Teaching the SHC enrichment/backup/ingress/SSO core about any of them would couple the core to N evolving foreign formats. The core must stay platform-agnostic; all origin knowledge lives at the edge, in per-origin adapters that run once, at ingest.
Terminology (collision-checked against the codebase 2026-07-16)
Section titled “Terminology (collision-checked against the codebase 2026-07-16)”- origin — a foreign app ecosystem:
umbrel,casaos,runtipi,coolify,cloudron; native apps have originshc. (platformis TAKEN — 513 hits,platform:manage/PlatformAdmin/shc_config.platform;origin’s 58 hits are all incidental.) - adapter — the per-origin INGEST-TIME transpiler (origin manifest + compose → real SHC package). NOT “shim”: a shim implies a runtime layer, and there is none — the transpile is materialized at ingest.
- adapted app vs native app — what an adapter produces vs a first-party package; the enrichment core cannot tell them apart.
- origin repo — a foreign catalog registered via
shc repo add --origin <origin> <url>. - import —
shc origin import: lift a LIVE install off a real origin box onto SHC as an adapted app, data included. - migration — the data transpile, adapted → native (2026-07-17-origin-repos-and-migrations.md).
Design
Section titled “Design”Core architecture — a pluggable ManifestAdapter
Section titled “Core architecture — a pluggable ManifestAdapter”parse(origin manifest + compose) -> normalized SHC package. Each origin
is a normalization FRONT-END; the SHC enrichment/backup/ingress/SSO
CORE stays platform-agnostic. Adding a brand-new origin touches only
the edge, never the core — an adapter owns four responsibilities:
// shape only — enumerate / parse / gate / transpiletype ManifestAdapter interface { Enumerate(catalog) []appRef // origin's package layout Parse(appRef) originManifest // manifest + compose Gate(originManifest) []reject // the ingest reject taxonomy Transpile(originManifest) shcPackage // materialized SHC package}plus, per origin: the env+secret contract map, the importer’s box-layout knowledge, conformance fixtures (sample origin apps → golden normalized packages), and the licensing check for that catalog.
Umbrel adapter (self-contained single-UI HTTP subset only)
Section titled “Umbrel adapter (self-contained single-UI HTTP subset only)”Pre-pass: strip app_proxy; synthesize host_spec from the app_proxy
service’s APP_HOST/APP_PORT env, NOT the manifest port (the
manifest port is where the proxy listens; using it routes to nothing for
much of the catalog); rewrite host bind-mounts (${APP_DATA_DIR}) →
named volumes; inject the APP_* env contract; skip integration
injection. Then the unchanged core runs
traefik/network/volume/placement/health.
Secrets → vault (opinionated): Umbrel derives secrets from a
per-device APP_SEED on disk; SHC instead mints APP_SEED +
APP_PASSWORD and stores them in the vault (deployment-scoped,
backed-up, rotatable in general), injecting them as env so the app’s own
derivation still works but SHC owns them. Exception: APP_SEED is
mint-once, IMMUTABLE, non-rotatable — apps persist seed-derived creds
(admin hash in their DB, initdb passwords) at first-init; rotation =
lockout the platform can’t see; restore must replay the SAME seed. No
standard vault-rotation UX on it.
Volumes: Umbrel packages ship seed files copied into
${APP_DATA_DIR} pre-start; named volumes populate from the IMAGE, not
the package → volume seed-file injection is a missing core primitive
(init-job / volume-seeding step). Overlapping subpath mounts need a
stated shared-parent-volume rule; UID/ownership first-boot failures are
expected; compat volumes must pin to LOCAL (never network-backed)
providers.
Scope: self-contained single-UI HTTP apps only. dependencies /
exports.sh carriers, torOnly, host-network/device apps are rejected
at the ingest gate.
Coolify adapter
Section titled “Coolify adapter”Compose service templates with magic env: SERVICE_FQDN_* →
host_spec; SERVICE_PASSWORD_*/_BASE64_* → vault mint via the
@secret contract. Bundled templates run as-is. Coolify’s SERVICE_*
magic env is the best-aligned secret/ingress contract to import, and the
cheapest adapter to build.
CasaOS adapter
Section titled “CasaOS adapter”Umbrel-path machinery (self-contained compose, bundled DB); metadata
read from the x-casaos: compose extension.
Integrations posture
Section titled “Integrations posture”Umbrel/CasaOS apps are self-contained — each bundles its own DB in
its own compose → run as-is, no SHC plug by default. Optional
bridges: front with the forward-auth gate for SSO (transparent) —
DEFAULT-ON for every adapted app, per-app opt-out requiring
ingest-gate evidence of self-auth (many Umbrel apps have no auth of
their own; publishing them ungated on public DNS + LE = unauthenticated
admin panels with a green padlock). Cross-app dependencies (mostly
Bitcoin/Lightning app-to-app) are rejected in v1. Cloudron is the
outlier — not compose (CloudronManifest.json + Dockerfile + declared
addons postgresql/mysql/redis/mail/oidc/ldap) — its addons map ~1:1
to SHC plugs/magic tokens (POSTGRES/KEYCLOAK/MAIL), so its adapter is
different (Dockerfile→service, addons→plugs) but the cleanest
integration fit.
Priority — REVISED
Section titled “Priority — REVISED”Original draft (optimized for catalog size): Umbrel → CasaOS → Coolify → CapRover/Portainer/Cloudron. The evaluation reversed it: SHC users look like Coolify/Cloudron users, not Umbrel users, and Coolify is also the cheapest adapter.
Revised: Coolify → Umbrel (self-contained single-UI HTTP subset only) → CasaOS → stop. CapRover and Portainer are dropped from the roadmap.
Cloudron is gated on three unscoped prerequisites — it is the best architecture fit (addons≈plugs, built-in SSO+backups) AND the worst supply-chain/legal fit:
- Registry licensing / image-build pipeline — Cloudron images come from a paid registry, so SHC would need its own image-build pipeline.
- LDAP addon — no SHC analog (the oidc addon maps to the keycloak plug; ldap only to the forward-auth gate, which is not the same contract).
recvmail— no SHC analog.
Licensing generally is a legal decision, not engineering: the Umbrel platform is PolyForm Noncommercial; catalog + gallery screenshots need diligence before any verbatim ingest, given SHC is commercially deployed.
Capability matrix — packaging + integration model (Table A)
Section titled “Capability matrix — packaging + integration model (Table A)”The load-bearing axis: does the compose bundle EVERYTHING, or does the app integrate with platform-managed external services?
| Platform | Package format | Compose bundles everything? | External / managed services | SHC compat handling |
|---|---|---|---|---|
| Umbrel | umbrel-app.yml + docker-compose.yml | YES — every app ships its own DB/cache in its own compose | none (its dependencies = other Store APPS via exports.sh env, mostly bitcoin/lightning) | run the stack as-is, bundled DB included; dependencies/exports.sh REJECTED at ingest (v1) |
| CasaOS | compose + x-casaos: extension | YES — self-contained | none | Umbrel-path machinery; metadata read from x-casaos |
| Runtipi | config.json + compose | YES — self-contained | none | same; form_fields → typed vars |
| Coolify | compose service template + SERVICE_* magic env | mostly — templates bundle their DBs | optional standalone managed DBs; magic env mints creds/FQDNs | bundled runs as-is; SERVICE_FQDN_* → host_spec, SERVICE_PASSWORD_*/_BASE64_* → vault mint |
| CapRover | captain-definition / one-click YAML | partial — single-service apps; DBs installed as SEPARATE one-click apps, env-wired | quasi (separate app instances, manual wiring) | dropped from roadmap |
| Portainer | JSON app template | YES per template | whatever env points at (unmanaged) | dropped from roadmap |
| Cloudron | CloudronManifest.json + Dockerfile — no compose | NO — single app container, read-only rootfs | YES — platform addons: postgresql/mysql/mongodb/redis/mail/oidc/ldap/localstorage/scheduler via CLOUDRON_* env | addons map ~1:1 to SHC plugs/magic tokens (POSTGRES/KEYCLOAK/MAIL); GATED on the three prerequisites above |
| SHC native | app.yaml + compose + plugs | either — bundle-your-own OR integrate | plugs / magic tokens, cross-tenant | — |
Capability matrix — feature × platform, with the compat disposition (Table B)
Section titled “Capability matrix — feature × platform, with the compat disposition (Table B)”Terse cells; – = the platform has nothing for this.
| Capability | Umbrel | CasaOS | Runtipi | Coolify | CapRover | Portainer | Cloudron | SHC compat handling |
|---|---|---|---|---|---|---|---|---|
| Web exposure | app_proxy + port/path | x-casaos port/scheme | port + exposable | SERVICE_FQDN_* | captain subdomain | published ports | httpPort (+multiDomain) | host_spec → traefik + LE + DNS self-reg (Umbrel: from app_proxy APP_HOST/APP_PORT env, NOT manifest port) |
| Non-HTTP ports (L4) | common (P2P: 8333/9735) | some | some | some | – | template ports | rare | explicit L4 publish policy or ingest REJECT (unique-exposer + tenant port allocation) |
| Auth / SSO | app_proxy session auth (many apps have NO own auth) | – | – | – | – | – | oidc/ldap addons | forward-auth gate DEFAULT-ON, per-app opt-out w/ evidence; Cloudron oidc → keycloak plug; ldap = NO analog (gate) |
| Secrets | APP_SEED derivation | plaintext env | generated field values | SERVICE_PASSWORD_*/_BASE64_* | $$cap_gen_random_hex | plaintext env | platform-injected addon creds | mint-once → vault; APP_SEED IMMUTABLE non-rotatable; generator grammars → @secret mint |
| Volumes | bind ${APP_DATA_DIR} (+ package seed files) | bind /DATA | bind app-data | named/bind | named | template volumes | localstorage /app/data | rewrite → named volumes + provider; seed-file init job; subpath shared-parent rule; LOCAL providers only |
| Backups | whole-device (Backups/Rewind) — not per-app | – | limited archive | scheduled DB dumps (S3) | app snapshot | – | per-app encrypted scheduled | per-app quiesce+capture auto-backup.yaml (physical; same-DB-major restore contract; no exporter.yaml → second-class export/import) |
| Jobs / init / cron | compose entrypoints + exports.sh | compose | compose | scheduled tasks | pre-deploy script | – | scheduler addon + start.sh | *-init.job.yaml + lifecycle hooks + crons; exports.sh NOT executed (reject or curated reimpl) |
| Healthchecks | – | sometimes compose | – | compose | – | – | healthCheckPath | synthesize compose healthcheck where declared; else healthy-because-running caveat |
| Cross-app deps | dependencies + exports.sh + cross-app DATA mounts | – | – | attachable shared DBs | separate one-click DB apps | – | addons (managed) | v1: REJECT dep-carrying apps at ingest (backup-boundary violation class); Cloudron addons → plugs |
| User config at install | – (env fixed) | x-casaos env labels | typed form_fields | env prompts | variables prompts | env label/select | env via UI | map to the typed vars DSL (@string/@secret/@boolean/@port…) |
| Post-install message | defaultUsername/defaultPassword | tips (verify name in census) | – | docs link | instructions.end | – | postInstallMessage | notes template (sensitive-safe render of minted creds + resolved host) |
| Store metadata | rich (tagline,gallery,developer,…) | x-casaos fields | config.json fields | template metadata | one-click fields | title/logo/categories | rich (mediaLinks, changelog, …) | canonical SHC-vocab fields (census-driven) + compat: passthrough bag |
| Updates / migrations | store updates + hooks | image pull | version bump | redeploy | image update | manual | platform-managed w/ migrations | re-transpile new version + redeploy; DB-image pin is part of the backup/restore contract |
| Resource limits | – | – | – | some | – | – | memoryLimit | quota DEFAULTS for compat apps (noisy-neighbor guard); map memoryLimit |
| Remote access / Tor | Tor hidden svc + Tailscale | – | – | – | – | – | – | public DNS + LE instead; torOnly REJECT; outbound Tor = optional sidecar |
| Host devices / host net | some (Home Assistant, USB) | common | some | rare | – | template-dependent | – (contained) | ingest REJECT (network_mode: host, /dev passthrough, privileged) |
| Multi-instance / scale | – | – | – | – | scaling | – | some (multiDomain) | swarm replicas / x-shc-scale where the app tolerates it; else single-replica pin |
Takeaway: SHC is the superset on the server/platform axis (per-app
backups, integrations, multi-tenant/multi-cloud). Cloudron is the
closest architectural analog (addons≈plugs, built-in SSO+backups) and
arguably the highest-value non-compose adapter — gated as above.
Coolify’s SERVICE_* magic env is the best-aligned secret/ingress
contract to import.
Backups: what adapted apps get, stated up front
Section titled “Backups: what adapted apps get, stated up front”Quiesce-and-capture makes physical capture of a paused bundled DB
crash-consistent and restorable — BUT only into the same DB major
version (image pins become part of the backup contract), and adapted
apps are second-class for export/import (no exporter.yaml). State
it, don’t let users discover it. The path out of second-class is a
migration to native apps
(2026-07-17-origin-repos-and-migrations.md).
Steady-state maintenance requirements
Section titled “Steady-state maintenance requirements”The steady-state cost = tracking N evolving manifest formats. Required, or the adapters rot in a quarter:
- pinned upstream snapshots (every sync records the origin sha);
- re-transpile CI diffing the normalized output on each re-sync (stage 7 of the ingest pipeline);
- conformance fixtures per origin — sample origin apps → golden normalized packages, living with the adapter;
- resource-quota defaults for quota-less manifests;
- synthesized healthchecks (Cloudron
healthCheckPath→ compose healthcheck; Umbrel apps will read healthy-because-running); - image pinning / provenance policy for unreviewed third-party compose.
Decided
Section titled “Decided”- Adapters are ingest-time transpilers, not runtime shims; the core cannot tell adapted from native.
- Priority: Coolify → Umbrel subset → CasaOS → stop; CapRover and Portainer dropped.
- Cloudron gated on registry-licensing/image-build-pipeline, LDAP-plug,
and
recvmail— all three unscoped. - Forward-auth gate default-on for adapted apps; opt-out per app with ingest-gate evidence.
APP_SEEDimmutable; other minted secrets follow the normal vault contract.- Compat volumes on LOCAL providers only.
- Scoping the three Cloudron gates (each is its own investigation).
- Volume seed-file injection as a core primitive: init-job shape and the shared-parent-volume rule for overlapping subpath mounts.
- Umbrel’s
implements(drop-in provider contract) is conceptually adjacent to SHC’s integration providers — future look, not v1. - Licensing diligence per catalog (legal, not engineering) before any verbatim ingest is served beyond private use.