Offer your app to customers
You built an app and want other organizations to run their own instances of it, each with its own data, without installing anything themselves. A deployment is how: your list of the apps you offer on a cluster, each pinned to one installed release. A customer creates an instance from your offer; the instance is theirs (their organization, their members, their data), and its usage is billed to you as its developer.
This guide uses meroctl raw (METHOD PATH [BODY]): meroctl has no
deployment commands yet. Every change below asks for a passkey step-up
(412 CONFIRM_REQUIRED first). On a development cluster, --dev-passkey FILE
answers it; on a production cluster the step-up is a passkey in a browser:
call the same routes from mero-js with { confirm: passkeyConfirm(browserPasskey()) },
as in Deploy from CI. The protocol behind each step
is in Deployments.
Before you start
Section titled “Before you start”- A signed release in the cluster’s registry. Build and sign it as in Packaging & signing.
- Who owns the deployment. Your account, or an organization in which you
hold
MANAGE_APPLICATIONorganization-wide (its owner and admins do). Whoever holds that capability there manages the deployment with you. - On a granted cluster, a deployer grant. Where the cluster’s deployments
are
granted, installing a release, creating a deployment and offering an app each need a live deployer grant: your account’s, or your organization’s. The operator (or the cluster’s owner) grants it; without it these answer403 DEPLOY_NOT_GRANTED. On anopencluster nothing is needed. - Bind your package’s signing key (recommended). Until you do, each
customer’s organization approves which node images hold its instance, and a
package that declares
upgrades: publishercannot be upgraded at all. Binding makes your organization the publisher for every customer’s instance.
1. Install the release
Section titled “1. Install the release”meroctl app install chat@1.0.0The node fetches the release from the registry, verifies its signature and its pinned signer, and records it as installed by you.
2. Create the deployment
Section titled “2. Create the deployment”meroctl raw post /deployments '{"name":"Chat","owner":{"org":"<org-id>"}}' --dev-passkey ./dev-passkey.json# { "id": "3f2a…", "deployment": { … }, "host": "lprf….apps.example.com" }owner is {"org": "<id>"} or {"account": "<id>"}. The name is what
customers see. The id comes from your passkey consent, and host is the
deployment’s own frontend host. An owner has at most 16 deployments.
3. Offer a release
Section titled “3. Offer a release”meroctl raw post /deployments/<id>/apps '{"app":"chat","package":"chat","version":"1.0.0"}' --dev-passkey ./dev-passkey.jsonapp is the name of the entry in your deployment; it offers exactly one
release. Offering the same app again with another version of the same
package moves the entry, and new instances get that release from then on.
Instances already created keep the release they run (see
Shipping a new version). A deployment holds at most
64 apps, and an offered release cannot be uninstalled.
4. Share the frontend
Section titled “4. Share the frontend”The release’s frontend opens at your deployment’s release on two addresses:
- The deployment’s host,
https://{host}/(from step 2):/redirects to/{version}/?deployment={id}of the release you offer. It serves a frontend only while the deployment offers one package; with none or several, it answers404. - The package’s own host,
https://{label}.<apps-domain>/d/{id}/, which redirects the same way.
Both keep the query string, so an invite link such as /?invite=… reaches
the page. GET /deployments/{id} is public: your frontend reads it to show
the deployment’s name, its owner’s name and what it offers, never its
managers or customers. See Deployment hosts.
5. A customer creates an instance
Section titled “5. A customer creates an instance”On your frontend, the customer signs in (or signs up) with a passkey. The session is bound to your package, and may create instances of it. Your frontend sends:
POST /apps{ "deployment": "<id>", "package": "chat", "org": "<their-org>", "name": "Support chat" }The instance runs the
release you offer (a version, if given, must be it), whether or not the
customer could see that release otherwise, and without a deployer grant:
nothing is installed. The customer needs what creating any instance in their
organization needs (CREATE_APP there, or admin); with no org, it goes to
their personal organization. The instance records the deployment, and the
log checks again, where CreateApp applies, that the deployment exists and
offers that release.
From then on the instance is the customer’s: its owner and organization decide who reaches it, and you have no path into its data.
6. See who uses it
Section titled “6. See who uses it”meroctl raw get /deployments/<id>/orgs{ "deployment": "3f2a…", "orgs": [{ "orgId": "…", "name": "Globex", "memberCount": 14, "activeMembers30d": 9, "instances": 2, "apps": [{ "app": "chat", "version": "1.0.0" }], "firstCreatedAt": "2026-11-02T09:12:44Z" }], "outsideOrgs": 3}For the deployment’s managers only, from an unbound session (your app’s own
frontend session cannot read it), on fresh state: each customer organization
with instances from this deployment, how many, which of your apps and the
releases they run, and two counts of people. Never who the members are,
teams, instance names or data. Instances in personal organizations are only
counted (outsideOrgs). Another deployment’s customers never appear, even on
the same release. GET /deployments lists the deployments you manage.
Who is billed
Section titled “Who is billed”Each instance’s usage series carries two labels (Usage metering):
org, the payer: the customer’s organization. Its quotas count the instance’s files and compute runs, and the instance counts against its owner’s app quota.developer: who Calimero Cloud bills, the deployment’s owner (your organization’s id, oraccount:<id>). An instance created without a deployment has its organization as developer.
A cluster may cap a developer’s fuel per day (maxFuelPerDeveloperPerDay),
summed over every instance of your deployments: past it, client calls to
them answer 429 QUOTA_EXCEEDED until the next UTC day. GET /auth/usage
(your account) and GET /orgs/{org}/usage (your organization, for its
admins) show today’s developerFuelToday and developerWritesToday.
Shipping a new version
Section titled “Shipping a new version”Offering a newer release (step 3 again) changes what new instances get. Existing ones move by an upgrade, and who upgrades them is what the release’s manifest declares:
upgrades: owner(the default): each customer upgrades their own.upgrades: publisher: your organization, once it has bound the package’s key, upgrades every customer’s instance at once with a publisher run:POST /orgs/{org}/packages/chat/upgrade { version }, with one passkey step-up, thenGET /orgs/{org}/upgrades/{run}to follow it.
Withdrawing an offer
Section titled “Withdrawing an offer”meroctl raw delete /deployments/<id>/apps/chat --dev-passkey ./dev-passkey.jsonmeroctl raw delete /deployments/<id> --dev-passkey ./dev-passkey.jsonWithdrawing is refused while any instance created from this deployment still runs a release of that app’s package: a customer’s instance is never left pointing at an offer that is gone. The release stays installed. A deployment is deleted once it offers nothing and no instance came from it, and an organization that owns a deployment cannot be deleted before it.
Not done yet
Section titled “Not done yet”- Migration. Releases installed before deployments existed are not moved into a deployment; offer them in one yourself.
- Transfer. A deployment cannot be handed to another owner yet: create a new one under the new owner and offer the same releases.
- Lockout guard. Nothing yet stops an organization-owned deployment from
being left with no manager able to sign (the design asks for at least two
managers). Keep more than one
MANAGE_APPLICATIONholder in the owning organization, each with more than one passkey. - The consent page does not name the deployment an instance came from.