Skip to main content

The life of a tenant's and an app's data

Companion to: operations.md (reference detail, §9), commands.md (the commands), tenant-backup.md (for a tenant's administrator)


1. Read this first​

  • A tenant is one organization's workspace. An app is something the tenant installed. Both own data: databases, files, buckets, people, passwords.
  • Eight acts touch that data. Four make or bring back, four copy or take away. This page says what each act does with each kind of thing.
  • A backup and an export are the same thing. Both produce a bundle: one encrypted set of files that holds a tenant's data at one moment.
  • Uninstalling an app keeps all of its data. Purging destroys it. Retiring a tenant keeps its data. Deleting destroys it. Destroying is always a second, separate act.
  • A backup holds the data of installed apps, the data uninstalled apps left, and what is the tenant's own: its people, its desktop, its mailboxes and the access rights that follow from nothing else.
  • §7 lists the limits of these acts.
ActWhat it is forWho starts it, and whereDoes it destroy anything?
Create a tenantA new, empty workspaceCluster administrator. Console, Tenants; or kubectl gentian tenants createNo
Install an appAdd an app to a tenant, with empty storesTenant administrator. App Store app; or kubectl gentian apps installNo
Back up (export)Copy the tenant's data into a bundleTenant administrator. Admin Console, Export; a schedule; or a TenantExport object. Download hands out the same bundle as one fileNo. Each app is paused while it is copied
RestorePut a bundle's data back into the tenant it came fromAdministratorYes: what was written since the backup is replaced
ImportMake a tenant again from a bundle, under the name it carries, here or on another clusterCluster administrator. kubectl gentian tenants importNo. The new tenant has its own names; nothing of another tenant is touched
Uninstall an appTake the app away, keep its dataTenant administrator. App Store app; or kubectl gentian apps uninstallNo
Purge an appDestroy what an uninstalled app leftTenant administrator. App Store app; or apps uninstall --purgeYes, for good
Retire a tenantTake the tenant away, keep its dataCluster administrator. Console, Retire; or kubectl gentian tenants retireNo. People can no longer sign in
Delete a tenantRetire it and destroy its dataCluster administrator. Retire with "delete its data"; or tenants retire --purgeYes, for good

2. One list, one order​

The platform keeps one list of the kinds of things an app and a tenant own. That list is the inventory. Every act reads it, so that what a backup copies is what a purge destroys. The list is in the code (internal/backup/teardown.go): what an app owns (AppKinds) and what a tenant owns that is no app's (TenantOwned). Tests fail when a kind is added without saying what each act does with it, and when a kind of thing a bundle can hold is copied by a backup and not put back by a restore, or destroyed by a deletion that the list says keeps it.

Order of creation: the records, stored credentials, the access group, the sign-in scope, databases, bucket, cache user, model key, sign-in client, the running app, and with it the files. Order of removal: the same list backwards. Nothing is destroyed while something made after it still needs it.

Every act is built from the same few primitives, each implemented once: declare (the director writes a tenant's manifest to the deployments repository as a signed commit; Argo CD and the operator do the rest), inventory (the list above, read from a tenant's apps and their profiles), capture (TenantExport: pause one app at a time, copy each inventoried store into the bundle, write the manifest last), load (TenantRestore: the inverse, into a tenant that exists) and destroy (deletion under deletionPolicy: Delete, over the same inventory). Import is declare plus load; deleting a tenant is retiring it plus destroy. No act introduces machinery the others lack, which is what keeps them agreeing about what "all of the data" is: a store that a backup copies is a store a deletion destroys, and the tests on the inventory hold them to it. Anything further — a backup on a timer, a recovery of the newest bundle, a migration from or to another product — is a composition of these acts, and whatever builds it calls the director like any other client (data-ownership.md §4, §5).

An app that was uninstalled with its data kept is in the table twice: its data is "kept" by the uninstall, and a backup taken afterwards copies it as it copies an installed app's — the same databases, bucket and files, marked as an uninstalled app's. A restore puts it back into what the tenant still holds for the app and installs nothing; an import brings it into stores made for it, still uninstalled. Data that was purged is in no later backup, and a restore does not bring it back (§4).

How to read the table: made = created empty. copied = written into the bundle. put back = the bundle's content replaces what is there. kept = left exactly as it is. removed = taken away, nothing a person stored goes with it. destroyed = deleted with what it held. "—" = not touched.

Kind of thingCreate / installBack upRestoreImportUninstall appPurge appRetire tenantDelete tenant
What an app owns
The running app (workloads)madenot copied; the build is noted—; the installed build is checkedmade newremoved—removedremoved, first
Files on volumesmade by the appcopiedput back on top of what is theremade new, then filledkeptdestroyedkeptdestroyed
PostgreSQL database and login, with any further database the app made itselfmadecopiedput backmade new, then filled; a further database comes back under the tenant's database name herekeptdestroyedkeptdestroyed
MariaDB database and user, with any further database named <database>_…madecopiedput backmade new, then filledkeptdestroyedkeptdestroyed
Bucket, with its user and access rulemadeobjects copiedbucket made if missing, objects put backmade new, then filledkeptdestroyedkeptdestroyed
Cache usermadenot copied—made newkeptremoved; its keys staykeptremoved; its keys stay
The app's own secrets (in the vault): what its profile has the platform generate, and its data is written withgeneratedcopied, encrypted by the operatorset to the bundle's values; the app is handed them and restartedset to the bundle's values, at the new tenant's own pathskeptdestroyedkeptdestroyed
Its other stored credentials (in the vault): a database's, a bucket's, a cache's, a sign-in client's, a model keygeneratednever copied—generated newkeptdestroyedkeptdestroyed
Access group and who is in itmadecopied, inside the realmkept under its id, its roles and who is in it put backmade by the platform; who is in it put backkeptdestroyedkeptdestroyed
Sign-in scopemadenot copied—made newkeptdestroyedkeptdestroyed
Sign-in clientmadecopied, inside the realm, its secret maskedkept: the bundle's is not put over itmade new by the platform, never from the bundleremoved—removed, with the running appdestroyed
Model key (an app that declared the model gateway, on a cluster that serves models)madenot copied—made newkeptremovedkeptremoved
Provisioning recordswrittennot copied—written newkeptdestroyed, lastkeptdestroyed, last
What the cluster owns
The app's definition in git (its profile and what comes with it)committednot copied; the build is noted—fetched at the build the bundle notes and committed first, or the import is refusedkeptkeptkeptkept
What a tenant owns that is no app's
The tenant's entry in gitcommittedits settings are copied—committed from the bundlethe app is taken off it—removedremoved
The namespacemade——made new——keptdestroyed
The realm: people, groups, rolesmadecopiedclients, roles and groups here kept under their ids, groups given back their roles and attributes; the tenant's missing ones added (a client with a new secret, at this domain); missing people addedmade new, then filled——kept, switched offdestroyed
People's passwordsset by each personnever copiednot put backnot brought——keptdestroyed
The desktop's databasemadecopiedput backmade new, then filled——keptdestroyed (the platform tenant's is emptied: the database itself is the cluster's)
The tenant's own stored credentials (in the vault)generatednever copied—generated new——keptdestroyed
The tenant's link into the platform's sign-inmadenot copied—made new——keptremoved
The team at the model gatewaymadenot copied—made new——keptremoved
Mail routing, mail logins, mail DNS recordsmadenot copied—made new——removedremoved
Mailboxes (a cluster that runs its own mail server; a mail domain that is the tenant's alone)filled by usecopiedput back on top of what is thereput back into the new tenant's mail domain——keptdestroyed, after the mail routing is gone
Archived mailboxes of removed people (the same clusters and domains)made when a person is removed and whoever removed them chose archivecopied, as archivedput back as archived, never into a live mailboxput back as archived into the new tenant's mail domain, and put on record there——keptdestroyed, with the mailboxes
Web addresses: routes, certificate, DNS recordsmadenot copied—made newthe app's route removed—removedremoved
Access rights that follow from the tenant, its apps and its people (the rights store)derivednot copiedderived againderived againthe app's removed—the apps' removedthe apps' removed; the tenant's own entries and who is its member stay (§7)
Access rights that follow from nothing else: a right granted beyond the defaults, a default withdrawnwritten in the storecopied; the manifest is signed by the clusterput back, after the rest is derived, from a bundle this cluster signed and took; from any other only what was withdrawnas a restore: granted ones (a person's among them) only from this cluster's own bundle of a tenant of the same name, otherwise not brought, and namedkept—keptnot removed (§7)
The backup bucket and the bundles in itmade by the first backupbundles are written hereread———keptdestroyed, unless the tenant keeps its bundles
The list of what was provisionedwrittennot copied—written newkeptshortened, kind by kindkeptdestroyed, last

The app's definition is the cluster's, shared by every tenant that installs the app. No act on a tenant or an app removes it (custom-catalogs.md §6 says what does).

3. The life of an app's data​

4. Each pair​

Create and delete. Deleting a tenant removes what creating it and installing its apps made: first the running apps, then every store of every app the tenant ever had (also the uninstalled ones), then the realm, the namespace, the vault entries and last the records. It uses the same steps a purge of one app uses. The tenant's mailboxes go once its mail routing is gone, so that nothing is delivered into a mailbox being destroyed, and the deletion waits for that step as it waits for a store's. Two things are left behind: the keys an app wrote into the shared cache, and the tenant's entries in the rights store other than its apps' (§7).

Removing a person is not one of the eight acts, and it is the one moment a single mailbox is decided about. Whoever removes the person chooses: archive moves the mailbox out of the live mail tree into /var/mail/.archive/<domain>/, where no address opens it; delete destroys it. An archived mailbox is then the tenant's like its mailboxes: in every backup, put back as archived, destroyed with the tenant, and deletable before that from the Admin Console. mail.md §5c has the detail.

Mailboxes on a domain the tenant shares with the cluster are not the tenant's alone: on a single-tenancy cluster the user tenant's addresses are on the cluster's own domain, with the administrators'. Those are neither copied nor destroyed, and a backup says so.

A tenant is refused, created or imported, when its realm, its database prefix or its bucket prefix is already another tenant's. Two tenants with one of these in common share the thing itself.

Back up and restore. A restore puts back exactly what the bundle says it holds, app by app. An app is restored whole or not at all, and every app left out is named with the reason. Databases and buckets are replaced. Files are written on top: a file created after the backup stays. Mailboxes too: a message the bundle holds and the mailbox lacks is added, with its flags and in its folder, and mail that arrived since stays.

An app the bundle holds as uninstalled is put back as that: into the databases, bucket and volumes the tenant still holds for it, with nothing installed and nothing paused. If the tenant holds none of it any more, it was purged since, and it is not restored; the result names it. If the app has been installed again, the data is the installed app's and is restored like any app's, which needs skipVersionCheck: no build is on record for data an uninstalled app left.

While a restore runs, every app it has reached is unreachable: from before anything of the app is changed until the restore has ended or is deleted, its addresses answer 503 "being restored" with Retry-After, and the restore names the app under heldBack. Nobody signs in to an app whose data is back but whose post-restore commands, rollout or people are not yet (operations.md §9.4).

The access rights a bundle holds are written last, after the people are back and the operator has derived the rest. A restore changes no stored credential and brings back no password. A database is replaced only when it is the app's own; one that belongs to another app or tenant is refused. A backup and a restore say what they did not do: the result of a backup names what the bundle does not hold, and a step that fails fails the whole act with its reason.

A restore into a tenant that exists is asked of the director by the tenant's administrator (can_set_policy, the right its backup policy is written under) or the cluster's (can_configure, the right an import needs), and the operator starts it, as it starts a backup; nothing is committed. The bundle is one of the tenant's own backups, named by its name, or one uploaded to the cluster, and its manifest has to name the tenant restored into: a bundle of another tenant is refused before anything starts, whoever uploaded it (commands.md §12).

A bundle states its format in its manifest (schemaVersion). The format written is 5. Format 3 holds the data of apps uninstalled with their data kept, the mailboxes, and the access rights that follow from nothing else. Format 4 adds each app's own secrets; format 5 adds each app's authorization store. A restore reads formats 1 to 5, puts back what the bundle holds, and refuses a format newer than its own. A bundle older than format 4 holds no secrets: a restore of one changes no stored secret, and says so.

Download. The bundle leaves the cluster as one .gentian file, the artifacts under its prefix streamed as a tar, still encrypted (GET /v1/tenants/{t}/backups/{name}/download). The download is authenticated and goes through the director, never a presigned URL: the director is the enforcement point for the tenant's data (AD-1), and a link that works without a session is a session nobody can revoke.

Import = create + restore. The director first makes sure the cluster has the definition of every app the bundle's tenant lists: already there at the same build, or fetched at that build from one of the cluster's catalogs. If one cannot be had, the import is refused, names it, and has changed nothing. The one exception is a platform component of an earlier release that this one no longer has (the per-tenant app-store-me): it is left out of the new tenant, its data is not restored, and the import's result lists it under skipped with the reason. Then it commits the new tenant by the code a normal create uses, waits until the platform has made it and its apps empty, and starts a restore that says the tenant was made new for the bundle (TenantRestore.spec.intoNewTenant).

The new tenant takes its settings from the bundle and its names from its own name: its realm, its database prefix and its bucket prefix. Three settings are not the bundle's: the new tenant keeps its data when retired (deletionPolicy: Retain) whatever the old one said, it has no catalogs of its own, and a namespace of another name that the old tenant was placed in is not carried. An import does not rename: the tenant is made under the name its bundle carries, and a request that names another is refused. It may be on another domain, which is how a tenant moves with its data, and says so. A custom domain (TenantDomain) the tenant had is kept unless the import is asked not to: it is committed with the tenant, refused before anything is written when another tenant here holds it, and the restore waits until the tenant is on it, so the domain is the same and nothing below applies; its DNS is the person's to point at the new cluster. Not kept, it can be declared again with kubectl gentian tenants domain. On another domain the people's addresses at the old mail domain come back at the new one under the same name, and a username that was such an address with them, so that each opens the mailbox put back under that name; other addresses are left, and so is every address of a bundle that does not record its domain. An app whose profile binds its data to its domain (spec.backup.restore.domainBound) is not restored onto another: it stays as installed, and the restore names it. The domain compared is the tenant's when the restore runs. A TenantRestore is held to the same rule: one whose bundle is of another tenant is refused before anything is changed (reason OtherTenant). A bundle meant for another name is rewritten into it, by a tool that does so, before it is restored. Where the tenant's database or bucket prefix here is not the one it had where the bundle was taken, each store still comes back into the tenant's own: an app's database, bucket and any database it made itself under the names here.

One thing is different from a restore, because the tenant is new, and follows from intoNewTenant: the data of apps the bundle holds as uninstalled comes with stores made for it — the database and its role, the bucket, the volume claims — and the apps stay uninstalled; an app whose definition this cluster does not have is left out and named.

Access rights follow one rule for an import and a restore alike. A right withdrawn in the old tenant is withdrawn in the new one. A right granted there, a person's among them, is granted only when the bundle provably comes from this cluster: its manifest signed with this cluster's key, taken on a cluster of this cluster's id (security/design.md §6.4). From any other bundle — unsigned, or another cluster's — each is named in the result, for whoever may grant it here, and status.rights.provenance says why.

Where a bundle comes from. An upload; a bucket of the cluster's own storage; or a bundle source, storage outside the cluster declared like a backup destination: its address a commit (BundleSource), its keys set at the custodian into the vault, read by the platform alone. A tenant's own backup bucket, its policy's destination and the stores its administrator declared (<tenant>.<name>, its keys in the tenant's part of the vault, which its exports can be written to) are its sources for its recovery, and list only its own bundles. A bundle is named by source and prefix, and the platform finds the address and the keys. Listing reads each bundle's plaintext bundle-info.json; an import or a restore reads the bundle where it is; a fetch copies it into gentian-imports, from where it is downloaded and handled as an upload (commands.md §12a).

Clean-up of the import storage. What is in gentian-imports, uploaded or fetched, goes once the restore or import that read it has run, and in any case seven days after it was put there: the operator sweeps the bucket hourly by the record each upload and fetch writes beside the bundle (.records/<prefix>.json: tenant, kind, time), sparing a bundle a restore that has not ended reads. A tenant's administrator may put one bundle of at most 20 GiB there, and 50 GiB at once for the tenant, uploads and fetches together (constants in internal/bundlestore/imports.go); past either the request is refused with 413 and nothing is kept. The cluster's administrator is not bounded.

The import is recorded in git beside the new tenant until it has finished. A director that restarts goes on with it. It needs the bundle's key again only if the restore had not yet started: the key is never written down.

An import does not depend on the source cluster. The credentials the platform mints for an app — its database's, its bucket's, its cache's, its sign-in client's — are made anew by the importing cluster, and where an app has cached one in its own store the profile's post-restore commands rewrite it; only the app's own secrets travel, because the data is unreadable without them (§5). The alternative, a recovery kit that is the cluster's identity so that a new server importing it becomes the old cluster, is not taken: it would bind a tenant's portability to its provider's kit, which is the dependency the bundle exists to remove (data-ownership.md §2).

Uninstall and purge. This is the critical difference. Uninstalling removes the running app and its sign-in client. Everything the app stored stays: databases, bucket, files, stored credentials, cache user, the access group with its members. The app is then retained: gone from the tenant's screen, data still there. Installing it again finds all of it. Purging is asked for separately, is refused while the app is still installed, and destroys everything that was retained. It cannot be undone. Until then the retained data is in every backup of the tenant, as an uninstalled app's; a backup asked for some apps only names the ones it left out.

Retire and delete. Every tenant starts with the policy keep the data. Retiring removes the tenant from git; its apps stop, its addresses and mail routing go, its realm is switched off, and all data stays. Creating a tenant of the same name again finds it, and the rights, memberships and cache keys the old tenant left, since they address a tenant by its name. So a new tenant under a retired tenant's name, created or imported, is refused unless the cluster administrator approves the reuse (--reuse-name): recovering that tenant is the case for it, and the commit records the approval. Deleting first changes the policy to delete the data, waits until the cluster has taken that in, and only then removes the tenant. The bundles go too, unless the tenant was told to keep them.

5. What a backup does not contain, and what to redo​

A bundle holds no person's password, and of the stored credentials one kind only: each app's own secrets, the ones its profile has the platform make (generated secrets and declared keys) and its data is encrypted or signed with. Without them the data a bundle brings back is unreadable wherever they are made anew. They are in the bundle as secrets/<app>.json.age, encrypted by the operator itself with the key of the rest of the bundle, and in no other place: not in the manifest, not in bundle-info.json, not in a Job (security/design.md §6.4). Every other stored credential stays out: it would put every secret of a tenant into a file that leaves the cluster, and none of them is needed to read restored data.

Whoever can open a bundle reads these secrets with the data: the holder of the key it was encrypted to. For the cluster's backup key that is the holder of the recovery kit and, where the key is also kept in OpenBao (spec.backup.escrowIdentity, the default), a platform administrator. A tenant's own key is made by a person, on the command line or in the browser, and is on the cluster only if that person had the custodian keep a copy (security/design.md §6.4).

The cluster signs a bundle's manifest and not its artifacts. The signature decides whether granted rights come back, nothing else: a restore cannot tell who made the data it is given, and sets the secrets of the apps it restores to the values that bundle holds. Restore only a bundle whose origin you know (security/design.md §6.4).

After a restore:

  • Send every member a password reset. People come back without passwords.
  • Re-enter credentials a person typed in after the backup was taken and that were since lost: a repository password, a mail relay's, an API key.
  • Check each app named under "not restored" and act on the reason given.

After an import, also:

  • Re-enter every credential a person had typed in. The new tenant has fresh, generated credentials and, but for its apps' own secrets, none of the old ones. An app that asks for one (secrets.supplied) waits for it before the restore begins, and the import's status names it.
  • Data an app encrypted with a secret the platform generated came with that secret, also on another cluster. Two cases are left to check: a bundle older than format 4 holds no secrets, and the result says so; and an app whose definition on this cluster does not declare a secret the bundle holds does not get it, and the result names it.
  • Set again what each app may use (app grants) and each tenant catalog.
  • The cache did not come. Mailboxes came only if both clusters run their own mail server; the result says when they did not.
  • Grant again each access right the result names as not brought.
  • On another domain (OutsideGuarantee): check each app's own settings for the old addresses, as far as its profile's post-restore commands did not rewrite them.
  • On another domain: declare the custom domain again, if the tenant had one and the import did not keep it (kubectl gentian tenants domain). The apps the result names as bound to their domain start empty.
  • Point DNS at the new cluster.

Never in a bundle: passwords, stored credentials other than the apps' own secrets, cache content, the access rights that are derived, the running apps themselves, the apps' definitions.

6. Where it can fail, and what you see​

ActWhen it failsWhat is leftWhat to do
Install, createThe tenant shows Degraded with the reasonWhat was made so farFix the cause; it continues by itself
Back upThe backup shows Failed with the reason; the paused app is started again. A step that cannot start is given up after a few attempts, in every stageA partial bundle, kept as evidence. It cannot be restoredTake a new backup; delete the failed one
RestoreRefused before it starts: nothing was changed. Failed later: Failed, complete: false, and the message names what is restored, what may be part restored and what was not touched. Every paused app is started again, also when the restore is deleted while it runsApps done so far are restored, the failed one may be half replaced, the rest untouched. One database is all-or-nothing; a bucket or a volume can be half writtenStart a new restore. An uploaded bundle is removed after a restore that ran, so upload it again
ImportRefused before it starts (an app's definition cannot be had, a name is taken): nothing was changed. Later: the status shows failed with the reason, or awaiting-key after a director restartThe new tenant exists, empty or part filledawaiting-key: ask for the import again with the key. failed: start a restore into it by hand, or delete it and import again
Purge an appThe answer names the step that failed, what is already destroyed and what was not triedExactly thatAsk again. It continues with what is left
Delete a tenantThe tenant stays Terminating. The reason is in the operator's logWhat was not yet destroyedNothing: it retries the failed step and never skips one

Purge and delete report success only when what they removed is verifiably gone.

7. Limits​

  1. Three things no act removes: the keys an app wrote into the shared cache, the entries in the rights store that say who is a tenant's member, and the entries on the tenant itself — the derived ones and any right granted beyond them. Consequence: they outlive a deleted tenant, and a tenant created later under the same name finds the old rights on its name.
  2. An import carries the privileges and published addresses the bundle's tenant had approved (spec.privileges, spec.exposures) as approved: the new tenant's entry in git is written from the bundle's settings, and these two are among them. Nobody on the importing cluster approved them. Rights in the rights store are not carried (§4); these are settings of the tenant's entry in git.
  3. An import brings an uninstalled app's data only if the cluster has the app's definition. It fetches the definitions of the apps the tenant lists, not of the ones it had uninstalled; such an app is named as not restored.
  4. A volume claim made for an uninstalled app's data is on the cluster's default storage class, whatever class it had where the bundle was taken.
  5. A tenant's mail domain that changed leaves the old domain's mailboxes behind. Backup and deletion go by the domain the tenant has now.
  6. Mail to a removed person's address is refused only while the record of the removal stands. The record of a deleted mailbox is removed after 30 days; after that, where the recipient policy is catchall, the default, mail to the address is accepted again and makes a new mailbox directory, which somebody given the address later finds (mail.md §5c).
  7. An import that is interrupted before its restore starts needs the bundle's key again. The status says awaiting-key.
  8. A tenant placed in a namespace of another name is not supported by backup and restore; both refuse it.
  9. At a tenant's deletion the web addresses are removed after the stores, not before them.

A restore creates every missing person enabled, also one who was disabled when the backup was taken. They have no password; a sign-in through another provider that the bundle recorded for them works. Each comes back under the id they had, so an app that keys an account by the sign-in's subject finds the same person; one already in the realm keeps the id they have there, and the restore's output says so.

Nothing the realm has is made anew from the bundle. Each client, role and group here keeps its id, which the platform's provider-keycloak resources know it by; Keycloak's OVERWRITE would delete it and make it under the bundle's, a role or group held by nobody, a client at the old addresses with the export's masked secret. A group here gets the bundle's roles added, and its attributes (the platform's keeps its own and gains only those it lacks). A client, role or group the tenant made and changed since keeps its changes; a role's description and composites are not reset.

What the platform declares is never made from the bundle, here or not: a client or group marked gentianos.io/managed-by: platform, or, in a bundle taken before the mark, named as the platform names its own (internal/keycloak PlatformClientPatterns and PlatformGroupPattern, and each catalog profile's client). The platform makes it; a copy from the bundle would take the name its resource makes it by. A client the tenant made that is no longer there is made with a new secret, which its user reads in the realm, at the bundle's addresses with each host of the tenant's old domain, or the cluster's, moved to the new one; where either domain is not known, as the bundle recorded them, and the result says so.

An external destination: a bundle records its endpoint, region, credential and addressing style (path, the default, or virtualHost), and its restore and deletion use them, whatever the policy says later. The steps of such a run that are in the tenant's namespace are given a way out to that destination alone while it runs; a run whose bundle stays in the platform's storage is given none.

Further technical limits (bundle format, MariaDB naming, cache keys) are in operations.md §9.6.

8. Pointers​