ObjectStackObjectStack

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

  • added
  • modified
  • removed
  • renamed

MetadataDiffItem

Single metadata change between package versions

Properties

PropertyTypeRequiredDescription
typestringMetadata type
namestringMetadata name
changeTypeEnum<'added' | 'modified' | 'removed' | 'renamed'>Category of metadata modification (added, modified, removed, or renamed)
hasConflictbooleanoptional (default: false)Whether this change may conflict with customizations
summarystringoptionalHuman-readable change summary
previousNamestringoptionalPrevious name if renamed

RollbackPackageRequest

Rollback package request

Properties

PropertyTypeRequiredDescription
packageIdstringPackage ID to rollback
snapshotIdstringSnapshot ID to restore from
rollbackCustomizationsbooleanoptional (default: true)Whether to restore pre-upgrade customizations

RollbackPackageResponse

Rollback package response

Properties

PropertyTypeRequiredDescription
successbooleanWhether the rollback succeeded
restoredVersionstringoptionalVersion restored to
messagestringoptionalRollback status message

UpgradeImpactLevel

Severity of upgrade impact

Allowed Values

  • none
  • low
  • medium
  • high
  • critical

UpgradePackageRequest

Upgrade package request

Properties

PropertyTypeRequiredDescription
packageIdstringPackage ID to upgrade
targetVersionstringoptionalTarget version (defaults to latest)
manifest{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }optionalNew manifest (if installing from local)
createSnapshotbooleanoptional (default: true)Whether to create a pre-upgrade backup snapshot
mergeStrategyEnum<'keep-custom' | 'accept-incoming' | 'three-way-merge'>optional (default: "three-way-merge")How to handle customer customizations
dryRunbooleanoptional (default: false)Preview upgrade without making changes
skipValidationbooleanoptional (default: false)Skip pre-upgrade compatibility checks

Nested Shape: UpgradePackageRequest.manifest

PropertyTypeRequiredDescription
idstringUnique package identifier (reverse domain style)
namespacestringoptionalShort namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project")
defaultDatasourcestringoptional (default: "default")Default datasource for all objects in this package
versionstringPackage version (semantic versioning)
typeEnum<'plugin' | 'ui' | 'driver' | 'server' | 'app' | 'theme' | 'agent' | 'objectql' | …>Type of package
scopeEnum<'cloud' | 'system' | 'project'>optional (default: "project")Deployment scope: cloud | system | project
namestringHuman-readable package name
descriptionstringoptionalPackage description
permissionsstring[] | { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }optionalRequired 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)
objectsstring[]optionalGlob patterns for ObjectQL schemas files
datasourcesstring[]optionalGlob patterns for Datasource definitions
dependenciesRecord<string, string>optionalPackage dependencies
configurationneveroptional[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[] }optionalPlatform contributions
data{ object: string; externalId?: string | string[]; mode?: Enum<'insert' | 'update' | 'upsert' | 'replace' | 'ignore'>; env?: Enum<'prod' | 'dev' | 'test'>[]; … }[]optionalInitial seed data (prefer top-level data field)
capabilitiesneveroptional[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.
extensionsneveroptional[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)[] }[]optionalNavigation items this package contributes into apps owned by other packages
loadingneveroptional[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 }optionalPlatform compatibility requirements (legacy; superseded by engines)
engines{ platform?: string; protocol?: string }optionalPlugin compatibility ranges (ADR-0025 §3.2; supersedes engine)
runtimeEnum<'node' | 'sandbox' | 'worker'>optionalPlugin 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
packagingEnum<'bundled' | 'manifest-deps'>optionalDependency packaging strategy (ADR-0025 §3.3)
mainstringoptionalEntry 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)
integrityRecord<string, string>optionalPer-file content digests of the plugin artifact (ADR-0025 §3.2)

UpgradePackageResponse

Upgrade package response

Properties

PropertyTypeRequiredDescription
successbooleanWhether the upgrade succeeded
phaseEnum<'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'>; … }optionalUpgrade plan
snapshotIdstringoptionalSnapshot ID for rollback
conflicts{ path: string; baseValue: any; incomingValue: any; customValue: any }[]optionalUnresolved merge conflicts
errorMessagestringoptionalError message if upgrade failed
messagestringoptionalHuman-readable status message

Nested Shape: UpgradePackageResponse.plan

PropertyTypeRequiredDescription
packageIdstringPackage identifier
fromVersionstringCurrently installed version
toVersionstringTarget upgrade version
impactLevelEnum<'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
affectedCustomizationsintegeroptional (default: 0)Count of customizations that may be affected
requiresMigrationbooleanoptional (default: false)Whether data migration scripts are needed
migrationScriptsstring[]optionalPaths to migration scripts
dependencyUpgrades{ packageId: string; fromVersion: string; toVersion: string }[]optionalDependent packages that also need upgrading
estimatedDurationSecondsintegeroptionalEstimated upgrade duration in seconds
estimatedDurationneveroptional[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.
summarystringoptionalHuman-readable upgrade summary

UpgradePhase

Current phase of the upgrade process

Allowed Values

  • pending
  • analyzing
  • snapshot
  • executing
  • migrating
  • validating
  • completed
  • failed
  • rolling-back
  • rolled-back

UpgradePlan

Upgrade analysis plan generated before execution

Properties

PropertyTypeRequiredDescription
packageIdstringPackage identifier
fromVersionstringCurrently installed version
toVersionstringTarget upgrade version
impactLevelEnum<'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
affectedCustomizationsintegeroptional (default: 0)Count of customizations that may be affected
requiresMigrationbooleanoptional (default: false)Whether data migration scripts are needed
migrationScriptsstring[]optionalPaths to migration scripts
dependencyUpgrades{ packageId: string; fromVersion: string; toVersion: string }[]optionalDependent packages that also need upgrading
estimatedDurationSecondsintegeroptionalEstimated upgrade duration in seconds
estimatedDurationneveroptional[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.
summarystringoptionalHuman-readable upgrade summary

Nested Shape: UpgradePlan.changes[number]

Single metadata change between package versions

PropertyTypeRequiredDescription
typestringMetadata type
namestringMetadata name
changeTypeEnum<'added' | 'modified' | 'removed' | 'renamed'>Category of metadata modification (added, modified, removed, or renamed)
hasConflictbooleanoptional (default: false)Whether this change may conflict with customizations
summarystringoptionalHuman-readable change summary
previousNamestringoptionalPrevious name if renamed

UpgradeSnapshot

Pre-upgrade state snapshot for rollback capability

Properties

PropertyTypeRequiredDescription
idstringSnapshot identifier
packageIdstringPackage identifier
fromVersionstringVersion before upgrade
toVersionstringTarget upgrade version
tenantIdstringoptionalTenant 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
customizationSnapshotRecord<string, any>[]optionalSnapshot of customer customizations
createdAtstringSnapshot creation timestamp
expiresAtstringoptionalSnapshot expiry timestamp

Nested Shape: UpgradeSnapshot.previousManifest

PropertyTypeRequiredDescription
idstringUnique package identifier (reverse domain style)
namespacestringoptionalShort namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project")
defaultDatasourcestringoptional (default: "default")Default datasource for all objects in this package
versionstringPackage version (semantic versioning)
typeEnum<'plugin' | 'ui' | 'driver' | 'server' | 'app' | 'theme' | 'agent' | 'objectql' | …>Type of package
scopeEnum<'cloud' | 'system' | 'project'>optional (default: "project")Deployment scope: cloud | system | project
namestringHuman-readable package name
descriptionstringoptionalPackage description
permissionsstring[] | { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }optionalRequired 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)
objectsstring[]optionalGlob patterns for ObjectQL schemas files
datasourcesstring[]optionalGlob patterns for Datasource definitions
dependenciesRecord<string, string>optionalPackage dependencies
configurationneveroptional[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[] }optionalPlatform contributions
data{ object: string; externalId?: string | string[]; mode?: Enum<'insert' | 'update' | 'upsert' | 'replace' | 'ignore'>; env?: Enum<'prod' | 'dev' | 'test'>[]; … }[]optionalInitial seed data (prefer top-level data field)
capabilitiesneveroptional[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.
extensionsneveroptional[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)[] }[]optionalNavigation items this package contributes into apps owned by other packages
loadingneveroptional[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 }optionalPlatform compatibility requirements (legacy; superseded by engines)
engines{ platform?: string; protocol?: string }optionalPlugin compatibility ranges (ADR-0025 §3.2; supersedes engine)
runtimeEnum<'node' | 'sandbox' | 'worker'>optionalPlugin 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
packagingEnum<'bundled' | 'manifest-deps'>optionalDependency packaging strategy (ADR-0025 §3.3)
mainstringoptionalEntry 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)
integrityRecord<string, string>optionalPer-file content digests of the plugin artifact (ADR-0025 §3.2)

On this page