Packages & frontends
An app instance runs one bundle: a WASM module named by
hex(SHA-256(wasm)). In production a bundle always comes out of a package
release: a signed .mpk (calimero-bundle) holding a calimero:app
component built with calimero-app-build, its manifest signed by its publisher
with mero-sign. See
Packaging & signing for building one.
What a release carries
Section titled “What a release carries”The .mpk manifest names the package, its version, its services (each a WASM
module; a single-service package has one unnamed service), a frontend, display
metadata, the minRuntimeVersion it needs, and the outbound URL prefixes it
declares. The manifest is signed by the publisher’s key, and every artifact in
the archive is listed with its digest.
Installing a release
Section titled “Installing a release”A release travels from its publisher to a registry, and from there to every
node that runs it, never from node to node: the node that installs it verifies
it before the system log records its hash, and each replica fetches and
verifies the same bytes again before running them. The installer is an
account (or an app’s creator, since POST /apps installs a missing release
first) or the operator. On a development cluster (--dev) an uploaded .mpk
(POST /packages) skips the registry and is spread to a majority of nodes
instead; a production node refuses uploads (403 UPLOADS_DISABLED).
There are two ways in:
POST /packages/install { package, version }fetchespackage@versionfrom the operator’s registry (--registry-url,calimero-app-downloader’sHttpRegistry).POST /packagestakes an uploaded.mpk, on a development cluster only.
Before the system log records anything (InstallPackage), the node that took
the request verifies the release (Node::verify_release):
- The publisher’s signature over the manifest and every artifact’s digest
(
calimero_bundle::VerifiedBundle), and that it ships WASM at all. minRuntimeVersionis not newer than this node’s release (CALIMERO_RELEASE_VERSION).- The package’s pinned signer: the first release of a package pins its
publisher key (
did:key); a release signed by any other key is refused. - Each service’s WASM compiles.
- A registry install got the package and version it asked for.
apply checks the signer pin again, refuses a version already installed with
other contents (the same release again changes nothing but who installed it:
the new installer is added to the release’s installers), and charges the .mpk’s
size to the installer’s bundle quota (the operator’s installs count against no
account). The log keeps, per release, the .mpk’s hash and size, where it came
from (registry or upload), each service’s bundle hash, the metadata, and the
declared outbound prefixes.
Where the bytes live
Section titled “Where the bytes live”- A registry release is never passed between nodes. Each node that needs
to run it fetches it from the registry itself (
Node::release_wasm), refuses bytes whose hash differs from the one the log recorded, and verifies the release again before using its WASM. That is the one-source rule: the registry is the only place a registry release comes from. - An uploaded release (development clusters only) has no registry. The
.mpkis stored on a majority of nodes before the log records it; a node that lacks it fetches it from a peer, checks the recorded hash and verifies it again. Production takes no uploads: an upload’s only copies are on the nodes it reached, and a cluster that grows from one node and retires it would lose the release, while a registry release can always be fetched again. - Each service’s WASM is also kept in the installing node’s blob store under
its bundle hash; other nodes extract it from the
.mpkon first use.
Creating, upgrading and removing
Section titled “Creating, upgrading and removing”POST /apps { package, version?, service?, … }creates an instance from a release; without a version, the newest installed one. If the release is not installed yet, it is installed from the registry first. A package with several services needsservice. Withdeployment, the instance runs the release that deployment offers forpackage, and records it.POST /apps/{app}/upgrade { version }moves an instance to another release of its package (Upgrades).DELETE /packages/{p}[/{v}]removes a release, or all of them, and is refused while any app still runs one of its bundles. A bundle another release also ships stays, recorded under that release. Only whoever installed the package’s first release (an account, or the operator) may uninstall, and never a release a deployment offers.
Who sees a release
Section titled “Who sees a release”Many organizations may share one cluster, and what one of them installed is
not another’s to list or run. A release is visible (SystemState::sees_release) to:
- whoever installed it: the account recorded as
installedBy, or one that installed the same release again later (installers); - every account, for a release the operator installed (
/ops/packages): the cluster’s own catalog, which the operator upgrades apps to; - an account that shares an organization with an account that installed it;
- an account that reaches an app running one of its services (so a member of an app always sees the release it runs);
- a session opened on the package’s own frontend, for every release of that package (the frontend is public by name, and it creates instances of it);
- the operator, for the releases it installed; it sees no account’s releases.
For anyone else the release is not installed. GET /packages and
GET /packages/{p} list only the releases the caller sees (a package with
none answers 404), and GET /ops/packages only the operator’s. Creating or
upgrading an app from it resolves as for a missing release: without a version,
the newest release the caller sees; with one, the release is installed from
the registry first, which makes a registry release the caller’s too, and
answers what a missing release answers when the registry lacks it (an upload).
Naming its WASM by hash answers as an unknown bundle, and approving outbound
hosts for a package with no release the caller sees answers 404. An
organization’s upgrade run moves an app only to a release its upgrader sees;
the operator’s, to one it installed or the app’s owner sees.
Records from before installers existed read with none; every release has
always recorded installedBy, so each is visible by the rules above from the
first start of this version.
Rolling upgrade (no version gate). A node on the previous release applies an install of an already installed release as a no-op. While both releases run, a repeated install is recorded only on the upgraded nodes (and a node upgraded later keeps the record its old snapshot had). Repeat the install once every node runs this version, and it is recorded everywhere. Nodes on the previous release still list every package to everyone.
Development bundles
Section titled “Development bundles”POST /bundles takes raw WASM (at most 16 MiB) with no publisher signature. It
is refused unless the node allows unsigned bundles (--dev, or
allow_unsigned_bundles). The node checks the WASM compiles, stores it on a
majority of nodes, then records PutBundle { hash, size }. Raw bundles have no
package, so they cannot be served as a frontend, cannot declare outbound
prefixes, and have no pinned signer.
ABI facts
Section titled “ABI facts”The node reads the ABI a component embeds (its calimero:abi section) for what
it acts on: which methods are views and confirm methods, whom each method admits
(accounts, API keys, other apps, the system), its timers, and the tables search
indexes. A method the ABI does not name is refused (default-deny).
Frontends
Section titled “Frontends”With --apps-domain D, each package’s frontend is served at its own origin,
{label}.D, where the label is 52 lowercase base32 characters of
SHA-256("calimero.app-host.v1\0" ‖ package) (app_host::label). merod local
uses localhost.
- The files are the release’s own
.mpkfrontend, fetched like its WASM and verified against the publisher’s signature before a byte is served. GET /redirects to the newest release’s entry;/{version}/{path}serves a file, falling back to the entry for client-side routes./.well-known/calimero.jsonnames the package, its releases, the deployments offering it (id,name,app,version) and the cluster’s API URLs.GET /app/{app}/{path}redirects to/{version}/{path}of the release that instance runs (the one it was created at or last upgraded to, never simply the newest shipping its WASM: releases may share one and differ only in their frontend), so a link handed to a customer opens the frontend matching their instance after a newer release is installed.GET /d/{deployment}/{path}redirects to/{version}/{path}?deployment={id}, the release that deployment offers for this package (the newest, if it offers several), so the page knows which deployment to create instances from. Neither names anything but a version; an instance of another package, or a deployment that does not offer this one, is a404.- Only
GETandHEADof files. An unknown label under D is a404, never a node route. Responses carry a fixed content security policy with the cluster’s API origins and the attestation mirror (https://cloud.calimero.network, where the page’s client gets the release’s signed measurements and Intel’s collateral; both are signed, so the mirror is trusted with nothing) added toconnect-src.
Cluster hosts
Section titled “Cluster hosts”A package may be deployed on several clusters while each customer’s account and
organization live on one, and a front that routes by hostname alone (Calimero
Cloud’s resolver) cannot tell them apart by the package’s host. So on each
cluster the package also has the host {cluster_label}.D, where the label is 52
lowercase base32 characters (unpadded, alphabet a–z, 2–7) of
SHA-256("calimero.app-host.cluster.v1\0" ‖ cluster ‖ "\0" ‖ package),
cluster being the node’s --cluster (app_host::cluster_label). For example,
com.calimero.kv on cluster eu-shared-1 is at
zqcnu7vlgoxqnu6ar6awxsbcclqru6ry4ygt3bou7tqpimvx25va.D.
On its cluster the host is the package’s site exactly as {label}.D is (every
path above, /d/{deployment}/ included), and an origin of the package: CORS
admits it, and a passkey ceremony from it opens a session bound to the package.
A cluster label made for another cluster answers 404.
Deployment hosts
Section titled “Deployment hosts”The front does not route these yet; cluster hosts are what it routes.
Every deployment has a host of its own too, {dlabel}.D, where the dlabel is
52 lowercase base32 characters (unpadded, alphabet a–z, 2–7) of
SHA-256("calimero.app-host.deployment.v1\0" ‖ id) and id is the
deployment’s id (32 lowercase hex characters; app_host::deployment_label).
For example, deployment 0123456789abcdef0123456789abcdef is served at
lprfchrsznkwfwiu7gfovvcywyujxdza3rentyjkivnw6cxescca.D. Its own prefix keeps a
dlabel apart from every package label.
The deployment’s host names the deployment, and with it the cluster that holds it.
- It serves the frontend of the one package the deployment offers, exactly as
/d/{deployment}/does on the package’s host:GET /redirects to/{version}/?deployment={id}of the release it offers; files, client routes and/app/{app}/as above;/d/{deployment}/only for its own deployment. /.well-known/calimero.jsoncarries"deployment": idand lists that deployment first (nullon the package’s host).- A deployment that offers nothing yet, or apps of several packages, has no
frontend: its host answers
404. - It is an origin of its own, of the same package: CORS admits it, and a passkey ceremony from it opens a session bound to that package, like one from the package’s host.
POST /deploymentsanswers the new deployment’shostbeside itsid, andGET /deploymentsandGET /deployments/{id}carry it (nullwithout--apps-domain).
Custom domains
Section titled “Custom domains”A package’s manager may have the cluster serve its frontend on a domain of
their own (https://kv.example.com) by registering it as a served origin
(POST /package-origins/{package} { origin, served: true }, with their
passkey; Registered origins). A
request whose Host is that name gets the package’s site exactly as {label}.D
serves it (every path above, the same content security policy, /.well-known/calimero.json),
before any node route. Unlike the package’s host, it is a registered origin: a
passkey ceremony there runs under the domain’s own rp-id, never the
cluster’s, and opens a session bound to the package that names the origin;
removing it ends the site and those sessions together. TLS for the name ends
inside the TD with a certificate Calimero Cloud orders for the node’s attested
key; the end-to-end design is docs/specs/custom-domains.md.
Why its own origin. An app’s page is third-party code. Served from the cluster’s own origin, it could reach anything the signed-in account can. On its own origin it is a separate site to the browser, and a passkey ceremony from it opens a session bound to its package: that session reaches only instances of that package, creates only instances of it, and is refused credentials, API keys, approvals, upgrades, uninstalls and uploads (Accounts, passkeys & sessions). Its step-ups come only from its own origin.