Package Upgrade
Package Upgrade protocol schemas
Package Upgrade Protocol
Defines the complete lifecycle for upgrading installed packages, including pre-upgrade analysis, snapshot/backup, execution, validation, and rollback capabilities.
Architecture Alignment
- Salesforce: Managed Package upgrade with push upgrades and subscriber control
- ServiceNow: Update Sets with preview, commit, and back-out support
- Helm: Helm upgrade with rollback to previous release
- Kubernetes: Rolling update with readiness probes and automatic rollback
Upgrade Flow
1. PreCheck → Validate compatibility, check dependencies
2. Plan → Generate upgrade plan with metadata diff
3. Snapshot → Backup current state (metadata + customizations)
4. Execute → Apply new package metadata with 3-way merge
5. Validate → Run post-upgrade health checks
6. Commit → Finalize upgrade (or Rollback on failure)Source: packages/spec/src/kernel/package-upgrade.zod.ts
TypeScript Usage
import { MetadataChangeTypeSchema, MetadataDiffItemSchema, RollbackPackageRequestSchema, RollbackPackageResponseSchema, UpgradeImpactLevelSchema, UpgradePackageRequestSchema, UpgradePackageResponseSchema, UpgradePhaseSchema, UpgradePlanSchema, UpgradeSnapshotSchema } from '@objectstack/spec/kernel';
import type { MetadataChangeType, MetadataDiffItem, RollbackPackageRequest, RollbackPackageResponse, UpgradeImpactLevel, UpgradePackageRequest, UpgradePackageResponse, UpgradePhase, UpgradePlan, UpgradeSnapshot } from '@objectstack/spec/kernel';
// Validate data
const result = MetadataChangeTypeSchema.parse(data);MetadataChangeType
Type of metadata change between package versions
Allowed Values
addedmodifiedremovedrenamed
MetadataDiffItem
Single metadata change between package versions
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type |
| name | string | ✅ | Metadata name |
| changeType | Enum<'added' | 'modified' | 'removed' | 'renamed'> | ✅ | Category of metadata modification (added, modified, removed, or renamed) |
| hasConflict | boolean | optional (default: false) | Whether this change may conflict with customizations |
| summary | string | optional | Human-readable change summary |
| previousName | string | optional | Previous name if renamed |
RollbackPackageRequest
Rollback package request
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| packageId | string | ✅ | Package ID to rollback |
| snapshotId | string | ✅ | Snapshot ID to restore from |
| rollbackCustomizations | boolean | optional (default: true) | Whether to restore pre-upgrade customizations |
RollbackPackageResponse
Rollback package response
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Whether the rollback succeeded |
| restoredVersion | string | optional | Version restored to |
| message | string | optional | Rollback status message |
UpgradeImpactLevel
Severity of upgrade impact
Allowed Values
nonelowmediumhighcritical
UpgradePackageRequest
Upgrade package request
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| packageId | string | ✅ | Package ID to upgrade |
| targetVersion | string | optional | Target version (defaults to latest) |
| manifest | { id: string; namespace?: string; defaultDatasource?: string; version: string; … } | optional | New manifest (if installing from local) |
| createSnapshot | boolean | optional (default: true) | Whether to create a pre-upgrade backup snapshot |
| mergeStrategy | Enum<'keep-custom' | 'accept-incoming' | 'three-way-merge'> | optional (default: "three-way-merge") | How to handle customer customizations |
| dryRun | boolean | optional (default: false) | Preview upgrade without making changes |
| skipValidation | boolean | optional (default: false) | Skip pre-upgrade compatibility checks |
Nested Shape: UpgradePackageRequest.manifest
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Unique package identifier (reverse domain style) |
| namespace | string | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| defaultDatasource | string | optional (default: "default") | Default datasource for all objects in this package |
| version | string | ✅ | Package version (semantic versioning) |
| type | Enum<'plugin' | 'ui' | 'driver' | 'server' | 'app' | 'theme' | 'agent' | 'objectql' | …> | ✅ | Type of package |
| scope | Enum<'cloud' | 'system' | 'project'> | optional (default: "project") | Deployment scope: cloud | system | project |
| name | string | ✅ | Human-readable package name |
| description | string | optional | Package description |
| permissions | string[] | { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] } | optional | Required permissions at the AUTHORING stage: legacy string[] or structured plugin block (ADR-0025 §3.2) — at the assembled stage the same key is the ADR-0090 PermissionSet[] collection instead (AssembledPackageBodySchema) |
| objects | string[] | optional | Glob patterns for ObjectQL schemas files |
| datasources | string[] | optional | Glob patterns for Datasource definitions |
| dependencies | Record<string, string> | optional | Package dependencies |
| configuration | never | optional | [REMOVED] manifest.configuration was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, properties.*.secret promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in defineStack({ plugins: [new MyPlugin({ … })] }), which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
| contributes | { kinds?: object[] } | optional | Platform contributions |
| data | { object: string; externalId?: string | string[]; mode?: Enum<'insert' | 'update' | 'upsert' | 'replace' | 'ignore'>; env?: Enum<'prod' | 'dev' | 'test'>[]; … }[] | optional | Initial seed data (prefer top-level data field) |
| capabilities | never | optional | [REMOVED] manifest.capabilities was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read implements, provides, requires, extensionPoints or extensions, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level manifest.dependencies, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. |
| extensions | never | optional | [REMOVED] manifest.extensions was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: contributes.kinds registers metadata kinds, navigationContributions injects navigation into other packages' apps, and code-level extension happens in the plugin itself (init/start). |
| navigationContributions | { app: string; group?: string; priority?: integer; items: (object | … +8 more)[] }[] | optional | Navigation items this package contributes into apps owned by other packages |
| loading | never | optional | [REMOVED] manifest.loading was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (strategy, preload, codeSplitting, dynamicImport, initialization, dependencyResolution, hotReload, caching, sandboxing, monitoring) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — defineStack registers them and the kernel runs init then start in an order topologically resolved from each composed plugin's own dependencies / optionalDependencies (resolvePluginOrder); the set is fixed until the process restarts. ⚠️ loading.sandboxing in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and allowedServices gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (manifest.runtime) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the node tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation. |
| engine | { objectstack: string } | optional | Platform compatibility requirements (legacy; superseded by engines) |
| engines | { platform?: string; protocol?: string } | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes engine) |
| runtime | Enum<'node' | 'sandbox' | 'worker'> | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting node → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares |
| packaging | Enum<'bundled' | 'manifest-deps'> | optional | Dependency packaging strategy (ADR-0025 §3.3) |
| main | string | optional | Entry module of a code-bearing plugin, relative to the plugin root; os plugin build bundles it and writes dist/index.mjs here in the compiled manifest (ADR-0025 §3.4) |
| integrity | Record<string, string> | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
UpgradePackageResponse
Upgrade package response
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Whether the upgrade succeeded |
| phase | Enum<'pending' | 'analyzing' | 'snapshot' | 'executing' | 'migrating' | 'validating' | 'completed' | 'failed' | 'rolling-back' | 'rolled-back'> | ✅ | Current upgrade phase |
| plan | { packageId: string; fromVersion: string; toVersion: string; impactLevel: Enum<'none' | 'low' | 'medium' | 'high' | 'critical'>; … } | optional | Upgrade plan |
| snapshotId | string | optional | Snapshot ID for rollback |
| conflicts | { path: string; baseValue: any; incomingValue: any; customValue: any }[] | optional | Unresolved merge conflicts |
| errorMessage | string | optional | Error message if upgrade failed |
| message | string | optional | Human-readable status message |
Nested Shape: UpgradePackageResponse.plan
| Property | Type | Required | Description |
|---|---|---|---|
| packageId | string | ✅ | Package identifier |
| fromVersion | string | ✅ | Currently installed version |
| toVersion | string | ✅ | Target upgrade version |
| impactLevel | Enum<'none' | 'low' | 'medium' | 'high' | 'critical'> | ✅ | Severity assessment from none (seamless) to critical (breaking changes) |
| changes | { type: string; name: string; changeType: Enum<'added' | 'modified' | 'removed' | 'renamed'>; hasConflict: boolean; … }[] | ✅ | All metadata changes |
| affectedCustomizations | integer | optional (default: 0) | Count of customizations that may be affected |
| requiresMigration | boolean | optional (default: false) | Whether data migration scripts are needed |
| migrationScripts | string[] | optional | Paths to migration scripts |
| dependencyUpgrades | { packageId: string; fromVersion: string; toVersion: string }[] | optional | Dependent packages that also need upgrading |
| estimatedDurationSeconds | integer | optional | Estimated upgrade duration in seconds |
| estimatedDuration | never | optional | [REMOVED] UpgradePlan.estimatedDuration was renamed to estimatedDurationSeconds in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to estimatedDurationSeconds; the value (seconds) is unchanged. |
| summary | string | optional | Human-readable upgrade summary |
UpgradePhase
Current phase of the upgrade process
Allowed Values
pendinganalyzingsnapshotexecutingmigratingvalidatingcompletedfailedrolling-backrolled-back
UpgradePlan
Upgrade analysis plan generated before execution
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| packageId | string | ✅ | Package identifier |
| fromVersion | string | ✅ | Currently installed version |
| toVersion | string | ✅ | Target upgrade version |
| impactLevel | Enum<'none' | 'low' | 'medium' | 'high' | 'critical'> | ✅ | Severity assessment from none (seamless) to critical (breaking changes) |
| changes | { type: string; name: string; changeType: Enum<'added' | 'modified' | 'removed' | 'renamed'>; hasConflict: boolean; … }[] | ✅ | All metadata changes |
| affectedCustomizations | integer | optional (default: 0) | Count of customizations that may be affected |
| requiresMigration | boolean | optional (default: false) | Whether data migration scripts are needed |
| migrationScripts | string[] | optional | Paths to migration scripts |
| dependencyUpgrades | { packageId: string; fromVersion: string; toVersion: string }[] | optional | Dependent packages that also need upgrading |
| estimatedDurationSeconds | integer | optional | Estimated upgrade duration in seconds |
| estimatedDuration | never | optional | [REMOVED] UpgradePlan.estimatedDuration was renamed to estimatedDurationSeconds in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to estimatedDurationSeconds; the value (seconds) is unchanged. |
| summary | string | optional | Human-readable upgrade summary |
Nested Shape: UpgradePlan.changes[number]
Single metadata change between package versions
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type |
| name | string | ✅ | Metadata name |
| changeType | Enum<'added' | 'modified' | 'removed' | 'renamed'> | ✅ | Category of metadata modification (added, modified, removed, or renamed) |
| hasConflict | boolean | optional (default: false) | Whether this change may conflict with customizations |
| summary | string | optional | Human-readable change summary |
| previousName | string | optional | Previous name if renamed |
UpgradeSnapshot
Pre-upgrade state snapshot for rollback capability
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Snapshot identifier |
| packageId | string | ✅ | Package identifier |
| fromVersion | string | ✅ | Version before upgrade |
| toVersion | string | ✅ | Target upgrade version |
| tenantId | string | optional | Tenant identifier |
| previousManifest | { id: string; namespace?: string; defaultDatasource?: string; version: string; … } | ✅ | Complete manifest of the previous package version |
| metadataSnapshot | { type: string; name: string; metadata: Record<string, any> }[] | ✅ | Snapshot of all package metadata |
| customizationSnapshot | Record<string, any>[] | optional | Snapshot of customer customizations |
| createdAt | string | ✅ | Snapshot creation timestamp |
| expiresAt | string | optional | Snapshot expiry timestamp |
Nested Shape: UpgradeSnapshot.previousManifest
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Unique package identifier (reverse domain style) |
| namespace | string | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| defaultDatasource | string | optional (default: "default") | Default datasource for all objects in this package |
| version | string | ✅ | Package version (semantic versioning) |
| type | Enum<'plugin' | 'ui' | 'driver' | 'server' | 'app' | 'theme' | 'agent' | 'objectql' | …> | ✅ | Type of package |
| scope | Enum<'cloud' | 'system' | 'project'> | optional (default: "project") | Deployment scope: cloud | system | project |
| name | string | ✅ | Human-readable package name |
| description | string | optional | Package description |
| permissions | string[] | { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] } | optional | Required permissions at the AUTHORING stage: legacy string[] or structured plugin block (ADR-0025 §3.2) — at the assembled stage the same key is the ADR-0090 PermissionSet[] collection instead (AssembledPackageBodySchema) |
| objects | string[] | optional | Glob patterns for ObjectQL schemas files |
| datasources | string[] | optional | Glob patterns for Datasource definitions |
| dependencies | Record<string, string> | optional | Package dependencies |
| configuration | never | optional | [REMOVED] manifest.configuration was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, properties.*.secret promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in defineStack({ plugins: [new MyPlugin({ … })] }), which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
| contributes | { kinds?: object[] } | optional | Platform contributions |
| data | { object: string; externalId?: string | string[]; mode?: Enum<'insert' | 'update' | 'upsert' | 'replace' | 'ignore'>; env?: Enum<'prod' | 'dev' | 'test'>[]; … }[] | optional | Initial seed data (prefer top-level data field) |
| capabilities | never | optional | [REMOVED] manifest.capabilities was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read implements, provides, requires, extensionPoints or extensions, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level manifest.dependencies, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. |
| extensions | never | optional | [REMOVED] manifest.extensions was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: contributes.kinds registers metadata kinds, navigationContributions injects navigation into other packages' apps, and code-level extension happens in the plugin itself (init/start). |
| navigationContributions | { app: string; group?: string; priority?: integer; items: (object | … +8 more)[] }[] | optional | Navigation items this package contributes into apps owned by other packages |
| loading | never | optional | [REMOVED] manifest.loading was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (strategy, preload, codeSplitting, dynamicImport, initialization, dependencyResolution, hotReload, caching, sandboxing, monitoring) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — defineStack registers them and the kernel runs init then start in an order topologically resolved from each composed plugin's own dependencies / optionalDependencies (resolvePluginOrder); the set is fixed until the process restarts. ⚠️ loading.sandboxing in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and allowedServices gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (manifest.runtime) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the node tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation. |
| engine | { objectstack: string } | optional | Platform compatibility requirements (legacy; superseded by engines) |
| engines | { platform?: string; protocol?: string } | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes engine) |
| runtime | Enum<'node' | 'sandbox' | 'worker'> | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting node → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares |
| packaging | Enum<'bundled' | 'manifest-deps'> | optional | Dependency packaging strategy (ADR-0025 §3.3) |
| main | string | optional | Entry module of a code-bearing plugin, relative to the plugin root; os plugin build bundles it and writes dist/index.mjs here in the compiled manifest (ADR-0025 §3.4) |
| integrity | Record<string, string> | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |