The manifest contract
manifest.json is the one contract in My Own Suite that is a promise to people outside the project: package authors. This page is the authoritative reference for manifest generation 1 — the first locked generation. It is written for both humans and AI agents authoring packages.
The promise, concretely:
- Every field documented here is supported by every MOS release that supports generation 1. A valid generation-1 manifest will not be rejected by a future MOS release.
- Unknown fields are ignored, never fatal. A manifest may carry fields this page does not describe; MOS validates what it knows, ignores the rest, and never projects an unknown field into a runtime. This is the amendment mechanism: future capabilities arrive as optional additions, and a package that uses one declares the
minimumMosVersionthat introduced it. - Nothing beyond the required core is ever mandatory. UI renders an absent optional field as absent, not as an error.
- Amendments are additive and rare. Changing the meaning of an existing field requires a new generation (
manifestVersion: 2), which is an event, not maintenance.
Two artifacts define the contract:
apps/manifest.schema.json— the machine-readable JSON Schema (draft 2020-12). Validate against it with any standard JSON Schema tool. MOS itself interprets this exact file; there is no second hand-written validator to drift from it.- The semantic rules on this page — cross-references and the template grammar, which JSON Schema cannot express. In a repository checkout,
npm run apps:manifest:checkruns both passes over every package (or over folders you name) without running MOS.
Baseline and additions
Section titled “Baseline and additions”The baseline is generation 1 as locked in MOS 0.17.0: every package must set minimumMosVersion to at least 0.17.0, and a manifest using only baseline fields works on every generation-1 release. Everything added since is optional, and a package that uses an addition raises minimumMosVersion to the release that introduced it, so an older MOS refuses the package instead of silently ignoring what it asked for.
| Addition | Since | What it adds |
|---|---|---|
resources.services.<id>.requires | 0.18.0 | Memory and CPU a service needs at rest and at peak, shown to the owner. |
${smtp.*} | 0.19.0 | The owner’s outbound email relay, projected into an app that sends mail. |
appVersion | 0.20.0 | The app’s own version, which owners see instead of the package version. |
routes[].kind | 0.21.0 | api marks an address only programs call: no Open button, no Homepage tile. |
requirements | 0.21.0 | What the app needs from the server, starting with https; Suite Manager disables Install and says why on a server that lacks it. |
A complete minimal manifest
Section titled “A complete minimal manifest”{ "manifestVersion": 1, "id": "example-app", "name": "Example App", "version": "0.1.0", "minimumMosVersion": "0.17.0", "summary": "One plain-language line about what the app does.", "category": "tools", "icon": "icon.png", "resources": { "services": { "example-app": { "dockerfile": "Dockerfile", "internalPort": 8080, "env": { "PUBLIC_URL": "${app.publicUrl}" }, "volumes": ["data:/data"] } } }, "routes": [{ "host": "example-app", "service": "example-app" }], "health": { "type": "http", "url": "http://example-app:8080/healthz" }}That is a working package once the folder also contains the pinned Dockerfile, an icon.png, and (for the official catalog) a privacy-review.json.
Required fields
Section titled “Required fields”| Field | Meaning |
|---|---|
manifestVersion | Always 1. Declares the generation this manifest targets, so a MOS release that does not know it refuses the package instead of misreading it. |
id | DNS-safe lowercase package id (^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$), stable for the life of the package. Names the folder, namespaces containers and volumes. |
name | Human-readable app name. |
version | The package version (semver). Independent of both the MOS platform version and the upstream app version. Any content change requires a bump — installed machines are offered updates purely by comparing this value. |
appVersion | Optional (MOS 0.20.0 and later). The version of the app itself, as it publishes it (3.1.0, 26.8.1). This is the number owners see on the app’s page and in an update preview; version above stays MOS bookkeeping. Keep it in step with the primary service’s pinned base image — for official packages, CI checks it against the privacy review’s component inventory. |
minimumMosVersion | Oldest MOS release the package works on. Raise it when you use a field or template namespace introduced later. |
summary | One-line catalog-card description. |
category | Free text. Reuse an existing category when one fits (media, storage, office, security, tools); an unknown category renders as written with default styling. |
resources.services | At least one service — see below. |
health | How MOS decides the app is up — see below. |
Services (resources.services)
Section titled “Services (resources.services)”Each key is a DNS-safe service id, which is also the container’s hostname on the package-private network.
| Field | Required | Meaning |
|---|---|---|
dockerfile | yes | Package-root Dockerfile: Dockerfile for the primary service, Dockerfile.<service> for others. Base images must be pinned by immutable digest (FROM image@sha256:…) — floating tags are refused at review. |
internalPort | yes | The one TCP port the service listens on. |
env | no | Environment map: UPPER_CASE keys, string values, template references allowed (see the grammar below). |
volumes | no | Persistent storage as <volume-name>:<absolute-container-path>. Named volumes only — host paths, bind mounts, and device paths are refused by design (they break backup, restore, and isolation). Volume names are unique within the package; each volume belongs to exactly one service. |
requires | no | What the service needs to run well — see below. |
MOS does not order service startup. A service must tolerate its dependencies starting later and retry — every mainstream server image already does.
Resource requirements (requires)
Section titled “Resource requirements (requires)”Added in MOS 0.18.0. Optional, display-only, and advisory: MOS applies no cgroup limits from these figures. They exist so an owner can be told whether another app still fits on the server.
"requires": { "cpuCores": 0.25, "memoryMb": 1024, "cpuPeakCores": 2, "memoryPeakMb": 2048 }| Field | Required | Meaning |
|---|---|---|
cpuCores | yes | Cores the service occupies in normal use. Fractional allowed. |
memoryMb | yes | RAM the service holds at rest. |
cpuPeakCores | no | Cores it wants available during heavy work (OCR, transcoding, indexing). |
memoryPeakMb | no | RAM it needs available during heavy work. |
Two rules make the arithmetic honest, so state the figures accordingly:
- Resting figures add up; peak figures do not. A resting figure is a running cost every installed app pays at once. A peak is headroom that only has to be free while that app is busy, so MOS keeps the largest peak rather than the sum. Within one package the services can be busy together, so a package’s own peaks are summed into its total.
- Describe the container, not the project’s recommended server. Upstream “minimum 2 GB RAM” usually means the machine, and often means the peak.
memoryMbis what the container actually occupies idle.
Declare requires on every service of a package or none: a package with figures on only some of its services shows no total, because a partial sum understates it. A peak below its resting figure is rejected.
Routes
Section titled “Routes”"routes": [{ "host": "example-app", "service": "example-app" }]Each route publishes one HTTPS hostname (<host>.<suite-domain>), terminated by MOS and reverse-proxied to the service’s internalPort. Routes are structured data — raw proxy configuration is refused. Generation 1 routes are HTTP(S) only; a future contract for other protocols would arrive as a separate optional field, not a reinterpretation of routes.
kind says who the address is for: web (the default) is a page people open; api answers only programs — a webhook, a trigger, an endpoint another app calls. The Open button and the Homepage tile use the first route, so when it is api Suite Manager shows the address for copying but no Open button, and the package must omit homepage, since a tile would open nothing. kind was added in MOS 0.21.0; a package using it declares minimumMosVersion 0.21.0 or later.
"routes": [{ "host": "scan-bridge", "service": "scan-bridge", "kind": "api" }]Health
Section titled “Health”"health": { "type": "http", "url": "http://example-app:8080/healthz" }type is http in generation 1. The URL’s hostname must be a declared service id — the probe runs on the package network. Future probe types (tcp, …) arrive as new type values gated by minimumMosVersion.
Setup fields (setup.fields)
Section titled “Setup fields (setup.fields)”The install form, as data. Each field:
| Field | Meaning |
|---|---|
id | camelCase id, referenced as ${config.<id>} (non-secret) or ${secret.<id>} (secret). |
type | text, email, password, url, or boolean. Values are stored as strings; boolean stores "true" / "false". |
label | What the owner sees. |
required | Optional boolean. |
secret | Secret values are stored as redacted references and materialized only for runtime apply. A secret field must not declare a default. |
redactedLabel | Label shown in place of a secret’s value. |
default | Prefill for non-secret fields. The only place ${owner.name} / ${owner.email} may appear. |
generated | { "kind": "random", "bytes": 16–128, "encoding": "base64url" | "hex" } — MOS generates the value at install; the owner is never prompted. |
Catalog metadata (catalog)
Section titled “Catalog metadata (catalog)”All display-only. description (the fuller paragraph for the app detail view — the one-line summary is what catalog rows show, and neither substitutes for the other), tags, related (app ids), features ({title, body?}), resourceHint (level: low/medium/high + label/description — the plain-language band, shown alongside the exact figures in resources.services.<id>.requires), privacy (summary, notes[] — the plain-language summary; the bound privacy-review.json is the authoritative assessment), links (website/docs/repository; other keys ignored), demoDeployTargets (public-site deploy links), and:
replaces— an array of product names, one per entry, ranked most-recognised first:["Google Photos", "iCloud Photos", "Amazon Photos", "Flickr"]. List every commercial product the app genuinely stands in for, not only the obvious two — search matches each entry, and the app’s page on this site lists all of them. Space-constrained surfaces (catalog cards, the Suite Manager detail hero) name only the first two, which is why the ranking matters.screenshots—{src, alt?, caption?}wheresrcis a package-relative path listed inpackageFiles. Remote screenshot URLs are refused: browsing the catalog must never fetch third-party origins.
Homepage tile (homepage)
Section titled “Homepage tile (homepage)”group and icon are required when the block is present; name defaults to the package name and description to the summary. Omit the block for packages that should not appear on the dashboard (capability providers usually do), and always when the first route is an api route.
Onboarding guide (onboarding)
Section titled “Onboarding guide (onboarding)”Declarative post-install guidance rendered by Suite Manager: title, summary, and sections[], each {id, type, title, body?, …} with type one of note, warning, steps (with steps: [string]), values (copyable {label, value, copy?} entries), choice-guide (per-device choices[], each with its own steps), manual-complete (with actionLabel).
Guides are data, never behavior: no scripts, no app-specific components, no host mutations. values[].value may interpolate ${app.publicUrl} and non-secret ${config.*}; secrets never appear in a guide — write “use the password you chose during install” instead.
Update expectations (update)
Section titled “Update expectations (update)”What updating to this package version means for an installed machine: backupRequired, breakingChanges (declared structural areas — an undeclared structural change makes the update unofferable), downtime (none/brief/extended/unknown), migrations[], ownerActions[], minimumAppAgentVersion, rollback (safe/not-guaranteed/unsupported).
Server requirements (requirements)
Section titled “Server requirements (requirements)”What the app needs from the server it is installed on. Suite Manager checks each one against this server: the Apps list disables Install and names the missing requirement, and the install, and an update that starts needing it, are refused with the same reason. A requirement Suite Manager cannot judge, for example before the suite has a recorded address, refuses nothing.
https—truewhen the app does not work unless the suite is served over HTTPS: browser crypto, service workers, or clients that refuse a plain-http server. On a suite served over http, such as the Easy Door, the owner is told to serve the suite from their own domain first.
"requirements": { "https": true }requirements was added in MOS 0.21.0; a package using it declares minimumMosVersion 0.21.0 or later. architectures below predates it and is judged the same way.
Other top-level fields
Section titled “Other top-level fields”architectures—["amd64"]and/or["arm64"]. Omitted means unconstrained; incompatible hosts show Install disabled with the reason and refuse the install before building.packageFiles— every extra file the package ships beyond the fixed root set (manifest.json,Dockerfile*,README.md,entrypoint.sh,icon.*,privacy-review.json). Undeclared files do not survive packaging.icon— package-relative icon path, conventionallyicon.png.
The template grammar
Section titled “The template grammar”Manifest strings may reference values MOS resolves at install or runtime:
${namespace.path}A reference is recognized only when the namespace is a lowercase word followed by a dot. Anything else — ${UPPER_CASE}, ${no-dot}, $plain — is literal text and passes through untouched, so shell-style ${VAR} syntax in env values keeps working.
| Namespace | Resolves to | Allowed in |
|---|---|---|
${config.<fieldId>} | A non-secret setup field’s value | Service env, onboarding values[].value, provisional areas |
${secret.<fieldId>} | A secret setup field’s value | Service env and provisional areas only — never onboarding, never catalog |
${app.host} / ${app.scheme} / ${app.publicUrl} | The app’s public hostname, scheme, and base URL — publicUrl always ends in / | Service env, onboarding values[].value, provisional areas |
${owner.name} / ${owner.email} | The suite owner’s profile | setup.fields[].default only |
${smtp.*} | The owner’s shared outbound email relay | Service env and provisional areas |
${import.*} / ${export.*} | Capability wiring | The provisional capability system only |
Because ${app.publicUrl} ends in /, it concatenates cleanly with a path (${app.publicUrl}welcome/) but is the wrong value for a variable that wants a bare origin — some servers reject an origin carrying a path and refuse to start. Compose those as ${app.scheme}://${app.host}.
The SMTP relay namespace
Section titled “The SMTP relay namespace”${smtp.*} projects the single outbound email relay an owner configures once in Settings → Email relay into any app that sends mail. The keys are host, port, username, password, fromAddress, fromName, and the encryption in whichever shape the app wants: security (the word none | starttls | tls, for an app that takes one string) or the pair startTls and implicitTls (each true | false, for an app that takes two booleans like Django’s EMAIL_USE_TLS / EMAIL_USE_SSL). All are derived from the owner’s single choice, so no app’s own vocabulary leaks into MOS. An app opts in simply by referencing them in its service env — MOS never sends the relay to an app that does not:
"env": { "MAILER_HOST": "${smtp.host}", "MAILER_PORT": "${smtp.port}", "MAILER_USER": "${smtp.username}", "MAILER_PASS": "${smtp.password}", "MAILER_FROM": "${smtp.fromAddress}"}${smtp.configured} is true only when a relay is set; use it for an app with an explicit on/off switch. ${smtp.allowInvalidCert} is true when the owner accepted a relay with an untrusted TLS certificate, so an app connecting to the same relay can make the same choice (its own ignore-cert / accept-invalid-certs flag) instead of failing where MOS succeeded. When no relay is configured every other key resolves to the empty string — never the literal ${smtp.host} text — so an app that gates on a non-empty host simply stays off. An app whose settings are written by its own package entrypoint should guard on a non-empty ${smtp.host} before writing them.
${smtp.password} is secret-grade: like ${secret.*} it resolves only when a runtime is materialized, never enters a stored projection or a package digest, and is redacted from logs and diagnostics. Because the relay resolves at materialize time, changing it does not alter any app’s digest — installed apps pick up a changed relay the next time they are updated or restarted. The namespace was added in MOS 0.19.0; a package that references it must set minimumMosVersion to 0.19.0, so an older MOS refuses it rather than shipping a literal ${smtp.host} into a container.
Every reference is validated. A typo like ${config.adminUserName} fails validation instead of shipping verbatim into a container env var and failing silently on someone else’s machine. An unknown namespace is an error too — a still-future namespace is reserved and arrives gated by minimumMosVersion.
Provisional areas — outside the lock
Section titled “Provisional areas — outside the lock”These work today for official packages but their shape is not frozen; avoid them in external packages unless you accept migration later:
role(capability-provider),exports,integrations,configTargets,usefulness— the capability system. Proven by exactly one relationship (Seafile ⇄ ONLYOFFICE); it will be locked when more relationships have shaped it.homepage.widget— only the monthly calendar widget exists; a general widget contract is future work.routes[].internalIcalBridge— a token-gated read-only proxy path, expected to be replaced by a general bridge contract.
What stays out — permanently
Section titled “What stays out — permanently”These are refused by design, not omissions to work around: host paths and bind mounts, cross-package shared volumes, host networking, device passthrough, privileged containers, the Docker socket, raw Caddy/proxy directives, arbitrary scripts in guides, and OIDC/LDAP/SSO wiring (MOS is deliberately single-owner). If your app cannot be expressed without one of these, that is a platform conversation — open an issue rather than bending the package.
Validating a package
Section titled “Validating a package”# both passes (structure + semantics), no running MOS required:npm run apps:manifest:check # every apps/<app>/npm run apps:manifest:check -- path/to/pkg # specific folder(s)Or validate structure alone against apps/manifest.schema.json with any JSON Schema validator. Semantic rules the schema cannot express (and the checker enforces): template references resolve against declared fields and namespaces; routes[].service and the health hostname name declared services; declared package files exist; screenshots are declared in packageFiles; secret fields carry no defaults; volume names are unique per package.
Amendment policy
Section titled “Amendment policy”Recorded in the repository’s decision log and agent rules: a manifest change must be an optional, additive field, listed under Baseline and additions with the release that introduced it; the UI must render its absence as absence; a change to an existing field’s meaning requires a new generation; and reintroducing a closed allow-list anywhere in the manifest shape is a regression (a unit test fails if anyone tries).