|
| 1 | +--- |
| 2 | +title: Install modes — managed or an unmanaged copy |
| 3 | +navTitle: Install Modes |
| 4 | +description: Declare which install modes a package permits with installModes, then pick one at install time. What a managed install keeps, and what an unmanaged copy keeps and drops, the vendor's protection locks included. |
| 5 | +--- |
| 6 | + |
| 7 | +A package reaches a running runtime in one of two **install modes** |
| 8 | +([ADR-0131](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0131-total-organization-ownership-no-null-organization-id.md) |
| 9 | +D6). The package author declares which modes it permits; whoever installs it |
| 10 | +picks one of those. |
| 11 | + |
| 12 | +| | **managed** (the default) | **unmanaged** | |
| 13 | +|:---|:---|:---| |
| 14 | +| What is installed | the package, registered as code | a copy of every metadata item, written once into the environment | |
| 15 | +| Who is the authority | the package | this environment: the copy is its own metadata | |
| 16 | +| Editing | nothing it ships is edited in place; you customise beside it | every copied item is edited like anything authored in Studio | |
| 17 | +| Later versions | install the next version | the copy follows no upgrade | |
| 18 | +| The vendor's protection locks | kept and enforced | not copied; the install lists each lock it drops | |
| 19 | +| Tenancy posture | any | refused where the deployment walls organizations (`group`, `isolated`) | |
| 20 | + |
| 21 | +--- |
| 22 | + |
| 23 | +## Declare the modes a package permits |
| 24 | + |
| 25 | +The manifest key is `installModes`: |
| 26 | + |
| 27 | +```ts |
| 28 | +import { defineStack } from '@objectstack/spec'; |
| 29 | + |
| 30 | +export default defineStack({ |
| 31 | + manifest: { |
| 32 | + id: 'com.acme.crm', |
| 33 | + namespace: 'crm', |
| 34 | + version: '1.0.0', |
| 35 | + type: 'app', |
| 36 | + name: 'Acme CRM', |
| 37 | + installModes: ['managed', 'unmanaged'], |
| 38 | + }, |
| 39 | + // objects, views, dashboards, … |
| 40 | +}); |
| 41 | +``` |
| 42 | + |
| 43 | +- **Leave it out and the package permits `managed` only.** The key has no |
| 44 | + parse-time default: absent means `['managed']`, and nothing is stamped into |
| 45 | + the stored manifest. |
| 46 | +- **Name at least one mode.** An empty list is refused; omit the key instead. |
| 47 | + A value other than `managed` or `unmanaged` is refused too. |
| 48 | +- **`unmanaged` alone is legal.** It suits a starter app a customer takes over |
| 49 | + wholesale. A deployment that walls organizations cannot install such a |
| 50 | + package at all. |
| 51 | + |
| 52 | +### Permitting `unmanaged` is checked when you build |
| 53 | + |
| 54 | +An unmanaged install copies every item through the same save door a Studio edit |
| 55 | +takes, or copies nothing; there is no partial copy. So `defineStack`, and with |
| 56 | +it `os validate` and `os build`, refuses a package that permits `unmanaged` |
| 57 | +while it carries something that door cannot write as environment metadata. The |
| 58 | +refusal is `STACK_UNMANAGED_INSTALL_UNCOPYABLE`, with one finding per key: |
| 59 | + |
| 60 | +- items of a metadata type the metadata-type registry declares |
| 61 | + runtime-uncreatable (`allowRuntimeCreate: false`); |
| 62 | +- code: `functions`, `plugins` and the other keys that carry or describe code; |
| 63 | +- extensions of another package's items: `objectExtensions`, |
| 64 | + `picklistExtensions`, `navigationContributions`, `contributes`; |
| 65 | +- `packages`, which describes an artifact of several packages, while one |
| 66 | + unmanaged install copies one package. |
| 67 | + |
| 68 | +The fix is to stop permitting `unmanaged`, or to move what cannot be copied |
| 69 | +out of the package. The install door judges the same body with the same |
| 70 | +classification, so a package that passes `os validate` is not refused there |
| 71 | +for a key you were never told about. |
| 72 | + |
| 73 | +--- |
| 74 | + |
| 75 | +## Pick the mode at install time |
| 76 | + |
| 77 | +```bash |
| 78 | +# managed, the default |
| 79 | +os package install ./dist/objectstack.json |
| 80 | + |
| 81 | +# an unmanaged copy, from a local artifact or from the runtime's catalog |
| 82 | +os package install ./dist/objectstack.json --install-mode unmanaged |
| 83 | +os package install com.acme.crm --version 1.2.0 --install-mode unmanaged |
| 84 | +``` |
| 85 | + |
| 86 | +`--install-mode` sets the `installMode` field of |
| 87 | +`POST /api/v1/marketplace/install-local`; without it no field is sent and the |
| 88 | +runtime installs managed. The door judges the mode before it writes anything: |
| 89 | + |
| 90 | +| The request | The answer | |
| 91 | +|:---|:---| |
| 92 | +| A mode the package's `installModes` does not permit | `422 INSTALL_MODE_NOT_PERMITTED`, naming what it permits | |
| 93 | +| `unmanaged` on a deployment whose requested tenancy posture walls organizations (`group`, `isolated`) | `409 UNMANAGED_INSTALL_WALLED_POSTURE`, naming the posture | |
| 94 | +| `managed`, while an unmanaged copy of the package holds its namespace | `409 RESOURCE_CONFLICT`, naming the copy's base | |
| 95 | + |
| 96 | +A runtime that predates install modes ignores the field and installs the |
| 97 | +package managed. If you asked for `unmanaged`, the CLI says so and exits `1`. |
| 98 | +The install door itself, credentials included, is described under |
| 99 | +[`os package install`](/docs/deployment/cli#os-package-install). |
| 100 | + |
| 101 | +--- |
| 102 | + |
| 103 | +## What a managed install keeps |
| 104 | + |
| 105 | +The package is registered as code and stays the authority. Its install is |
| 106 | +recorded on the runtime host, so it is registered again at every boot, and a |
| 107 | +later version is installed over it. |
| 108 | + |
| 109 | +**Nothing it ships is edited in place.** What a customer changes lives beside |
| 110 | +the package, as environment-level state: |
| 111 | + |
| 112 | +- a `view`, `dashboard`, `report`, `translation` or `email_template` is |
| 113 | + overlaid by saving one under the same name, and removing the overlay brings |
| 114 | + the package's own back; |
| 115 | +- a flow is switched off, or cloned under a new name and changed there; |
| 116 | +- a save onto any other item the package ships is refused, and the refusal |
| 117 | + names the route its type has. |
| 118 | + |
| 119 | +**The vendor's protection locks hold.** A package locks an item in either |
| 120 | +[ADR-0010](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0010-metadata-protection-model.md) |
| 121 | +spelling: the `_lock` envelope (`_lock`, `_lockReason`, `_lockDocsUrl`), or the |
| 122 | +author-facing `protection: { lock, reason, docsUrl }` block, which the install |
| 123 | +translates into the envelope with `_lockSource: 'package'`. The item is served |
| 124 | +locked with the vendor's reason, and the write doors refuse what the lock |
| 125 | +forbids with `403 ITEM_LOCKED`: `no-overlay` refuses a save, `no-delete` a |
| 126 | +delete, and `full` both. |
| 127 | + |
| 128 | +--- |
| 129 | + |
| 130 | +## What an unmanaged copy keeps, and what it drops |
| 131 | + |
| 132 | +The package's metadata is copied **once** into a new writable base, |
| 133 | +`<package id>-copy`, which keeps the package's namespace. Every item is written |
| 134 | +through the save door a Studio edit takes, environment-wide, and is served as |
| 135 | +environment-authored metadata: the next save edits it. |
| 136 | + |
| 137 | +- **Translations are copied.** Each locale of each bundle becomes one |
| 138 | + `translation` item named `<namespace>_<locale>`, the locale lower-cased with |
| 139 | + its hyphens underscored (`crm_zh_cn`). |
| 140 | +- **Sample data is not loaded.** When the package declares any, the install |
| 141 | + answer says so, with `seeded.mode: 'skipped'`. |
| 142 | +- **Nothing is registered under the package's own id.** No install record, |
| 143 | + nothing re-registered at boot, nothing upgraded. A managed install of the |
| 144 | + package is refused while the copy holds its namespace, and a second unmanaged |
| 145 | + install is refused because the base already exists. |
| 146 | +- **All or nothing.** Every refusal is judged before the base exists, including |
| 147 | + a name the environment or a managed package already holds. A write that fails |
| 148 | + deletes the base, so there is never a half-copied app. |
| 149 | + |
| 150 | +### The copy keeps no lock, in either spelling |
| 151 | + |
| 152 | +The package permitted `unmanaged` through `installModes`, so its vendor already |
| 153 | +chose to let environments take editable copies. Once the package is no longer |
| 154 | +the authority, a lock whose source is the package has nothing to stand on. So |
| 155 | +every copied item is written: |
| 156 | + |
| 157 | +- without `_lock`, `_lockReason`, `_lockSource` and `_lockDocsUrl`; |
| 158 | +- without its `protection` block, on a type whose schema declares one. |
| 159 | + |
| 160 | +Every copied item is then served unlocked, and its next save is accepted. |
| 161 | + |
| 162 | +Two cases are left to the save door exactly as before, and either refuses the |
| 163 | +whole install, which rolls back: |
| 164 | + |
| 165 | +- a lock value the declarations refuse, such as `_lock: 'locked'`, a reason |
| 166 | + that is not a string, or a malformed `protection` block, is not stripped: the |
| 167 | + save door answers `422 INVALID_METADATA`; |
| 168 | +- a `protection` block on a type whose schema does not declare one (`action`, |
| 169 | + `hook`, `page`) is left in place, and the save door refuses it as an |
| 170 | + unrecognised key. |
| 171 | + |
| 172 | +### The install lists each lock it drops |
| 173 | + |
| 174 | +The install answer carries `strippedLocks`: one entry per dropped lock that |
| 175 | +binds (`no-overlay`, `no-delete` or `full`), in the order the items were |
| 176 | +written. |
| 177 | + |
| 178 | +```json |
| 179 | +"strippedLocks": [ |
| 180 | + { "type": "dashboard", "name": "pipeline_dashboard", "lock": "full", "reason": "The pipeline figures feed the board pack — edit the source package instead." }, |
| 181 | + { "type": "app", "name": "crm_app", "lock": "no-delete" } |
| 182 | +] |
| 183 | +``` |
| 184 | + |
| 185 | +- `reason` is the vendor's text, verbatim, and is absent when the package gave |
| 186 | + none. |
| 187 | +- An item that carries both spellings is listed once, with the lock a managed |
| 188 | + install would bind: the `protection` block's. |
| 189 | +- A `none` lock declares nothing and is not listed. |
| 190 | +- The list is always present, and empty when the package locked nothing. |
| 191 | + |
| 192 | +`os package install --install-mode unmanaged` prints the list under |
| 193 | +`Locks removed`, one line per lock as `<type>.<name>: <lock> — <reason>`, the |
| 194 | +dash and reason left off when there is none: |
| 195 | + |
| 196 | +```text |
| 197 | + Locks removed: 2 — the package locked these items; the copy is editable |
| 198 | + dashboard.pipeline_dashboard: full — The pipeline figures feed the board pack — edit the source package instead. |
| 199 | + app.crm_app: no-delete |
| 200 | +``` |
| 201 | + |
| 202 | +Read it before you edit: it is why the vendor locked each item. |
| 203 | + |
| 204 | +--- |
| 205 | + |
| 206 | +## Choosing what to permit |
| 207 | + |
| 208 | +- **Permit `unmanaged`** when the package is a starting point a customer should |
| 209 | + be able to take over and change freely. |
| 210 | +- **Keep it managed only** when you must keep shipping upgrades to it, or when |
| 211 | + some item must never be edited by any customer. An unmanaged copy drops every |
| 212 | + lock, so a lock is no reason to permit the copy; it is a reason to withhold |
| 213 | + it. |
| 214 | + |
| 215 | +## Related |
| 216 | + |
| 217 | +- [Publish, Versioning & Preview](/docs/deployment/publish-and-preview): compile, |
| 218 | + publish and install a version |
| 219 | +- [`os package install`](/docs/deployment/cli#os-package-install): the flag-level |
| 220 | + reference |
| 221 | +- [Package manifest reference](/docs/references/kernel/manifest) |
| 222 | +- [Package lifecycle reference](/docs/references/api/package-lifecycle): the |
| 223 | + import's request and answer, `strippedLocks` included |
| 224 | +- [Tenancy Modes](/docs/deployment/tenancy-modes) |
0 commit comments