@serve.zone/appstore
The serve.zone App Store: the curated app templates, the publisher that turns them into one signed runtime catalog, and the client Onebox and Cloudly use to load that catalog at runtime.
A consumer never bundles App Store data. It loads the signed catalog from an npm registry in the background, verifies it against the signing keys its own release trusts, keeps the last verified catalog as its last-known-good copy, and picks up new apps and versions without a release of its own. The App Store is never a startup blocker: while no verified catalog is active, only App Store install, upgrade and upgrade-preview are refused.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install
pnpm add @serve.zone/appstore
The client speaks the @serve.zone/interfaces 32 App Store contracts. The storage feature IDs, delivery formats, and index and manifest shapes documented below are the 32 spellings; nothing older is accepted under a second name.
What Is In This Repo
| File or directory | Purpose |
|---|---|
apps/<app>/app.json |
The app's catalog info (id, name, description, category, optional iconName, iconUrl, tags, maintainer, links), its latestVersion and the versions it publishes, in order. |
apps/<app>/versions/<version>/config.json |
The version config: digest-pinned image, ports, environment, storage, platform requirements, compatibility minimums and upgrade flags. |
apps/<app>/versions/<version>/withdrawn.json |
Present once a version is withdrawn: { "at", "reason" }. A withdrawal is final. |
catalog.lock.json |
The publication record: every published app@version with its config digest and withdrawal. It only grows. |
ts_publisher/ |
The catalog publisher (not published): builds dist_catalog/, signs it, maintains the lock. |
ts_client/ |
The published @serve.zone/appstore client: the catalog loader, catalog lookups and the storage handoff. |
dist_catalog/ |
Build output, published with the package: catalog.json, catalog.digest and, in a release, the signed catalog.envelope.json. |
Nothing else may sit in an app or version folder; the publisher refuses any other file.
Current App Store
The catalog lists 19 apps. Every image is pinned to the digest of its multi-platform index.
| App ID | Name | App Store category | Latest | Image | Port |
|---|---|---|---|---|---|
adminer |
Adminer | Dev Tools | 1.0.0 |
adminer@sha256:74f29c416e148b98305e84446a18db7cd2c1038264dec464359d36774ef080ce |
8080 |
cloudly |
Cloudly | Dev Tools | 1.30.9 |
code.foss.global/serve.zone/cloudly@sha256:b5b1d9f0ba494d4e8472f141c03b7b76a8590d256debfd9c144f1eb866c18069 |
3000 |
ghost |
Ghost | CMS | 1.0.0 |
ghost@sha256:9482099b1c8700764dc3d38c293e09c16d70f8bacc14af589c47d90b4bebd017 |
2368 |
gitea |
Gitea | Dev Tools | 1.0.0 |
gitea/gitea@sha256:a9dc25679ebdd33356242d2e0667cc220043745e8339922879f187c20329a42e |
3000 |
grafana |
Grafana | Monitoring | 1.0.0 |
grafana/grafana@sha256:b28bae15e219c998fb0e0424ed724930cc61b1f61fb404d47c862f9a23f9e572 |
3000 |
mariadb |
MariaDB | Database | 1.0.0 |
mariadb@sha256:f1bba652ba57bea3099ca2fe1af692af537c27d96e0bcde39dce29e2ba1ec4f3 |
3306 |
mattermost |
Mattermost | Communication | 1.0.0 |
mattermost/mattermost-team-edition@sha256:6ad5912b45875ef2fafc41886b5a70a3ec1b6c17ec706e0492566e8a2fd8aa85 |
8065 |
n8n |
N8N | Automation | 1.0.0 |
n8nio/n8n@sha256:240eaa2a3d491adac5817aa4c3f1c521bb18ec79f6e448517158d5220ee0f37b |
5678 |
nextcloud |
Nextcloud | Storage | 1.0.0 |
nextcloud@sha256:4ba2af4695e1801b5f66ed4df863c0fa0f236f632ffd04b2fdc32ee0c2fefe42 |
80 |
nginx |
Nginx | Web Server | 1.0.0 |
nginx@sha256:df221db836e1754089190208cee7eeda94f233197056426eda74a43ab1abeac2 |
80 |
openbao |
OpenBao | Security | 1.0.0 |
openbao/openbao@sha256:900bb64d0671cd1d82b693c56206f7263b582445f3a3bb6ba6e5213f524a6653 |
8200 |
plausible |
Plausible Analytics | Analytics | 1.0.0 |
plausible/analytics@sha256:cd5f75e1399073669b13b4151cc603332a825324d0b8f13dfc9de9112a3c68a1 |
8000 |
portainer |
Portainer | Dev Tools | 1.0.0 |
portainer/portainer-ce@sha256:4d616db18cfeb5dd41a69c0958bc825c84483ea9cde1106eb82a5d26f3bd8b0e |
9000 |
postgres |
PostgreSQL | Database | 1.0.0 |
postgres@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea |
5432 |
rustdesk-server |
RustDesk Server | Remote Access | 1.0.0 |
rustdesk/rustdesk-server-s6@sha256:5974cd791a5aca6fbf01a167b37e7427db1d136e95b2710c33a34fdc9fea724c |
21116 |
uptime-kuma |
Uptime Kuma | Monitoring | 1.0.0 |
louislam/uptime-kuma@sha256:70233f4acb5163fd2a59a49909cf01e44415cecff13dba69e3a86647a2919f83 |
3001 |
valkey |
Valkey | Database | 1.0.0 |
valkey/valkey@sha256:c9b77919daeba2c02ad954d0c844cc4e7142069d177b89c5fd771f405daf9e02 |
6379 |
vaultwarden |
Vaultwarden | Security | 1.0.0 |
vaultwarden/server@sha256:efb3cde962015fcc036b2ea625242248611943b27212ebf4392841fb30fad055 |
80 |
wordpress |
WordPress | CMS | 1.0.0 |
wordpress@sha256:736761c95c327cb21602ab9175ee4dba6e779b00f3f4895536a3e1af5ac4fe4c |
80 |
Cloudly 1.0.0 and 1.1.0 are withdrawn: they were floating-tag templates (cloudly:latest) and stay listed, pinned to the image that tag served when the catalog was created, only so that services installed from them keep reading their config. Install 1.21.0 or later.
Platform Requirements
Some templates ask Onebox to provision local platform services and inject connection variables into the workload.
platformRequirements accepts the optional booleans mongodb, s3, clickhouse, valkey, mariadb, and mail. Unknown keys and non-boolean values fail validation.
mail: true requests a platform mail binding. Onebox provisions a CoreMail binding for the app and injects MAIL_COREMAIL_URL, MAIL_COREMAIL_BINDING_ID, MAIL_COREMAIL_CREDENTIAL_ID, MAIL_COREMAIL_CREDENTIAL_VERSION, MAIL_COREMAIL_CREDENTIAL_SECRET, MAIL_FROM, SMTP_HOST, SMTP_PORT, SMTP_TLS_MODE, SMTP_USERNAME, and SMTP_PASSWORD. Those identities are platform-owned, so a template must not declare them itself.
| App ID | Requirements | Notes |
|---|---|---|
cloudly |
MongoDB, S3 | Versions 1.30.7 and 1.29.11 run the Cloudly 33.24.0 image, which adds the dashboard's Temp tokens view (Security, Temp tokens: mint, list and revoke delegated admin temp tokens, so an administrator who signs in with a passkey through Onebox mints one without a Cloudly password; the token is shown once and never stored). It adds no data step, no environment variable, no setting, no collection and no index (data version 0.33.0). Version 1.29.11 is 1.29.10 with that image and is the newest target of a host that does not fulfil storage.database.backup-exclusions; version 1.30.7 is 1.29.11 with the ledger declaration of 1.30.0 and is the upgrade target of 1.30.6, 1.30.5, 1.30.4, 1.30.3, 1.30.2, 1.30.1, 1.30.0, 1.29.11, 1.29.10, 1.29.9 and every version 1.30.0 upgrades from; both keep the Onebox minimum 33.8.0 and the four upgrade flags (an upgrade from 1.29.8, 1.30.4 or earlier runs the forward-only step to 0.33.0). Versions 1.30.6 and 1.29.10 run the Cloudly 33.23.2 image, a fixes-only patch over 33.23.0 that takes 33.23.1: a service whose stored plan, rollout status or newest deployment operation violates the contract gets a failed composition status that names it instead of failing the services list, and servezone service credential ensure takes over a legacy operator-managed SERVEZONE_PLATFORM_AUTHORIZATION (it revokes the legacy versions, not the legacy user token). It adds no data step, no environment variable, no setting, no collection and no index (data version 0.33.0). Version 1.29.10 is 1.29.9 with that image and is the newest target of a host that does not fulfil storage.database.backup-exclusions; version 1.30.6 is 1.29.10 with the ledger declaration of 1.30.0 and is the upgrade target of 1.30.5, 1.30.4, 1.30.3, 1.30.2, 1.30.1, 1.30.0, 1.29.10, 1.29.9 and every version 1.30.0 upgrades from; both keep the Onebox minimum 33.8.0 and the four upgrade flags (an upgrade from 1.29.8, 1.30.4 or earlier runs the forward-only step to 0.33.0). Versions 1.30.5 and 1.29.9 run the Cloudly 33.23.0 image, which adds the forward-only data step retire-stale-reconciliation-stages (data version 0.32.0 to 0.33.0): a service's status is what the producers Cloudly runs state about it (the runtime's verdict per target, the current rollout, the newest deployment operation and the observers' records), the Services list and the Overview show that one status, and the step deletes the reconciliation records nothing reports again (the Swarm-era Coreflow kinds and the externalGateway records of routes their service no longer declares). The step has no way back: Cloudly 33.22.0, 33.21.4 and earlier refuse data version 0.33.0, so a return to 1.29.8, 1.30.4 or an earlier template means restoring the database backup taken before the upgrade. Cloudly 33.23.0 also adds delegated admin temp tokens and moves to @serve.zone/interfaces 32.61.0, and takes Cloudly 33.22.0 (the scheduled backup task backs up each service on its own node and cluster; servezone service credential). Version 1.29.9 is 1.29.8 with that image (CLOUDLY_BACKUP_CRON kept) and is the newest target of a host that does not fulfil storage.database.backup-exclusions; version 1.30.5 is 1.29.9 with the ledger declaration of 1.30.0 and is the upgrade target of 1.30.4, 1.30.3, 1.30.2, 1.30.1, 1.30.0, 1.29.9, 1.29.8 and every version 1.30.0 upgrades from; both keep the Onebox minimum 33.8.0. Versions 1.30.4 and 1.29.8 run the Cloudly 33.21.4 image, a fixes-only patch over 33.21.3 (a launcher-environment secret file is owned by the identity the workload runs as, AGL-459; @serve.zone/interfaces 32.60.0), and set CLOUDLY_BACKUP_CRON to 15 2 * * *, so Cloudly registers its backup-all-services task and backs up the databases of its workload services daily at 02:15 UTC, keeping the newest 24 backups of each service; a workload's object storage is not part of these backups (AGL-473). Version 1.29.8 is 1.29.7 with that image and variable and is the newest target of a host that does not fulfil storage.database.backup-exclusions; version 1.30.4 is 1.29.8 with the ledger declaration of 1.30.0 and is the upgrade target of 1.30.3, 1.30.2, 1.30.1, 1.30.0, 1.29.8, 1.29.7 and every version 1.30.0 upgrades from. Cloudly 33.21.4 adds no data step, no collection, no index and no setting and keeps the Onebox minimum 33.8.0. Versions 1.30.3 and 1.29.7 run the Cloudly 33.21.3 image, which adds the forward-only data step retire-corestore-environment-secrets (data version 0.31.0 to 0.32.0): a service migrated from Swarm reaches its object storage at its node's Corestore endpoint again, because the step retires the service-owned secrets in scope platform:cloudly-corestore that the Secrets v2 cutover made of the nine object-storage endpoint names and the eleven S3_RETENTION_* names, which still named the Docker host corestore and replaced the binding's plain environment in the workload, and publishes each affected service's manifests again, so its replicas are replaced once. The step has no way back: Cloudly 33.21.2 and earlier refuse data version 0.32.0, so a return to 1.29.6, 1.30.2 or an earlier template means restoring the database backup taken before the upgrade. Version 1.29.7 is 1.29.6 with that image and is the newest target of a host that does not fulfil storage.database.backup-exclusions; version 1.30.3 is 1.29.7 with the ledger declaration of 1.30.0 and is the upgrade target of 1.30.2, 1.30.1, 1.30.0, 1.29.7, 1.29.6 and every version 1.30.0 upgrades from. Cloudly 33.21.3 adds no collection, no index and no setting and keeps the Onebox minimum 33.8.0. Versions 1.30.2 and 1.29.6 run the Cloudly 33.21.2 image, a fixes-only patch over 33.21.1: a recovery redeploy over a dead retained rollout plan is no longer refused by its reserve and promote preflights while its Corestore bindings wait for the credentials only a promotion can bring, since those preflights now defer the readiness of such a binding (PLATFORM_BINDING_READINESS_DEFERRED) as they did for a held service's first promotion, and the route preflight still requires it ready. Version 1.29.6 is 1.29.5 with that image and is the newest target of a host that does not fulfil storage.database.backup-exclusions; version 1.30.2 is 1.29.6 with the ledger declaration of 1.30.0 and is the upgrade target of 1.30.1, 1.30.0, 1.29.6, 1.29.5 and every version 1.30.0 upgrades from. Cloudly 33.21.2 adds no data step, no collection, no index and no setting, keeps the data version 0.31.0 and the Onebox minimum 33.8.0. Versions 1.30.1 and 1.29.5 run the Cloudly 33.21.1 image, which carries Cloudly 33.21.0 and its fixes: a greenfield reserve creates its Corestore bindings, a held service's binding waits for its first promotion, the gateway domain sync keeps the operator's activation and records the gateway listing in gatewayListedAt, a dead retained rollout plan is superseded at promotion, and a Pallet service that still carries its Swarm-era placement is granted its Corestore credentials again. Version 1.29.5 is 1.29.4 with that image and is the newest target of a host that does not fulfil storage.database.backup-exclusions; version 1.30.1 is 1.29.5 with the ledger declaration of 1.30.0 and is the upgrade target of 1.30.0, 1.29.5 and every version 1.30.0 upgrades from. Cloudly 33.21.1 adds no data step, no collection and no index, keeps the data version 0.31.0 and the Onebox minimum 33.8.0. Version 1.30.0 also declares Cloudly's runtime log ledger non-backup data: databaseBackupExclusions names the collections cloudly_runtime_log_attempts, cloudly_runtime_log_batches, cloudly_runtime_log_captures, cloudly_runtime_log_inline_payloads, cloudly_runtime_log_node_discards and cloudly_runtime_log_queue of its mongodb database, and requiresFeatures names storage.database.backup-exclusions beside storage.object-storage.backup-exclusions, so a host that does not fulfil the database feature lists it, refuses it as a target and keeps 1.29.7 as its newest; 1.30.1, 1.30.2 and 1.30.3 carry the same declaration. A backup on a host that fulfils it exports those collections with their indexes and without their documents, so a restore brings them back empty; every Cloudly 33.21.0 start re-records the log attempts that still run. Version 1.30.0 is 1.29.4 with the Cloudly 33.21.0 image and the ledger declaration and is the upgrade target of 1.29.4, 1.29.3, 1.29.2, 1.29.1, 1.29.0, 1.28.6, 1.28.5 and 1.28.3; Cloudly 33.21.0 adds no data step, no collection and no index and keeps the data version 0.31.0. Versions 1.30.3, 1.30.2, 1.30.1, 1.30.0, 1.29.7, 1.29.6, 1.29.5, 1.29.4, 1.29.3, 1.29.2, 1.29.1, 1.29.0, 1.28.6 and 1.28.5 declare Cloudly's workload logs non-backup data: backupExclusions names the key prefix runtime-logs/ of the legacy-s3 bucket, and requiresFeatures names storage.object-storage.backup-exclusions, so a host that does not fulfil it lists them and refuses them as targets. A backup on a host that fulfils it leaves those objects out, and a restore brings back Cloudly's state, not its past workload logs. Version 1.29.4 is 1.29.3 with the Cloudly 33.20.4 image and is the upgrade target of 1.29.3, 1.29.2, 1.29.1, 1.29.0, 1.28.6, 1.28.5 and 1.28.3. Cloudly 33.20.4 keeps the runtime log sweep bounded by its own work: its reads no longer scan the whole runtime log ledger, each sweep step commits at most 16 rows under a 15 s deadline instead of retrying past the database's transaction lifetime, and each step first lets critical node requests (such as the managed VPN credential renewal) through, so a large ledger no longer stalls node credentials. It adds no data step, no collection and no index and keeps the data version 0.31.0, so an upgrade from 1.29.3 or 1.29.2 runs none. Version 1.29.3 is 1.29.2 with the Cloudly 33.20.3 image and is the upgrade target of 1.29.2, 1.29.1, 1.29.0, 1.28.6, 1.28.5 and 1.28.3. Cloudly 33.20.3 lets a service that already holds a Corestore binding of a capability adopt its own legacy resource of that capability into that binding: Corestore 34.1.4 and later refuse a binding beside an unauthorized legacy resource (legacy-adoption-pending), and adoptCorestoreLegacyResources refused such a service binding-exists, so the service stayed down; now the adoption is written onto the matching enabled canonical binding in the adoption's transaction, and every other binding still refuses binding-exists. It adds no data step and no collection and keeps the data version 0.31.0, so an upgrade from 1.29.2 runs none; an upgrade from 1.29.1 or earlier also takes the changes of Cloudly 33.20.2 below. Version 1.29.2 is 1.29.1 with the Cloudly 33.20.2 image and is the upgrade target of 1.29.1, 1.29.0, 1.28.6, 1.28.5 and 1.28.3. Cloudly 33.20.2 keeps its registry storage bounded: a storage retention runs 3 minutes after start, then daily, prunes releases no plan, running tag, deployment operation or unfinished rollout needs beyond each service's 3 most recently promoted accepted releases and the last 7 days, and deletes manifests and blobs nothing reaches after 24 hours, in time-bounded holds of the registry mutation lock; the per-push prune is gone, and a service deletion deletes only its own repository's manifests and tags. A rollout of what a replica already runs keeps it running, and gateway, mail and CoreMail status reported under a service lifecycle lease is kept, one record per observer in cloudly_service_reconciliation_stages. Its startup runs the forward-only data step service-reconciliation-observations from 0.30.0 to 0.31.0; every Cloudly before 33.20.2 refuses 0.31.0, so a return to 1.29.1 or earlier means restoring the backup taken before the upgrade, and registry objects the retention deleted do not come back with it. An upgrade from 1.29.0 or earlier also takes the changes of Cloudly 33.20.1 below. Version 1.29.1 is 1.29.0 with the Cloudly 33.20.1 image and is the upgrade target of 1.29.0, 1.28.6, 1.28.5 and 1.28.3. Cloudly 33.20.1 no longer blanks a cluster's ingress routes when it restarts: its first table of a cluster is the full one, never an empty revocation, a node that only registers again keeps its routes because a route endpoint is a node instance's statement under its credential generation (nodeInstanceNonce), unreadable route sources keep the relay's table for at most 180 s after a start, and a revocation withdraws exactly the revoked routes. A Pallet registration Cloudly cannot serve for a reason of its own is refused as momentary (controller-unavailable, controller-deadline; @serve.zone/interfaces 32.57.0), so the node registers again instead of ending its session, and a credential check whose store cannot be read fails closed. It adds no data step and no collection and keeps the data version 0.30.0, so an upgrade from 1.29.0 runs none; an upgrade from 1.28.6 or earlier also takes the changes of Cloudly 33.20.0 below. Version 1.29.0 is 1.28.6 with the Cloudly 33.20.0 image and is the upgrade target of 1.28.6 and 1.28.5. Cloudly 33.20.0 serves a non-public hostname from a node's cluster ingress to dcrouter alone once the setting dcrouterForwardSourceAddresses names dcrouter's forwarding sources (the cluster's Coreflow and CoreTraffic must run releases on @serve.zone/interfaces 32.55.0 or later), writes workload log payloads as one segment object per capture every few minutes, keeping a committed batch inline in the new collection cloudly_runtime_log_inline_payloads until its segment is written, and lets a service whose retained rollout plan cannot run be redeployed (RECOVERY_REDEPLOY_OVER_FAILED_RUNTIME). It adds no data step and keeps the data version 0.30.0, so an upgrade from 1.28.6 runs none; its first start creates that collection, which after an unclean stop of the previous Cloudly fails with EFENCE_BUSY for up to 30 minutes on Corestore 32.2.4. Cloudly 33.19.2 and earlier refuse the log ledger rows 33.20.0 writes, so a return to an earlier template needs the backup taken before the upgrade. 1.28.6 is 1.28.4 (Cloudly 33.19.2) with that declaration; 1.28.5 is 1.28.3 (Cloudly 33.19.1) with it, so a host on 1.28.3 moves its template without moving Cloudly. 1.28.5 sorts above 1.28.4 but runs the older image: from 1.28.4, upgrade to 1.28.6. Version 1.28.4 pins Cloudly 33.19.2 and keeps the 1.28.3 runtime contract, including the Onebox 33.8.0 minimum: its platform Corestore 32.2.4 runs NoSQLDB 10.6.1 with the D3 write-conflict fix, and on an earlier engine a node's network plan loses every commit to a WriteConflict, so Cloudly 33.15 must not run on it. Cloudly 33.19.2 is a fixes-only patch over 33.19.1: a greenfield deployment, a legacy import's successor included, proves its namespace blank on the nodes of the Pallet clusters only, so a fleet that still keeps a Coreflow-owned Swarm cluster without a relay can reserve one, while a Pallet node whose inventory cannot be read still blocks; a greenfield rollout without a Pallet runtime spec is refused GREENFIELD_PALLET_RUNTIME_SPEC_REQUIRED instead of falling back to Coreflow placement (state the spec with setServiceRuntimeSpec, then promote again); and a node's network compilation proves each workload lease and each authority or eligibility proof once per transaction. It adds no data step and keeps the data version 0.30.0, so an upgrade from 1.28.3 runs none. The 1.28.4 changelog carries the changes of 33.19.2 and of 33.18.3 to 33.19.1 and the earlier releases' upgrade path only, because a catalog changelog is bounded at 65536 characters; 1.28.2's carries the rest. From 1.28.2 or earlier, Cloudly 33.19.1's changes apply too: Cloudly 33.19.1 carries 33.18.3 to 33.19.1: a node's log report costs one append transaction that queues the batch with its payload in the new collection cloudly_runtime_log_queue, and a background drain writes the payloads and commits the ledger rows without holding the node's request or fencing the controller record (a full queue refuses a report with the retryable log-intake-saturated), object writes list nothing, workload logs are kept for at least 30 days, route verification reads the certificate each route presents on a connection of its own, and readServiceDeploymentStatus (interfaces 32.54.0) reads a deployment operation without changing it. Its first start creates cloudly_runtime_log_queue; on NoSQLDB 10.6.1 that creation waits at most 5 s for the open transactions on the Cloudly database, and a transaction an earlier Cloudly left open by exiting before its stop completed holds the database until its session expires 30 minutes later, so after an unclean stop of the previous Cloudly start the upgrade no earlier than 30 minutes after that exit. It adds no data step and keeps the data version 0.30.0, so an upgrade from 1.28.2, 1.28.1 or 1.28.0 runs none. From 1.28.1 or earlier, Cloudly 33.18.2's fixes apply too: Cloudly 33.18.2 is a fixes-only patch over 33.18.1: a node's network configuration advances only once the node's application receipt names the projection it holds, so a node no longer receives a new version while it still applies the last one; upgrade every node to Pallet 36.3.0 or later (interfaces 32.50.0 or later) first, because a node that sends no receipt keeps the projection it holds and receives no successor. It adds no data step and keeps the data version 0.30.0, so an upgrade from 1.28.1 or 1.28.0 runs none. From 1.28.0 or earlier, Cloudly 33.18.1's fixes apply too: Cloudly 33.18.1 is a fixes-only patch over 33.18.0: a node's log batches append without its node fences, so they no longer miss their budget against the node's producer passes, and the deployment preflight answers within 25 s, past which it answers the retryable DEPLOYMENT_PREFLIGHT_UNAVAILABLE. It adds no data step and keeps the data version 0.30.0, so an upgrade from 1.28.0 runs none. From 1.27.0 or earlier, Cloudly 33.18.0's changes apply too: Cloudly 33.18.0 loads the App Store catalog at runtime instead of bundling it, from the registries of its appStoreCatalogRegistries setting (empty means the npm registry), so a Cloudly template update needs an appstore release only; it ignores APPSTORE_URL, whose remote source is removed, and while no verified catalog is active it refuses App Store install details, upgrade preview and upgrade with APPSTORE_CATALOG_UNAVAILABLE. It adds no data step and keeps the data version 0.30.0, so an upgrade from 1.27.0 runs none. From 1.26.0 or earlier, Cloudly 33.17.3's changes apply too: Cloudly 33.17.3 moves the data version from 0.28.0 to 0.30.0: every upgrade from 1.26.0 or earlier runs the forward-only data steps route-claim-absent-owners to 0.29.0 (the stored nulls of absent owners are removed from every route claim) and bounded-runtime-history to 0.30.0 (every archived deployment record is deleted, the index archiveState_1 is dropped and the accepted Spark observations are pruned to the rows the census reads), and every Cloudly before 33.17.3 refuses the 0.30.0 data. Runtime history is bounded from then on: a Coreflow report deletes the deployment records it no longer names instead of archiving them, getDeployments answers the current records whether or not it is asked to includeArchived, deploymentArchiveRetentionDays is accepted but has no effect, and the dashboard no longer offers archived deployments. A deployment preflight no longer fails DEPLOYMENT_PREFLIGHT_UNAVAILABLE on a route claim that stores an absent owner as null, and the deployment and adoption preflights log the cause of an unexpected failure. A node's requests no longer contend on the controller fence and share a 6 s budget below the cluster relay's 8 s forward deadline, a request out of budget is refused controller-deadline, which the node retries, and a node deletion refuses what a racing write bound to the node. Cloudly consumes the projection application receipts of Pallet 36.3.0, so a flow a node's projection withdrew is granted again on the same leases once the node reports that it applied the withdrawal; an earlier Pallet keeps every withdrawal carried. Cloudly 33.17.2 starts a database below data version 0.25.0 that holds a runtime slot: the data step runtime-log-attempts read through models startup has not prepared and was refused, and now reads and writes through the driver; an install whose first start failed there stayed at data version 0.24.0 and upgrades to 1.28.4 normally. Cloudly 33.17.1 fixes the first start of 33.17.0, which crash-looped on a database holding Pallet-era nodes: its first data step cluster-node-hostname-intent stated each intent through the node and cluster models, which startup has not prepared while it runs the data steps, and was refused before its first write; the step now reads and writes the stored node and cluster documents directly, like the other data steps. The migration ledger does not advance past a failed step, so an install on 1.25.0 whose first start failed stayed unchanged at data version 0.25.0 and upgrades to 1.28.4 normally. Cloudly 33.17.1 keeps the data steps of 33.17.0, which move the data version from 0.25.0 to 0.28.0: every upgrade from 1.24.0 or earlier, and from 1.25.0 whose first start failed, runs the forward-only data steps cluster-node-hostname-intent to 0.26.0 (every Pallet-era node gets the intent of the hostname it reports, and nothing is renamed), deployment-operation-route-evidence to 0.27.0 (a routeless deployment operation that succeeded records its empty route evidence) and service-secret-target-predecessor to 0.28.0 (a secret target state an earlier service deletion left behind is removed) before the 0.28.0 to 0.30.0 steps. The template stays marked breaking, migration-required, backup-before-upgrade and manual-review, because hosts read the flags from the target version alone and every upgrade from 1.26.0 or earlier runs the 0.28.0 to 0.30.0 steps, after the 0.25.0 to 0.28.0 steps from 1.24.0 or earlier; every upgrade from 1.19.0 or earlier runs the forward-only data step runtime-log-attempts from 0.24.0 to 0.25.0 before them (the log record of every slot's current attempt is written), and every upgrade from 1.15.0 or earlier runs corestore-control-token-state from 0.23.0 to 0.24.0 before that (a Corestore control token stored earlier is recorded as revision 1); every Cloudly before 33.17.3 refuses the migrated data: stop Cloudly and take a verified backup first, as rollback is full-restore-only. From 1.24.0 or earlier, Cloudly 33.17.0's changes apply too: Cloudly 33.17.0 lets a legacy import reserve: it takes over every route claim its legacy service holds in the import's organization, the cluster ingress keeps serving each hostname for the legacy service until the import's service declares it, and cleanup or rollback before promotion hands the claims back while the legacy service exists; ROUTE_UNAVAILABLE and DEPLOYMENT_ROUTE_COLLISION name each conflicting hostname and the services holding it. It owns the hostname a node should carry (interfaces 32.52.0, cloudly-worker-<name> by default): setClusterNodeHostname renames a Pallet-era node and resetClusterNodeHostname sets the default, Spark reads the intent at POST /spark/nodes/host, and a Pallet service's gateway routes and inbound mail follow the node its runtime spec pins by id, so a rename moves no traffic. A routeless deployment records its empty route evidence when it succeeds; deleting a cluster's designated ingress service is refused SERVICE_IS_CLUSTER_INGRESS, designateClusterIngress recovers a designation whose bearer is no longer active, and a deleted service's secret target state goes with its row, so a service recreated under its id writes its secrets again. From 1.23.0 or earlier, Cloudly 33.16.0's changes apply too: Cloudly 33.16.0 captures a legacy service's runtime attestation from an administrator's host observation, without a Coreflow session: captureLegacyServiceRuntimeAttestation takes hostObservation, the legacy Swarm service's spec image and mode and, per replica, its node, Swarm task, container, image store, image id, RepoDigests, state and health, read at most 15 minutes before the capture; Cloudly judges it against the one root every replica runs, which it requires whole in its registry and which the legacy tag need not still name, and refuses incomplete evidence without storing anything. The registry prune keeps an attested root until a release's cloudly-legacy-import evidence names the attestation or the legacy service is retired. From 1.22.0 or earlier, Cloudly 33.15.3's changes apply too: Cloudly 33.15.3 serves a promoted route: a route's identity carried the service's runtime write fence runtimeAuthorityFenceGeneration, which every runtime write of the service advances, in a Pallet-born cluster the route join's own workload lease read included, so the route plan read after the cluster ingress's certificate fetch never matched the one before it and every refresh that fetched a certificate withdrew the cluster's whole route table; a route's identity now carries only the versions that change with its authority (the route intent, the runtime policy revision, and per endpoint its assignment, its node's session and its target), while a service being deleted or holding no runtime authority is still refused by the route join, so a revoked route is withdrawn as before, by name. A table withdrawn because its authority changed or its route sources could not be read names that withdrawal as the reason the cluster ingress serves none of its routes, so a failing route verification states it; that reason states facts about its own route only, because one table carries the routes of several organizations: a route whose authority changed names its changed field, every other route only that another route's authority (or the ingress designation) changed during the certificate fetch, and a source-read failure only that the route sources could not be read, its error staying in the operator log. From 1.21.0 or earlier, Cloudly 33.15.2's changes apply too: Cloudly 33.15.2 lets a promoted route reach CoreTraffic: a route's identity carries the route intent version routeIntentGeneration instead of the whole-document routeStateGeneration, so the status writes during the cluster ingress's certificate fetch no longer withdraw the cluster's route table, a plan change during the fetch withdraws it only when it revokes a route the table is about to serve, and the change is named by route and input. A cluster ingress flow that a compensated promotion withdrew and a later promotion wants again no longer stalls the node's network projection: Cloudly holds that flow back with its withdrawal carried and signs the rest of the node's network, while a workload or router grant wanted again while withdrawn withholds the node's projection as withdrawn-grant-reissued and any other contract refusal as projection-rejected, each naming what it holds; the cluster ingress leaves a route whose grant is held out by name, and a failed route verification states why no route is served for its hostname. A held flow is granted again only once the target or CoreTraffic runs under a new lease. From 1.20.0 or earlier, Cloudly 33.15.1's changes apply too: Cloudly 33.15.1 fixes the deployment preflight of an existing service Pallet runs: it read the current runtime from Coreflow alone and answered NO-GO (CURRENT_RUNTIME_DIGEST_MISSING, RUNTIME_ATTESTATION_MISMATCH) for a service whose rollout status was verified, and now reads the runs of the service's current plan, the evidence its deployment status reads, when Pallet executes that plan on every cluster it targets. It also lets a route promotion survive the status writes of its own gateway sync: the promotion fenced on routeStateGeneration, the service's whole-document version, which the route status its gateway sync records, Coreflow's reconciliation reports and its own verified-route status all advance, so every promotion failed DEPLOYMENT_ROUTE_FENCE_CONFLICT with no route evidence; it now fences on routeIntentGeneration, which only a write that changes the service's domains advances, so a concurrent route change, a change and back included, still fails it fenced, and a promotion interrupted under 33.15.0 is recovered as fenced (retryable), without a rollback. From 1.19.0 or earlier, Cloudly 33.15.0's changes apply too: Cloudly 33.15.0 keeps workload logs: a Pallet node's log batches are kept per attempt, their payloads in Cloudly's own bucket (the template's S3_BUCKET) under runtime-logs/, bounded to 8 MiB and 8192 batches per attempt and the newest 8 attempts per slot, each batch for its config's logs.retentionMs, and read with getServiceRuntimeLogAttempts and getRuntimeAssignmentLogs (interfaces 32.47.0) or the service page's Logs tab. A Pallet-executed rollout that succeeded stays succeeded when a replica later fails or is replaced; a failing service runtime pass names its step, and a database error its code name and number. A launcher secret written before a service's first image plan no longer leaves the service refused secrets-unsupported, because every image plan is written with the service's secret deployment plan in one transaction. The designated cluster ingress runs with SERVEZONE_CLUSTER_RELAY_ORIGIN and SERVEZONE_CLUSTER_NODE_ID, which Cloudly writes into that workload's environment (a cluster without a valid relay refuses it cluster-relay-missing); the template sets neither. From 1.18.0 or earlier, Cloudly 33.14.1 and 33.14.2's changes apply too: Cloudly 33.14.2 completes a rollout Pallet executes: Cloudly reports on every rollout whose target clusters are all on Pallet, one reporter per cluster (pallet/<clusterId>) over the runs of exactly the current desire, so it moves to succeeded (or ready-for-route) once its runs are ready at the requested digest and fails when a run fails, and getServiceDeploymentStatus reads its runtime tasks from the runtime manager instead of Coreflow, so tsdeploy no longer times out on a Pallet-attached service. A launcher-secret run admitted after a Cloudly restart, reconnect or credential rotation gets its material again instead of waiting secret-mount-uncertain (secret-recipient-inactive) until its node process restarts, because a node's secret recipient is active for every session of the node process that enrolled it. A failing producer pass is logged under its producer's subject once per change of cause, with how long it ran and a value-free cause naming the failing step and check (such as network-projection-rejected), and its recovery once; a failed sweep the timer started is no longer dropped. From 1.17.0 or earlier, Cloudly 33.14.0's changes apply too: it gives a platform-owned service's published ports infrastructure-administrator authority: an administrator designates a platform organization's service (an organization id carrying :) with designatePlatformOwnedService, withdraws it with undesignatePlatformOwnedService and reads it with getServicePlatformOwnership (interfaces 32.45.0), after which only a verified administrator reads and writes its published ports, any other organization id is refused organization-not-platform, and the designation, stored on the service document, is read inside the published-port request's one admitted operation; every organization service keeps the organization-owner rule. It also names and logs every assignment dispatch outcome under a closed, value-free reason, pushes a refused or unanswered push again on the next pass, and keeps a run its node refused (cause refused) instead of replacing it on the failure backoff until its input changes. From 1.16.0 or earlier, Cloudly 33.13.3 and 33.13.4's changes apply too: Cloudly 33.13.4 delivers a run's secret material through the WorkloadInit launcher again (Corestore's MONGODB_URI included), remembers a successful WorkloadInit approval verification per artifact, and names the check of every refused material request with a closed, value-free reason; since Cloudly 33.13.3 a node's session is pushed each assignment revision once instead of on every delivery pass. From 1.15.0 or earlier, Cloudly 33.11.0 to 33.13.2's changes apply too: they let an administrator set the Corestore control token without an exec into Cloudly's container (setCorestoreControlToken, interfaces 32.42.0, with its revision and value-free receipts in the new collections cloudly_corestore_control_token_state and cloudly_corestore_control_token_receipts, and servezone corestore set-control-token), keep every managed VPN member across a protected-authority step that only adds, retire a node and its egress owner (retireClusterNode, interfaces 32.43.0), ask a retiring node on interfaces 32.44.0 to withdraw its allocation protection so that a stopped or offline node no longer blocks allocation, and keep a stopping workload's lease in its node's projection until the node's stop receipt is accepted. From 1.14.0 or earlier, Cloudly 33.10.0's changes apply too: it lets an operator obtain a cluster relay's bearer without an exec into Cloudly's process: exportClusterRelayAuthorization (interfaces 32.41.0) requires a freshly signed-in administrator, proves the delivered credential version against the hash its generation issued, commits an audit receipt fenced at that generation before it answers the bearer, and never logs it; the receipts live in the new collection cloudly_cluster_relay_authorization_exports, installed at startup and listed by getClusterRelayAuthorizationExports, and servezone relay export-authorization (@serve.zone/cli 33.10.0) writes the bearer as a new 0600 file. From 1.14.0 or earlier, Cloudly 33.9.0's changes apply too: a node's managed VPN member is kept across its network projection changes instead of being withdrawn from the hub network on every workload change (bindGetRuntimeManagedVpnCredentialSuccessor, interfaces 32.39.0), the failure cause a node reports with a failed run (interfaces 32.40.0) is stored and shown in the runtime assignment views and the dashboard's Cause column, and deleteServiceById answers success once a service is marked for deletion while another deleting service drains, naming its refusals SERVICE_LIFECYCLE_BUSY, SERVICE_ORGANIZATION_AUTHORITY_REQUIRED and SERVICE_MANAGER_STOPPING instead of an internal error. From 1.13.0 or earlier, Cloudly 33.8.3's fix applies too: it lets deleteServiceById retire a legacy service whose Corestore resources a successor adopted: its live Corestore bindings are tombstoned with the ordinary config push once every claimed legacy resource of each binding's capability has passed to the successor's live adopting binding, while a service without such a handover is still refused CORESTORE_CLEANUP_TARGET_AUTHORITY_REQUIRED. A legacy service whose runtime attestation a legacy-import operation still uses (reserving, reserved, provisioning or awaiting-image) is refused SERVICE_LEGACY_IMPORT_PENDING (retryable) and keeps running; import and deletion are serialized on the registry repository, so an import that starts after the check refuses as LEGACY_IMPORT_SOURCE_MISMATCH, and a deletion that proceeds deletes the attestations no release consumed. From 1.12.0 or earlier, Cloudly 33.8.2's fix applies too: Cloudly names why it refused a node's report: a refused reportRuntimeNetworkProtection, reportRuntimeAssignmentTerminalReceipt, reportRuntimeAssignmentObservation or reportRuntimeAssignmentLogs is logged as a warning once per node, report and distinct reason, and the node is answered with the unchanged error text plus { code: 'runtime-session-report-refused', reason } as the error's data. From 1.11.0 or earlier, Cloudly 33.8.1's fix applies too: deploy before any Pallet that sends discardedCaptures or a not-kept log loss reaches a fleet node, since it accepts those log batch fields of interfaces 32.38.0, which Cloudly 33.8.0 refuses on every retry, and still keeps no workload logs. From 1.10.0 or earlier, Cloudly 33.8.0's changes apply too: deploy before any Pallet that captures workload output reaches a fleet node, since Cloudly 33.8.0 answers a Pallet's reportRuntimeAssignmentLogs (interfaces 32.37.0) with not-kept, which an earlier Cloudly leaves unhandled. Cloudly 33.8.0 lets a legacy deployOnPush service without deployable source move to a greenfield successor that runs the exact image it ran (captureLegacyServiceRuntimeAttestation, a legacyImport reservation under the capability deployment:import-legacy, and importServiceDeploymentLegacyImage in place of a push), judges promotion against the platforms of the service's rollout target, refuses a registry repository that belongs to another service as REGISTRY_REPOSITORY_MISMATCH, promotes and routes a service with a Corestore database or objectstorage binding again, and reads a node whose last report is older than 180 seconds as offline. Its web UI moved to path URLs and a regrouped menu, so bookmarks of the old UI locations open the not-found screen; server paths such as /status and the OIDC callback are unchanged. From 1.9.0 or earlier, Cloudly 33.7.0's changes apply: Cloudly 33.7.0 lets a fresh greenfield service take over a legacy service's Corestore database and bucket with their data and retention intact: an adoptCorestoreLegacyResources selection entry may name successorServiceId (interfaces 32.34.0, carried out by Corestore 33.2.0 or later), the successor must share the owner's organization, be an immutable-deployment service, own no legacy resource of the capability and hold no other binding of it, and each legacy resource is adopted by one service only (legacy-resource-adopted-elsewhere, successor-organization-mismatch, successor-not-greenfield, successor-owns-legacy-resource). Since Cloudly 33.6.1, a deployment reservation whose release tag the registry already holds is refused DEPLOYMENT_RELEASE_TAG_OCCUPIED instead of waiting for its image forever, an administrator's manifest push to a release tag a deployment operation reserved is refused 409 release-tag-reserved, and a binding status report reaches its cluster relay only when it changed what the relay reads. From 1.8.0 or earlier, startup runs the forward-only data step deployment-machine-user-grants from 0.22.0 to 0.23.0, which keeps on every deployment machine user only the grants a capability check reads and permanently removes the rest (such as the { serviceId, capabilities } grants of Cloudly 11); stop Cloudly and take a verified backup first, as the way back is the pre-upgrade backup. Cloudly 33.6.0's changes apply too: Cloudly stops gracefully on SIGTERM and SIGINT within 8 seconds, below the 10-second stop grace of Docker and Swarm, so a stop or restart no longer leaves a database transaction held and the next start no longer crash-loops on EFENCE_BUSY; a process ended by SIGKILL still holds its database session until the server's session timeout. Every new service gets its own deployment namespace, so two services of one project reserve without DEPLOYMENT_RESOURCE_COLLISION, while a deployed service keeps its namespace, and getDeploymentMachineUsers lists users configured under Cloudly 11 again. From 1.7.0 or earlier, Cloudly 33.5.0's changes apply: Cloudly reserves a deployment only for a deployment machine user: reserveServiceDeployment refuses an administrator with DEPLOYMENT_MACHINE_IDENTITY_REQUIRED, including a deployer that signs in with a username and password (such as tsdeploy with TSDEPLOY_CLOUDLY_USERNAME/TSDEPLOY_CLOUDLY_PASSWORD), so before upgrading create a deployment machine user per deployer (mutateDeploymentMachineUser, or Security → Access → Deployment machine users in the dashboard), attach its grants with configureServiceDeploymentMachineUser and switch the deployer to its token; an existing api machine user with its grants already qualifies. A machine identity or registry bearer issued by an earlier release is refused until its holder exchanges its token again with getIdentityByToken, and removing a token now ends the sessions opened with it. Let no deployment run during the upgrade: a run in progress keeps its pre-upgrade identity and is refused for the rest of that run (rerun it, or resume it with deploy-retry). From 1.3.0 through 1.6.0, startup runs the forward-only data step runtime-session-protocol-offer from 0.21.0 to 0.22.0, which states protocol: null on every Pallet session row recorded before Cloudly stored its node's protocol offer; stop Cloudly and take a verified backup first, as rollback is full-restore-only. From 1.6.0, Cloudly 33.4.0's changes apply: Cloudly 33.4.0 lets setServicePublishedPorts publish a contiguous range through hostPortEnd (at most 1024 ports in 64 entries per protocol) and same-port outbound through symmetric: true, which needs public egress (published-port-symmetric-without-egress) and owns its inside ports (published-port-symmetric-inside-overlap). New handoff leases translate outbound flows only into 49152-65535, whose ports are refused published-port-reserved; a node keeps its 1024-65535 lease until its router is re-created. A policy write for a service being deleted is refused service-deleting. A projection carrying a range or a symmetric entry is withheld as node-contract-behind from a Pallet node whose session states a release below interfaces 32.31.0, or an unrecorded one, so ranges and symmetric entries take effect on a node only once it runs a Pallet release built on interfaces 32.31.0 or later. From 1.5.0, Cloudly 33.3.0's changes apply: Cloudly 33.3.0 lets an administrator set a service's network egress with setServiceNetworkEgress (public internet, and the platform endpoints of the protected authority), and refuses a workload whose database or object-storage binding sits on a node whose Corestore endpoint its network does not select as database-endpoint-unselected or objectstorage-endpoint-unselected; a replica already running keeps running while its target reports the refusal. From 1.4.0, Cloudly 33.2.0's changes apply: Cloudly takes a dcrouter admin token once through bootstrapExternalGatewayCredential to provision its own gateway-client credential, starts a first enrollment with the gateway settings, and stages a gateway settings change the held credential cannot carry until the next bootstrap. From 1.2.1 or earlier, startup runs the forward-only migration from 0.20.0 to 0.21.0 before that step, dropping the relay certificate and DNS collections of Cloudly's retired Cloudflare and Let's Encrypt integration and logging every relay name whose A record Cloudly had written at Cloudflare, which the operator removes at the provider. dcrouter holds every cluster relay name's A record and certificate from then on, SERVEZONE_SSLMODE=letsencrypt is refused, and the typed API changes as the Cloudly 33.0.0 changelog lists. Stop Cloudly and take a verified backup before upgrading; rollback is full-restore-only. Roll out Cloudly 33.19.2 before Coreflow 33.2.0; Coreflow 33.1.0 is the minimum. From 1.3.0, also roll out Cloudly before Coreflow and Corestore 33.0.0, and set every node's Corestore database endpoint, because a database binding on a node without one is not granted and its service is refused database-endpoint-missing, and existing database bindings return no credentials until they are granted again for the endpoint. From 1.1.10 or earlier, startup first runs the 1.2.0 steps from 0.10.0 and needs completed Secrets v2 cutover retirement. Both the existing Secrets v2 keyring and operator-approved WorkloadInit authority are required encrypted inputs delivered as separate root-owned 0400 files; the public environment contains only their fixed paths. |
ghost |
MariaDB | Uses MySQL-style database__connection__* environment variables. |
gitea |
MariaDB | Uses Gitea database environment variables with ${MARIADB_*} placeholders. |
nextcloud |
MariaDB, Valkey | Uses MySQL-compatible database variables. Valkey is declared as a platform requirement. |
plausible |
ClickHouse | Declares ClickHouse as a platform requirement. |
wordpress |
MariaDB | Uses standard WordPress database environment variables. |
Standalone templates such as nginx, adminer, valkey, portainer, mattermost, n8n, uptime-kuma, and vaultwarden currently define only image and port unless their version config adds explicit environment variables. rustdesk-server uses the official all-in-one s6 image and requires RELAY to advertise the public relay endpoint; RustDesk clients also require firewall access to TCP 21115, TCP/UDP 21116, TCP 21117, and optional web-client TCP 21118/21119.
Template Schema In Practice
Version configs use a small, pragmatic schema understood by serve.zone runtimes:
{
"image": "container/image@sha256:<64 lower-case hex>",
"port": 8080,
"envVars": [
{
"key": "ENV_NAME",
"value": "${PLACEHOLDER_OR_DEFAULT}",
"description": "Shown in the App Store UI",
"required": true
},
{
"key": "APP_TOKEN",
"description": "Generated application token",
"required": true,
"secret": true,
"delivery": {
"type": "launcher-environment",
"variableName": "APP_TOKEN",
"uid": 1000,
"gid": 1000,
"mode": 256
},
"generate": {
"encoding": "hex",
"bytes": 32
}
}
],
"platformRequirements": {
"mariadb": true,
"mongodb": true,
"s3": true,
"valkey": true,
"clickhouse": true,
"mail": true
},
"minOneboxVersion": "1.24.2"
}
Secret declarations never contain values. Each declares exactly one file or
launcher-environment delivery destination and may request bounded hex or
base64url generation. Public environment keys may use the mixed-case forms
required by upstream images; secret and platform-owned environment identities
remain uppercase canonical keys.
Every image is digest-pinned (name@sha256:…); the catalog contract refuses a
tag-only image.
platformRequirements.s3: true and volumes are legacy inputs.
normalizeAppStoreStorageConfig normalizes them deterministically for existing
templates, but new templates use
portable, template-local storageClasses and stable named storageRequests.
Legacy platformRequirements.s3: true cannot coexist with an explicit
objectStorage request. Legacy volume objects may retain name, mountPath,
readOnly, and backup; source, driver, and options fail validation
because they cannot be normalized portably.
Portable Storage Requests
Filesystem mounts and managed object-storage resources are different request kinds in the same portable contract. An app can request both, including more than one isolated object-storage binding:
{
"storageClasses": {
"databaseFast": {
"kind": "filesystem",
"purpose": "database",
"required": {
"performanceTier": "highIops",
"durability": "persistent",
"hardQuota": true,
"snapshots": "native",
"backup": true,
"encryptedInTransit": true
}
},
"backupCapacity": {
"kind": "objectStorage",
"purpose": "backup",
"required": {
"performanceTier": "capacity",
"durability": "persistent",
"hardQuota": true,
"backup": true,
"encryptedInTransit": true
}
}
},
"storageRequests": [
{
"id": "database-data",
"kind": "filesystem",
"storageClass": "databaseFast",
"mountPath": "/var/lib/example",
"accessMode": "ReadWriteOnce",
"capacity": { "request": "20GiB", "limit": "40GiB" },
"reclaimPolicy": "retain",
"protection": {
"backup": "required",
"snapshots": "native",
"consistency": "crashConsistent"
}
},
{
"id": "backup-archive",
"kind": "objectStorage",
"storageClass": "backupCapacity",
"accessMode": "readWrite",
"capacity": { "request": "100GiB", "limit": "1TiB" },
"reclaimPolicy": "retain",
"delivery": {
"type": "file",
"targetPath": "/run/secrets/backup-archive.json",
"format": "servezone-object-storage",
"uid": 1000,
"gid": 1000,
"mode": 256
},
"protection": {
"backup": "required",
"versioning": "required",
"retentionDays": 30
}
},
{
"id": "registry-capacity",
"kind": "objectStorage",
"storageClass": "backupCapacity",
"accessMode": "readWrite",
"reclaimPolicy": "retain",
"delivery": {
"type": "launcher-environment",
"keys": {
"endpoint": "REGISTRY_S3_ENDPOINT",
"bucket": "REGISTRY_S3_BUCKET",
"region": "REGISTRY_S3_REGION",
"accessKeyId": "REGISTRY_S3_ACCESS_KEY_ID",
"secretAccessKey": "REGISTRY_S3_SECRET_ACCESS_KEY"
}
}
}
],
"requiresFeatures": [
"storage.bindings",
"storage.filesystem",
"storage.object-storage",
"storage.object-storage.file"
]
}
Capacity values are positive integers followed by KiB, MiB, GiB, or
TiB; the amount must stay within the safe integer range. A limit requires
storageClass.required.hardQuota: true. For explicit
requests, delivery environment keys and file delivery paths must be unique across
the app config; provider credential values never belong in an App Store manifest.
protection is optional and states what a request needs beyond the storage
itself. A filesystem request may declare backup: "required", snapshots
(portable or native), and consistency (crashConsistent or
applicationConsistent); an object-storage request may declare
backup: "required", versioning: "required", and retentionDays (1 to
36500). A required backup or snapshots mode must also be declared by the
request's storage class. Omit protection to request no protection — a
declared protection must request at least one requirement, and
"protection": {} is refused, because an empty object states nothing a
fulfillment adapter can act on and names a request the platform storage
contract rejects.
backupExclusions declares data an app keeps only for observability, such as
workload logs: each entry names one object-storage request (an explicit request
id, or legacy-s3 with platformRequirements.s3), up to 16 key prefixes ending
in / and one operator-readable reason. A backup leaves the objects under those
prefixes out; a restore neither brings them back nor removes them. A version that
declares an exclusion must name storage.object-storage.backup-exclusions in
requiresFeatures, so a host that cannot honour it refuses the version instead
of backing up what it was told to leave out or ignoring the declaration:
{
"platformRequirements": { "s3": true },
"backupExclusions": [
{
"storageRequest": "legacy-s3",
"keyPrefixes": ["runtime-logs/"],
"reason": "Workload logs; a restore brings back state, not past logs."
}
],
"requiresFeatures": ["storage.object-storage.backup-exclusions"]
}
databaseBackupExclusions declares database collections an app keeps only for
observability, such as a workload log ledger: at most one entry per database
(mongodb, which platformRequirements.mongodb provisions), 1 to 64 collection
names in strictly increasing order and one reason. A backup exports those
collections with their indexes and without their documents, so a restore brings
them back empty; the app must rebuild what it needs of them on start. A version
that declares one must name storage.database.backup-exclusions in
requiresFeatures:
{
"platformRequirements": { "mongodb": true },
"databaseBackupExclusions": [
{
"database": "mongodb",
"collections": ["app_log_batches", "app_log_queue"],
"reason": "Workload log ledger; a restore brings back state, not past logs."
}
],
"requiresFeatures": ["storage.database.backup-exclusions"]
}
The full version config and every storage object are exact-key validated.
Classes express portable policy only. Provider addresses, protocols,
identities, host paths, drivers, and driver options are rejected instead of
being passed through. Unknown storage.* feature IDs also fail closed. Request
IDs are stable across upgrades, restores, and migrations.
Every version config is judged by the catalog contract of @serve.zone/interfaces
(appstore.validateAppStoreCatalog, which applies appstore.validateAppStoreVersionConfig
to every config); this package has no validator of its own. Before provisioning, a
runtime advertises its supported feature IDs and calls the fail-closed normalization
entry point:
import { normalizeAppStoreStorageConfig } from '@serve.zone/appstore';
const normalizedStorage = normalizeAppStoreStorageConfig(catalogVersion.config, {
supportedFeatures: capabilityAdvertisement.featureIds,
});
The result contains canonical storageClasses, storageRequests,
requestOrigins, requiredFeatureIds, backupExclusions, the declared
non-backup key prefixes and reason by object-storage request id (legacy-s3 for
platformRequirements.s3, empty when the version declares none), and
databaseBackupExclusions, the collections and reason a backup exports without
documents by database (mongodb, empty when the version declares none). requestOrigins marks explicit,
legacy-volume, and legacy-S3 requests so fulfillment adapters can preserve old
delivery behavior without weakening the new contract. Legacy S3 normalization
uses the servezone-platform-s3 delivery profile; adapters keep
the established S3/AWS environment aliases for that profile. Those
binding-provided values deterministically replace same-key legacy template
environment values. Legacy inputs do not claim new feature support, while every
explicit request fails before provisioning if a derived storage feature is
unavailable. A declared backup exclusion fails the same way when the target does
not advertise storage.object-storage.backup-exclusions.
Environment variable keys must start with a letter or underscore and may contain letters, numbers, and underscores. Mixed-case keys are supported because some upstream images, including Ghost and Gitea, require lowercase nested configuration segments such as database__connection__host or GITEA__database__HOST.
The Runtime Catalog
The catalog is one document, IAppStoreCatalog of the @serve.zone/interfaces App Store catalog contract: every app, every published version and every version's full config with its configDigest, plus contract, revision, publishedAt and sourceRevision. A release ships it as an IAppStoreCatalogEnvelope: the catalog, its catalogDigest and one or more Ed25519 signatures.
revisionismajor × 10¹² + minor × 10⁶ + patchof the package version (33.0.0→33000000000000). Package versions only grow, so every release's catalog has a greater revision than the one before, and a consumer refuses any catalog whose revision is not greater than its active one (revisionRegressed). A prerelease version is refused. A catalog published from an older release line therefore never replaces a newer one.sourceRevisionis the commit the catalog was built from andpublishedAtthat commit's committer time, so the same commit always builds the same catalog.- Apps are ordered by id; versions in the order
app.jsonlists them. - A published
app@versionnever changes and never disappears. A version is retired by withdrawing it (withdrawn.json); it stays listed with its config and is refused as an install or upgrade target.catalog.lock.jsonrecords every published version, and the build and the test suite refuse a catalog that removes, rewrites or reinstates one. Consumers apply the same rules between two catalogs (publishedVersionRemoved,publishedVersionMutated,publishedWithdrawalChanged).
Publishing A Catalog
| Command | What it does |
|---|---|
pnpm build |
Compiles, then writes dist_catalog/catalog.json and dist_catalog/catalog.digest. Signs as well when SERVEZONE_APPSTORE_CATALOG_SIGNING_KEY_FILE is set. |
pnpm run build:release |
The release build (.smartconfig.json release.preflight.buildCommand). Refuses a dirty working tree, any version catalog.lock.json does not record, and a missing signing key; writes the signed dist_catalog/catalog.envelope.json. |
pnpm run catalog:lock |
Records every new version (and every new withdrawal) in catalog.lock.json; refuses a catalog that violates it. Run it, and commit the lock, with every new version or withdrawal. |
pnpm run catalog:trustedkey |
Prints the { keyId, publicKey } record consumers ship for the configured signing key. |
SERVEZONE_APPSTORE_CATALOG_SIGNING_KEY_FILE names a file holding the PKCS#8 PEM of an Ed25519 private key (openssl genpkey -algorithm ed25519); the key itself never appears in arguments or logs. A release refuses to ship without it twice: build:release fails, and the package exports ./catalog.envelope.json, so gitzone's entry-point check refuses a tarball that does not carry the envelope.
Signing Key And Rotation
A release signs with exactly one key: the one SERVEZONE_APPSTORE_CATALOG_SIGNING_KEY_FILE names. Consumers ship the trusted keys (IAppStoreCatalogTrustedKey, printed by pnpm run catalog:trustedkey) with their own release, and an envelope verifies when one of its signatures is by a key the consumer trusts. Rotating the key is therefore trust first, switch second:
- Generate the new key and print its trusted-key record.
- Release Onebox and Cloudly trusting both the current and the new key. Catalogs signed with the current key keep activating everywhere.
- Once every running consumer trusts the new key, point the variable at the new key file for the next App Store release. A consumer that still trusts only the old key refuses that catalog (
signatureInvalid, no signature by a trusted key) and keeps its last-known-good catalog until it is upgraded. - Release Onebox and Cloudly without the old key.
A compromised key follows the same steps, with step 2 dropping the compromised key at once: consumers then refuse every catalog until the first one signed with the new key is published. The contract allows up to eight signatures per envelope, but this publisher signs with one key; signing with two keys at once is not supported.
Signing Key Custody
The signing key is a key file on the release machine, outside every repository, held by the release operator; nothing in this repository, its CI or its logs carries it. A release runs with SERVEZONE_APPSTORE_CATALOG_SIGNING_KEY_FILE naming that file:
- gitzone passes its environment to the release build, which it runs as
pnpm run build:releasein a clean checkout of the release commit; the publisher reads the file (importAppStoreCatalogSigningKeyPemof@serve.zone/interfaces/runtime), signs the catalog, verifies the envelope against the key's own trusted-key record and writesdist_catalog/catalog.envelope.jsoninto the package gitzone publishes. It logs the key id and public key only. - The Gitea tag workflow (
.gitea/workflows/release.yml) has no key: it builds the unsigned catalog of the tag and attachescatalog.jsonandcatalog.digest, whose digest equals the published envelope'scatalogDigest. - Generate a key with
openssl genpkey -algorithm ed25519 -out <key file>on the release machine and keep the file readable by the release user only;pnpm run catalog:trustedkeywith the variable set prints the record Onebox and Cloudly ship. Losing the file means rotating to a new key as above.
Loading The Catalog
import { AppStoreCatalogLoader } from '@serve.zone/appstore';
const loader = new AppStoreCatalogLoader({
trustedKeys, // IAppStoreCatalogTrustedKey[] shipped with this release
supportedFeatureIds, // the requiresFeatures ids this consumer fulfils
registries: configuredRegistries, // empty or absent: the default, npmjs
requestTimeoutMs: 30_000, // per request, body included; the default
});
// At start: the stored last-known-good envelope, verified again against today's trust.
const restored = await loader.restoreEnvelopeText(storedText, { origin: storedOrigin, loadedAt: storedLoadedAt });
// 'restored' activate restored.envelope; status.active = restored.active
// 'refused' no active catalog: status unavailable, cause 'refused', refused = restored.refused
// On the consumer's schedule, never awaited by startup or a request:
const result = await loader.loadFromRegistries(activeEnvelope ?? null, { signal: stopController.signal });
// 'activated' store result.envelope as last-known-good; status.active = result.active
// 'unchanged' nothing to store
// 'refused' keep the active catalog; status.refused = result.refused
// 'failed' keep the active catalog; retry with backoff
// Always: status.lastAttempt = result.attempt; result.registries reports each registry's answer.
// An aborted signal rejects the load with its reason: the consumer is stopping, nothing is recorded.
One load asks every configured registry for the package its latest dist-tag names, downloads the tarball (bounded in time and size, its unpacked size bounded too), checks it against the registry's dist.integrity, extracts dist_catalog/catalog.envelope.json, verifies it with verifyAppStoreCatalogEnvelope (shape, digest, a signature by a trusted key, contract, schema with the consumer's supportedFeatureIds, every config digest) and judges it against the active catalog with judgeAppStoreCatalogSuccession. The first registry whose catalog activates ends the load; otherwise the result is the first unchanged, else the first refused, else failed. Transport, integrity and packaging failures and every refusal are results, never exceptions; an envelope the verifier cannot read is refused as schemaInvalid.
The registry list follows the one rule of @serve.zone/interfaces: appstore.validateAppStoreCatalogRegistries (https: base URLs without credentials, query or fragment, each registry once) and appstore.resolveAppStoreCatalogRegistries (an empty list means appStoreCatalogDefaultRegistries). A consumer validates an operator's setting with the same function before it stores it.
The loader holds the consumer's options and nothing else: no catalog, no timer, no schedule, no persistence. The consumer's catalog runtime owns the one schedule of its process. The cadence Onebox and Cloudly follow, as guidance:
- restore the stored envelope at start and start the first load in the background, without waiting for it;
- load every 15 minutes while the last attempt activated or answered
unchangedorrefused; - after a
failedattempt, retry after 1 minute, doubling to at most 30 minutes, and return to 15 minutes after the next success; - run one load at a time, abort it with the signal on stop, and answer an administrator's refresh with one load now.
An operator-imported envelope (an offline signed bundle, or the envelope a package carries at @serve.zone/appstore/catalog.envelope.json) is judged the same way with judgeImportedEnvelope(value, active) or judgeImportedEnvelopeText(text, active); its activation reports origin: 'operatorImport'.
Showing The Catalog
import { getAppStoreCatalogListing, projectAppStoreCatalogApp } from '@serve.zone/appstore';
const consumer = { supportedFeatureIds, consumer: { kind: 'onebox', version: oneboxVersion } };
const apps = getAppStoreCatalogListing(activeCatalog, consumer); // IAppStoreApp[] for getAppStoreTemplates
const projection = projectAppStoreCatalogApp(catalogApp, consumer);
// projection.versions: every listed version, each with installRefusal (null, versionWithdrawn,
// featuresUnsupported or consumerVersionTooOld)
// projection.latestInstallableVersion the newest version this consumer may install, or null
// projection.display { app: IAppStoreApp, meta: IAppStoreAppMeta }, or null without an install target
The display shapes list only the versions this consumer may install in versions and name the newest of them as latestVersion, so a version picker built from them never offers a withdrawn version, one gated on a feature the consumer lacks or one that needs a newer consumer release; versionStates states every listed version with its refusal. Under semver the newest install target is the highest one; under branch it is the catalog's latestVersion when that is an install target, and otherwise the app has none.
Testing A Consumer
@serve.zone/appstore/testing serves a consumer's tests, never its runtime: createAppStoreTestSigningKey makes a throwaway key and its trusted-key record, createAppStoreTestCatalog and createAppStoreTestVersionConfig a catalog the contract accepts, signAppStoreTestCatalog its envelope, createAppStoreTestPackageTarball the package tarball carrying it, createAppStoreTestRegistry a registry stub and createAppStoreTestFetch the fetch a loader under test is given.
The published Cloudly templates are qualified here (test/test.cloudlytemplates.node.ts: pinned image, Onebox minimum, upgrade flags and platform OIDC of every template), so a template release needs no host test change.
Migrating From 32.x
AppStoreResolverand itspackageandremotesource modes are gone, with the bundleddata.generated.ts,appStorePackageMetadata,appStoreDefaultRemoteBaseUrlandgetSourceInfo(). Load the signed catalog withAppStoreCatalogLoader, keep the last verified envelope in your database (restore it withrestoreEnvelopeText), and read apps and versions from it (findAppStoreCatalogApp,findAppStoreCatalogVersion). Snapshot a service's version config andconfigDigestat install and upgrade; runtime paths read that snapshot, not the catalog.- Linked
servezone.appstore.jsonmanifests (source.type=repoManifest),dockerImagesources and digest tracking (upgradeStrategy: 'dockerDigest') are gone: every version is a catalog entry with a digest-pinned image.parseAppStoreIndex,parseServezoneAppStoreManifest,parseDockerImageReferenceandcreateAppStoreVersionForDockerSourceare removed. resolver.getApps()isgetAppStoreCatalogListing(catalog, { supportedFeatureIds, consumer })andresolver.getAppMeta(appId)isprojectAppStoreCatalogApp(app, { supportedFeatureIds, consumer }).display?.meta;resolver.getAppVersionConfig(appId, version)isfindAppStoreCatalogVersion(catalog, appId, version)?.config.- Executable
migrate-from-*.tsscripts are no longer supported; a version folder holds onlyconfig.jsonandwithdrawn.json. - Validation is owned by
@serve.zone/interfaces: useappstore.validateAppStoreVersionConfiginstead ofresolver.validateAppStoreVersionConfig, and the storage contract instead ofvalidateAppStoreStorageConfigandparseStorageCapacityQuantity. resolver.normalizeStorageConfig(config, options, label)isnormalizeAppStoreStorageConfig(config, options, label), andresolver.normalizeVolumes(volumes)isnormalizeAppStoreVolumes(config, label); both read the catalog version config shape (appstore.TAppStoreCatalogVersionConfig).- The package no longer ships
apps/,appstore.jsonorappstore.resolved.json;appstore.jsonis folded into eachapp.json.
Working With Templates
- A new version is a new
apps/<app>/versions/<version>/config.json, listed inapp.jsonversions(andlatestVersionwhen it is the highest). Never edit a published version: publish a new one, and withdraw the old one withwithdrawn.jsonwhen it must not be installed any more. - Pin every image to the digest of its multi-platform index (
name@sha256:…) and record the tag and digest in the changelog. - Run
pnpm run catalog:lockand commitcatalog.lock.jsonwith every new version or withdrawal; the test suite fails on an unlocked or changed version. - Gate a version that uses a config member newer than the catalog contract's minor on a feature id in
requiresFeatures, so older consumers keep loading the rest of the catalog. - Prefer explicit environment variable descriptions because Onebox surfaces them to users during installation.
- Use platform placeholders such as
${MARIADB_HOST},${MARIADB_PORT},${MARIADB_DATABASE},${MARIADB_USER},${MARIADB_PASSWORD},${MONGODB_URI},${S3_BUCKET},${S3_ACCESS_KEY},${S3_SECRET_KEY},${MAIL_FROM},${SMTP_HOST},${SMTP_PORT},${SMTP_USERNAME}, and${SMTP_PASSWORD}only when the matching platform service is declared. - Use named
objectStoragerequests instead ofplatformRequirements.s3in new templates, with a distinct delivery destination for every binding. - Use named
filesystemrequests instead ofvolumesin new templates. Never put provider, host-mount, protocol, identity, driver, or driver-option details in portable manifests. - Validate non-trivial templates by installing them through Onebox instead of only checking JSON syntax.
Project Map
appstore/
├── apps/
│ ├── cloudly/
│ │ ├── app.json
│ │ └── versions/<version>/config.json (+ withdrawn.json)
│ └── ...
├── catalog.lock.json
├── ts_publisher/ catalog builder, signing, lock (not published)
├── ts_client/ AppStoreCatalogLoader, catalog lookups, storage handoff
└── dist_catalog/ build output: catalog.json, catalog.digest, catalog.envelope.json
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
Cloudly 1.30.8
The current Cloudly target pins the published 33.26.0 image at code.foss.global/serve.zone/cloudly@sha256:b5b1d9f0ba494d4e8472f141c03b7b76a8590d256debfd9c144f1eb866c18069. It keeps 1.30.7's environment aliases, NoSQLDB/Corestore and S3 mappings, encrypted keyring and WorkloadInit approval files, OIDC capability, health check, Onebox minimum 33.8.0, backup exclusions and required feature ids. Hosts without storage.database.backup-exclusions still resolve to 1.29.11.
Cloudly 33.26.0 adds the independent seven-method native monitor at /typedrequest/native/v9; a full monitor still needs administrator authority. Its returned identity also has the instance's legacy administrator authority, so a reviewer instance must own only synthetic infrastructure data and isolated credentials. The exact source and both native image artifacts are published; a catalog entry does not install them or establish live reviewer readiness. No production control-plane upgrade or public route is performed by this publication.
An upgrade from 1.30.7's data version 0.33.0 runs the four forward-only steps service-hostname-canonical (0.34.0), runtime-authority-tokens (0.35.0), runtime-assignment-failures (0.36.0) and bounded-slot-history (0.37.0). Stop the existing Cloudly and verify a pre-upgrade database backup first. Rollback requires restoring that backup; an older image cannot open the migrated database. Fresh synthetic installations receive their own empty namespace, bucket and key material through the normal host workflow. The four manual review/migration/backup/breaking flags remain set.
Cloudly 1.30.9: interim 33.26.0 bootstrap compatibility
Cloudly 1.30.9 is 1.30.8 on the same published 33.26.0 image, with one optional, supplied-only secret declaration: SERVEZONE_ADMINACCOUNT. This is an interim compatibility option for that exact image, following the transitional delivery alternative recorded in AGL-1246. The owning target in AGL-1179 under AGL-1178 is Onebox sign-in through idp.global with passkey fallback and removal of both Cloudly's password-admin and parent initial-admin bootstrap paths. This template does not implement or extend that final authentication design. Existing installations and upgrades leave it unset. A fresh empty Cloudly database needs a distinct private operator in username:password form through the existing sealed App Store input or the owner CLI's appstore install ... --secrets-stdin path. It has no public default and no automatic generator. Onebox administration still uses passkeys; this input is for the hosted Cloudly workload and is never the reviewer credential.
The template retains both backup feature requirements, the Onebox minimum, private keyring and WorkloadInit approval file mappings, OIDC capability and all upgrade flags. A host lacking storage.database.backup-exclusions still refuses the target and keeps its compatible template. This declaration does not provide active WorkloadInit authority, a parent JWT delivery path, a new launcher or an install-time bypass.
After the first successful boot, the Onebox owner removes only the encrypted startup alias through the existing normal replacement lifecycle:
printf '%s' '{"set":{},"unset":[],"unsetSecrets":["SERVEZONE_ADMINACCOUNT"]}' | onebox service patch-env <service> --stdin
onebox service inspect-metadata <service>
The patch contains no credential. The replacement preserves the exact image, database, other secrets, keyring/approval mappings and prior running/stopped state; metadata inspection reports key names without values. An unchanged replay performs no second replacement, and later upgrades do not regenerate this optional input.
Removing the startup alias does not revoke the persisted Cloudly operator password. After real hosted OIDC establishes a distinct fresh human administrator, retire that password if required through Cloudly's existing getHumanCredential / mutateHumanCredential operation revokePassword, with the actual user id and generation fence. Reviewer access uses the genuine OIDC human and its own guarded password ceremony. The isolated application fixture is not evidence of a fresh App Store install, active host admission or a public reviewer instance. Password revocation is an explicit operator decision, never an automatic effect of startup, secret removal or upgrade. This 33.26.0 template also does not meet AGL-1246's separate lab/catalog requirement for an install and newer upgrade target on Cloudly >=33.27.0; that work retains its own owning qualification and host feature gates.