Skip to content

App instance lifecycle

An app instance (what meroctl still calls a context) is one running copy of a bundle with its own state and its own Raft group. Its directory record lives in the system group; its state lives only on its replicas.

The node that takes the request does the checks it can, picks the replicas and proposes one system command; the system log is where the decision is made, so two racing creates cannot both exceed a quota. Nothing about the app’s state exists until its own group forms on the replicas and its first leader runs init, which is why the answer waits for that entry.

POST /apps { name, package+version | bundle, service?, initArgs?, org?, access?, confirmMethods?, metadata? } by a passkey session, an unscoped API key, or (for its own package) a package-bound session:

  1. Resolve the bundle. A package release (installed from the registry first if the cluster lacks it) or, on a development cluster, a raw bundle hash.
  2. Place. The node picks --replicas nodes (3 by default) among those whose attested mero-tee release is approved to hold the owner’s (or the organization’s) apps (Release approvals). No such node: 503. The replicas span as many zones as those nodes are in (the zone that POST /ops/nodes registered), so an app outlives the loss of a zone whenever the cluster has three: the least-loaded node (fewest replicas, then the lower id) of each zone first, least-loaded zones first, and once every zone holds one, the remaining replicas by load alone. Nodes without a zone (every node from --peers until it is registered with one) count as one unknown zone together, since nothing says they are apart; a cluster with no zones is placed by load alone. Moves (/ops/apps/{app}/replicas) go where the operator says.
  3. Record. CreateApp applies in the system group: the bundle must be known, the owner within their app quota, a member (at least member) of the organization if one is named, and a writer of the team for access: team. It assigns the app its group number. The default access is org inside an organization and owner outside one.
  4. Start the group. Each replica’s reconcile loop starts the group; the first replica in the list forms it with all replicas as voters.
  5. Init. The group’s first leader runs init with initArgs as the owner and appends the Init entry. The create request waits for it (up to 30 seconds) and answers { appId, contextId, group, replicas }, or 422 INIT_FAILED with the app’s error, recorded on the instance (AppState::init_error) rather than leaving a dead app.

Calls, views and events go through any node (The write path, Reads). The API takes the app id or its context id (hex(SHA-256(app_id))), the id the app itself sees and hands to others.

Change Route Who
Access rule POST /apps/{app}/access The owner, with a passkey session
Metadata (a title, an icon; at most 16 KiB) POST /apps/{app}/metadata The owner, or an admin of its organization
An alias for the app PUT /auth/aliases/{alias} Any account, for an app it reaches; aliases are the account’s own

The operator can move an instance to another replica set (POST /ops/apps/{app}/replicas): new nodes join the group as learners, catch up, and become voters, and nodes that left stop the group and delete its data (Replica moves). A move only lands on nodes whose release the app’s approver has approved.

POST /apps/{app}/upgrade switches the instance to another release and runs its migration; the directory records the new bundle only once the upgrade succeeded (Upgrades & migrations).

DELETE /apps/{app} by its owner, or an admin of its organization, with a passkey session. DeleteApp removes the directory record; each replica’s reconcile loop then stops the group and deletes its data from that node.

An app is also deleted with its organization (DELETE /orgs/{org}, the owner with a passkey step-up) and with the team it is deployed to (DeleteTeam takes the team, the teams below it, and their apps).