Shipping a New Version of Your App
Who this is for: developers who already have an app deployed and want to ship a change — a new build, a new type, a new status, a new setting. Scope: what happens today, command by command and pointer by pointer. No roadmap, no "will." Related: Building apps on Starhive (what the manifest is) · Starhive CLI (every command and flag).
1. The two pointers, and who follows which
An app has two pointers at any moment:
| Pointer | Moved by | Who resolves to it |
|---|---|---|
| latest deployed | every starhive app deploy | every developer of the app, on every workspace |
| published | starhive app promote | everyone else |
deploy ──▶ latest deployed ──▶ every developer's own installs
│
│ promote
▼
published ──▶ every other install, on their next upgrade
That split is the whole release model: deploying changes nothing for customers. You can deploy twenty builds in an afternoon and every customer stays where they are.
2. The version number
app.version in manifest.yaml is the version. There is no other source of truth, and nothing in
the CLI bumps it for you.
app:
key: com.acme.tracker
version: 1.3.0
The rules, exactly as they are enforced:
- The published version is immutable. Re-deploying the version that is currently published is refused.
- Any other version is replaceable. Re-deploying a version that exists but is not published
overwrites it in place: the manifest, the raw YAML, and the bundle are all replaced, and the
version id stays the same. This is what makes the develop loop work — you iterate on
1.4.0as many times as you like before promoting it. - Nothing reads semver. No comparison, no ordering, no "newer than." The published pointer is
wherever you last pointed it, so promoting
1.2.0after1.4.0is allowed and means exactly what it says. Use semver because customers read it, not because the platform does.
3. starhive app deploy
starhive app deploy [--build] [--dir ./build] [--manifest ./manifest.yaml]
| Flag | Default | What it does |
|---|---|---|
--dir <path> | ./build | The built bundle directory to zip and upload. |
--manifest <path> | ./manifest.yaml | The manifest to publish. |
--build | off | Runs npm run build first. |
--env <prod|dev|local> | prod | Which deployment to deploy to. |
--api <url> | — | A specific app-platform URL. Only if Starhive tells you to. |
Four HTTP steps, in this order:
- Publish the manifest. The YAML is parsed and validated, checked against the version customers are running (§10), and a version row is created or replaced.
- Request an upload URL for the zip.
- PUT the zip to that presigned URL. Files sit at the archive root, so
index.htmlis at the top. - Finalize, which flips the bundle to
READY.
→ zipping ./build…
→ publishing manifest…
→ uploading bundle (42 KB)…
→ finalizing…
✓ Deployed 1.4.0
appId: …
versionId: …
bundleStatus: READY
serveUrl: https://…/index.html
What a deploy moves and what it doesn't:
- It moves the latest-deployed pointer to this version, always.
- It does not touch the published pointer, so customers are unaffected.
- It does not change the app's marketplace status. A first deploy creates the app as
DEPLOYED— not registered, not installable by anyone. Registration, marketplace details and pricing live in the web console (Marketplace → Manage your apps), not in the manifest. - It moves every workspace where you're a developer of the app — not opt-in, see §4.
A 401 is the token, not the bundle — re-run starhive access with a fresh PAT. A 403 on a
version-up means you are not a developer of that app key; starhive app developers (see
Building apps §11.3)
shows who is.
4. Testing the build: development builds
There is no per-installation "development builds" opt-in, and no toggle a workspace turns on to follow a developer's builds. Every workspace a developer installs the app into automatically follows the latest deployed build, with no toggle needed, and there is no way for a non-developer workspace (a customer's) to opt into pre-promotion builds. If you want a test bed, install the app as one of its developers; there's nothing to switch on beyond that, and no way to hand a customer's workspace this behavior deliberately.
One more thing worth knowing if you rely on the "Update available" signal to know whether a build is new: it compares an install's pinned version against the most recently deployed version of the app, regardless of promotion. So this signal alone doesn't distinguish a promoted release from a build still being tested; see §7 for what drives the update flow for non-developer installs in practice (the published pointer).
5. starhive app promote
starhive app promote [--manifest ./manifest.yaml]
promote reads app.key and app.version from the manifest, finds that deployed version, and
points the published pointer at it. It reads the registry only — it never moves the
latest-deployed pointer, so your own builds are untouched.
Requirements:
- You are a developer of the app.
- That version exists and its bundle is
READY. A version whose upload never finished is refused:version 1.4.0 has no ready bundle to publish.
What happens the moment it lands:
- New installs and every customer upgrade (once triggered — see §7) now resolve to this version.
Promoting a version only changes what a future upgrade resolves to — it does not reach out and upgrade anything already installed. See §7 for how an upgrade actually reaches an installation.
→ resolving com.acme.tracker v1.4.0…
→ promoting 1.4.0…
✓ Promoted 1.4.0
Promote is the step that is felt — it's the only command here that changes what other people run.
6. When a promote waits for review
Promote is never review-gated today. The app's status is exactly DEPLOYED, PRIVATE, PENDING_REVIEW, PUBLISHED, DECOMMISSIONED — there is no status for a live app waiting on Starhive
to approve a promote. Promoting a version takes effect as soon as the caller is a developer and the
version's bundle is ready; it does not inspect the manifest at all, let alone diff it against the
previously-published one. Every successful promote takes effect immediately, for every version,
with no review step of any kind.
7. How the new version reaches a customer
An upgrade re-pins the install to whichever version currently resolves for that app (the published version for a non-developer, per §1/§4), then re-applies your data model changes. It's a no-op if the install is already on the target version. Whether a given upgrade fires automatically or waits for an explicit click in the web console is a detail of that screen — contact Starhive support if you need to know exactly when an upgrade takes effect for a particular install.
There is no distinct "upgrade in progress" or "upgrade failed" status — the status enum is exactly
INSTALLING, NEEDS_CONFIG, ACTIVE, UNINSTALLED, FAILED. An installation that's mid-upgrade still
just reads whatever it read before (typically ACTIVE). If an upgrade fails, the installation most
likely stays exactly as it was before the attempt — still on the old version, still whatever
status it had (typically ACTIVE) — rather than landing on a distinct failed state you can resume
from. Clicking Update again attempts the same upgrade from scratch rather than resuming a tracked
partial state.
| Status | Meaning |
|---|---|
INSTALLING | First-time setup running. |
NEEDS_CONFIG | A required setting is unset. Your settings page must be usable in this state. |
ACTIVE | Set up and running — also what an installation reads while an upgrade is in progress or has failed against it, per the note above. |
FAILED | Declared in the enum, but not a status Starhive is currently seen putting an installation into in practice (see Building apps §9). |
UNINSTALLED | Soft — the space and its data are kept, and a reinstall reactivates them. |
8. Shipping a data-model change
Your data block is not your schema. It describes things that live in the customer's
workspace, holding the customer's objects, beside whatever their admins have added or renamed
themselves. A new version is therefore not a migration you run — it is a description the platform
converges on: it compares what you declared against what the workspace actually has, and makes
up the difference.
Nothing enforces this convergence being safe. There is no deprecated field anywhere in the
manifest schema — not on an attribute, type, or workflow state — and deploying a new version never
loads or diffs the previously-published manifest. The only check that can block a deploy is
re-deploying the exact version number that's already published (refused as immutable); nothing
checks what the new manifest removes, narrows, or renames relative to any other version. The only
validation deploy actually runs checks manifest-internal consistency: app.key format, app.version
non-blank, at least one module, every ${config.*} binding referencing a real config field, every
seed key referencing a real type, and workflow/transition/state consistency — it never looks at
any other version.
In practice this means "keys are forever," "nothing is ever deleted from a workspace," and "widening is free" are true as observed effects of the reconciler being purely additive (see §9) — not because deploy enforces or refuses anything. You can deploy a manifest that removes a type your customers depend on, and it will succeed. Nothing retroactively touches what already exists in a workspace (the reconciler only acts on what's still in your current manifest), but nothing warns you that you've done this either. If you remove a key and later reuse it for something else, nothing stops you — and nothing protects you from the consequences in a workspace that still has the old thing.
9. What an upgrade actually applies
| Change | Reaches an existing install? |
|---|---|
| New type | Yes — created |
| New attribute on an existing type | Yes — created (except the type's label attribute) |
| Renamed type/attribute display name (key unchanged) | Yes — but only if the tracked type/attribute can still be found in the workspace |
| New workflow (a whole new workflow key) | Yes — created with whatever states/transitions it declares |
| New state or transition on an already-tracked workflow | No — the whole workflow is skipped once its key is tracked |
New OPTION choice on an existing attribute | No — a tracked attribute's config is never revisited |
required: true dropped on an existing attribute | No — cardinality is set once, at creation |
cardinality: one dropped on an existing attribute | No — same |
New labelAttribute on an existing type | No — resolved only at type-creation time |
seed rows added to a type that already exists | No — seeding happens once per type, ever; new installs only |
Space name change | No — no rename call exists for the space |
| A type/attribute deleted out-of-band (space still present) | No — left stale rather than recreated |
${config.*}-bound reference created or re-pointed | Yes, when the config value changes — finalizeBindings |
| Anything removed from your manifest | Not refused at deploy — see §10 |
10. What deploy actually checks
Deploy does not refuse breaking changes. It never loads or compares the previously-published
manifest at all — the only refusal it has is re-deploying the exact version number that's already
published (immutability, not a schema check). There is no deprecated: true-based workaround for
anything, because there's nothing it would work around.
What deploy actually validates is manifest-internal consistency only:
app.keyis non-blank and dot-notation;app.versionis non-blank.- At least one module is declared.
- Every
${config.*}-bound attribute target names a config field that's actually declared. - Every
seedkey names a type that's actually declared. - Every workflow has at least one state, no duplicate state keys, exactly one
initialstate, and every transition'sfrom/tonames a real state. - Every
WORKFLOWattribute names a workflow that's actually declared.
There is no includeChildren field to flip (§5.6 of the building guide), and there is no
deprecated field to reference in a workaround.
:::
11. Retiring something
There is no deprecated: true mechanism, or any other sanctioned way to retire a type, attribute,
or workflow state. What actually happens if you just stop declaring something: per §9, the
reconciler only acts on what's in your current manifest. A type or attribute you stop declaring
simply stops being touched — it's neither recreated, renamed, nor deleted, and it stays fully
resolvable (its id stays in typeKeyToId/attributeKeyToId on every installation that already has
it, since those maps are only ever added to, never pruned). So in practice, today: the customer
keeps everything (provisioning was never going to touch existing data anyway), and your app can
still resolve the old key if you kept the id around client-side, since nothing withholds it. There
is no protection against later reusing that key for something else, and no signal anywhere that a
key was ever meant to be retired. If you need to stop using something, treat this as a
documentation/discipline problem on your side, not a platform-enforced one.
12. Recipes
Add a type or an attribute
Declare it. Nothing else.
types:
- key: item
name: Item
labelAttribute: title
attributes:
- { key: title, name: Title, type: TEXT, required: true }
- { key: notes, name: Notes, type: TEXT } # new in 1.4.0
- key: sprint # new in 1.4.0
name: Sprint
labelAttribute: title
attributes:
- { key: title, name: Title, type: TEXT, required: true }
A new type gets its seed rows on the next upgrade, because that type has never been seeded
on that installation. A new required attribute is created required, and every object that
already exists is then missing a value for it — allowed, but required: false plus a default in
your own write path is usually kinder.
Rename anything
Names are matched by key, so a changed name is issued as a rename.
- key: item
name: Work Item # was "Item"
attributes:
- { key: title, name: Summary, type: TEXT, required: true } # was "Title"
Renaming a key is not refused — deploy doesn't diff manifests at all (§10). What actually happens: the old key's type/attribute simply stops being managed (it's not in your manifest anymore, so the reconciler ignores it — it stays in the workspace, unrenamed, unseeded, with whatever data it has), and the new key gets created fresh as if it were brand new. Nothing stops you from doing this; nothing warns you either. If you want the old one gone from view, that's a manual conversation with the customer today, not something the platform does for you.
Add a workflow status
Adding a blocked state and new transitions to an existing lifecycle workflow does not reach
already-installed workspaces: a workflow whose key is already tracked is skipped entirely once it
exists. A new state or transition added to an existing workflow key is only ever seen by a new
installation from that point on; it never reaches a workspace that already has that workflow. There
is no way to get a new state or transition into an existing install today — a new workflow key
would be provisioned fresh, but that's a different workflow, not an addition to the old one.
Add an OPTION choice
A tracked attribute is only ever checked for a rename; its options are never re-read once the
attribute exists. Appending a choice to an existing OPTION attribute's manifest
declaration has no effect on installations that already have that attribute — same limitation as
workflow states above. A brand-new OPTION attribute picks up whatever choices you declare at
creation time, same as any other new attribute.
Relax a bound
Dropping required: true or cardinality: one on an attribute that's already provisioned has no
effect — a tracked attribute's required/cardinality are never revisited after creation. This only
does something meaningful for an attribute you're declaring for the first time in this version,
which is created fresh with whatever bound you wrote.
Move the label
A type's label attribute is resolved and set only once, at type-creation time — there's no way to
re-point an existing type's label at all. Changing labelAttribute in a later version's manifest has
no effect on installations that already have that type; it only matters for a type being created for
the first time.
Change the space
space.access isn't a manifest field at all — the manifest only has key and name for a space —
and a space is only ever created when there's no tracked space yet, with nothing to rename it
afterward. Changing name in a later version has no effect on an installation that already has the
space provisioned.
Re-point a ${config.*} reference
Not a manifest change at all — it is the admin changing a setting on your settings page, which re-points the binding at the type they picked, on the spot.
Changing a static targetType (not config-bound) in the manifest is neither applied nor refused:
deploy doesn't diff manifests (§10), and a tracked attribute is only ever re-checked for a rename,
never re-targeted. So changing a static targetType in a later version has no effect on an
already-created attribute; it would only take effect on a brand-new attribute.
config:
- { key: linkedType, name: Linked type, type: typeRef, default: item }
types:
- key: timeEntry
attributes:
- { key: workItem, type: REFERENCE, targetType: "${config.linkedType}", cardinality: one }
13. Known limits
-
No data migrations. There is no way to backfill a value onto existing objects, or convert one attribute's values into another's.
-
No upgrade preview. There's no diff shown before an admin updates an installed app to a new version.
-
A tracked type or attribute that's gone missing from the workspace is silently skipped, not recreated — only a deleted Space triggers full recreation. An already-tracked workflow is never revisited regardless of whether it still exists (§9).
-
There is no
deprecated: truemechanism, so there's no reserved-key protection, and retired states are not withheld as destinations (§11). -
There is no key-reservation mechanism, so nothing accumulates and there's no price paid for reusing a key. The actual risk runs the other way: reusing an old key for something new is not protected against at all (§11).
-
Installing and upgrading are web-console actions or direct API calls. The CLI ships versions; it does not install or upgrade them.
14. Release checklist
app.versionbumped, and the build you are shipping is the one you tested.data.typesand the attribute names your code reads still match.- Installed cleanly into a fresh workspace, reaching
ACTIVE. - Upgraded a workspace that was on the currently published version, not just a fresh
install — that is the path every customer takes. Check specifically for the gaps in §9: a new
workflow state/transition, a new
OPTIONchoice, or a relaxed bound on an existing attribute will not show up after an upgrade the way you might expect from the original version of this guide — only brand-new types/attributes/workflows do. - Every declared slot renders; the settings page works with nothing configured.
- Errors surface as toasts, not blank frames. Light and dark both fine.
starhive app promote— this takes effect immediately for every version; there is no review wait to expect (§6).