The manifest
pithyx.json at the package root describes an app: who it is, which window it opens, which permissions it asks for, which folders it wants and, if it has one, how its backend runs. The JSON Schema is sdk/schema/manifest.v1.schema.json (published as @pithyx/sdk/schema/manifest.v1.schema.json); unknown fields are errors. The schema is a stable contract: a new optional field is a minor change, anything else a major one.
Required fields: manifestVersion, id, version, name, author, entry, sdk, permissions and window. Optional: $schema, description, license, homepage, pithyx, dataVersion, storage, fileHandlers and backend. See also the schema reference with every field and the Compose rules for the backend.
{ "$schema": "./node_modules/@pithyx/sdk/schema/manifest.v1.schema.json", "manifestVersion": 1, "id": "com.example.notes", "version": "1.0.0", "name": { "en": "Notes", "de": "Notizen" }, "author": { "name": "Example Author" }, "entry": "frontend/index.html", "sdk": "^1.0.0", "permissions": ["notifications"], "window": { "instances": "single", "defaultSize": { "width": 800, "height": 600 }, "minSize": { "width": 400, "height": 300 } }}Identity
Section titled “Identity”manifestVersion: the number1.id: see App ids.version: SemVer 2.0 without build metadata, at most 64 characters (1.2.0,2.0.0-beta.1).name: an object keyed by language tag (en,de,pt-BR).enis required; each text is 1 to 60 characters on one line. The desktop uses the user’s language, then its base language, thenen.description: optional, same keys, 1 to 500 characters, line breaks allowed.author:name(1 to 100 characters) and an optionalurl(http or https).license: optional SPDX expression. Catalogs require it.homepage: optional http or https URL.entry: the HTML page,frontend/<path>.html, at most 200 characters, no segment starting with a dot.sdk: required npm-style version range for the SDK, for example^1.0.0. The box checks its syntax (app.manifest.sdk_invalid); it does not compare it with the desktop’s protocol today. With a release candidate, declare the version you installed, for example^1.0.0-rc.8: that range also covers 1.0.0 and every later 1.x, while^1.0.0would not match a candidate.pithyx: optional range of box versions the app works with. The box installs the app only if its own version is inside. Catalogs require it. Prerelease versions match by plain SemVer ordering, so>=0.1.0includes0.2.0-beta.1, but^0.1.0does not.dataVersion: optional integer from 1 to 2147483647. A catalog downgrade is allowed only between versions with the samedataVersion. A rollback to the previous version after an update does not depend on it.
Windows
Section titled “Windows”window is required:
instances:singlefocuses the user’s existing window on this device and passes launch arguments;multiopens a new window each time.defaultSizeandminSize:widthandheightas integers from 100 to 8192 CSS pixels.minSizemay not be larger thandefaultSizein either direction. The default size is used when the user has no remembered size; the minimum applies on screens that can fit it.loading(optional):text, the line a new window shows while the backend starts, localized likename(up to 60 characters; without it the desktop says “Starting…”), and untilReady, whether the placeholder waits forapp.window.ready()after the app connected. See Events and context.
fileHandlers (optional, up to 32) makes the app appear under “Open with” for file types. Each handler has extensions (1 to 32 lowercase extensions without a dot, such as png or tar.gz), and optionally mimeTypes (1 to 32) and isDefault. An extension can belong to one handler only. The file reaches the app through the launch or openFile event.
Permissions
Section titled “Permissions”permissions is a required list (it can be empty) of unique values from this set:
| Permission | Lets the app |
|---|---|
files.read |
read files the user opens with it or picks in a desktop file dialog |
files.write |
write those files and save new files through the desktop save dialog |
files.shares |
browse every shared folder the user may access, with the user’s own rights |
notifications |
show notifications |
directory.read |
list all users and groups |
network.internet |
make outgoing connections to the internet from its window (connect-src https:) and its backend |
network.lan |
make outgoing connections from its backend to other devices in the local network (private, shared and link-local addresses); never to the box itself |
An administrator confirms the list at install and can revoke each permission later; the app then receives a permissionsChange event. A permission that is new in an update waits for the administrator, and a revoked permission stays revoked across updates. Calling something without the permission fails with app.permission.denied.
An app that asks for directory.read, runs a container as root (backend.root) or asks for special rights (backend.elevated) is suggested for administrators only: the install dialog preselects Administrators only and says why. The administrator can still let everybody use it.
storage (optional, up to 16) asks for folders. Each request has an id, a name, an optional description, access (read or readWrite), required and, for backends, services (the services that get the folder). An administrator maps each request to a folder in a shared folder. The window addresses it as { storage: id } in the SDK file API; a backend service sees a Compose volume with that name. An app cannot start until its required requests are mapped.
Backend
Section titled “Backend”backend is optional. Required inside it: compose, endpoints and lifecycle.
compose: path of the Compose file in the package, usuallybackend/compose.yaml.endpoints: an object from endpoint name to{ "service": "...", "port": 8080 }, up to 16. The endpointapiis required and is the default. Names match^[a-z][a-z0-9-]{0,31}$, so at most 32 characters; that is the limit the box enforces. The SDK accepts longer names, but the box would refuse them.port(1 to 65535) is the container’s port inside the app network and is never published.lifecycle:alwaysoronDemand.idleTimeout: seconds from 60 to 86400, default 600; ignored foralways.root:trueruns the services as root inside the container (unprivileged on the host); defaultfalse. The install dialog warns. Services without root lose every Linux capability.network:{ "services": ["fetcher"] }names the services that get the network out of the box whichnetwork.internetandnetwork.lanopen; the others stay on the app’s internal network. Without it every service gets it. Each name must be a service of the Compose file (app.compose.reference_unknown).health:{ "endpoint": "api", "path": "/health" }. The daemon callsGETon it; a 2xx answer means the backend is running.schedules: up to 16 entries of{ id, cron, endpoint, path }. The daemon starts the backend and callsPOST pathwithX-Pithyx-Schedule(see Who is calling). The cron expression has five fields (minute 0 to 59, hour 0 to 23, day of month 1 to 31, month 1 to 12, weekday 0 to 7) in the box’s time zone, with*, numbers, ranges, lists and steps; names are not allowed (app.manifest.cron_invalid).elevated: a list of extra rights (host.network,host.pid,device:/dev/<path>,capability:<NAME>,docker.socket,host.full), up to 32. Elevated rights are not granted yet: the box in its first release (Pithyx 1.0.0, not the version of the npm packages) refuses every entry withapp.elevated.unsupported.
Beyond the schema the box checks that service and endpoint names exist (app.manifest.reference_unknown) and that the storage requests match the Compose mounts.
Lifecycle
Section titled “Lifecycle”lifecycle decides when the backend runs. always starts it whenever it can run (the app is enabled, the pool is available, required folders are mapped). onDemand runs it while a window is open or a request or schedule holds it, and stops it idleTimeout seconds after the last one. The details are on Backends.
Versions
Section titled “Versions”The box compares the installed and the uploaded version and shows install, update, reinstall or downgrade. Reinstalling the same version replaces its files; installing an older one warns that its data may not fit. The box keeps the previous version and a snapshot of the app’s data until the next update so that an administrator can roll back. In a catalog, a published version is never changed or removed.