suideveloper
All questions
Packages

How do package upgrades work on Sui?

November 13, 2026

Sui packages are immutable, so an upgrade doesn't change the existing package. It publishes a new package object with a new ID that the chain records as the next version of the original. You need the package's UpgradeCap, the new code has to be compatible with the previous version (public function signatures and existing struct layouts can't change), and init doesn't run again. The old version stays on chain and anyone can still call it, so code that relies on shared objects has to handle that itself.

Upgrade with the CLI

From the package directory, after changing the code:

Terminal
sui client upgrade

The CLI reads the UpgradeCap ID from Published.toml for your active environment. If it isn't recorded there (for example, a package published with an older CLI), pass it yourself:

Terminal
sui client upgrade --upgrade-capability 0x<UPGRADE_CAP_ID>

The output contains the new package ID, and Published.toml is updated with the new published-at address and version. original-id stays the same. Add --dry-run to check compatibility and cost first.

What's allowed

Under the default compatible policy, an upgrade may:

  • change any function's implementation
  • add new modules, structs and functions
  • change, remove or make public any non-public function, including public(package) and entry functions
  • remove ability constraints from generic type parameters

It may not:

  • change the signature of an existing public function
  • change an existing struct's fields or abilities
  • remove a public function or a struct

sui client upgrade checks this locally and stops if the new code breaks the rules. Validators check it again on chain.

What happens under the hood

An upgrade is one transaction with three steps:

  1. Authorize: 0x2::package::authorize_upgrade(&mut cap, policy, digest) returns an UpgradeTicket for this exact bytecode digest.
  2. Upgrade: the built-in Upgrade command consumes the ticket, verifies the bytecode and compatibility, creates the new package and returns an UpgradeReceipt.
  3. Commit: 0x2::package::commit_upgrade(&mut cap, receipt) points the cap at the new package and increments its version.

The ticket and receipt have no abilities ("hot potatoes"), so all three steps must happen in the same transaction. The CLI builds this for you. In TypeScript, tx.upgrade({ modules, dependencies, package, ticket }) is the middle step.

Old versions and types

  • Types keep their original ID. A struct defined in v1 is always <original-id>::module::Type, even when you call functions from v3. Event types and object types don't change across upgrades. New types added in a later version use that version's ID.
  • Callers choose the version. A transaction calls a specific package ID. Clients that still call the v1 ID run v1 code. Update your frontends to the new published-at ID.
  • Dependents don't move automatically. If package B depends on your package A, B keeps linking to the A version it was built with until B is upgraded too.

Versioned shared objects

Because old code stays callable, a fix to a shared object's logic does nothing if people can still call the old functions on it. The documented pattern is a version field on the shared object:

Move
module example::counter;

const VERSION: u64 = 2;
const EWrongVersion: u64 = 1;
const ENotAdmin: u64 = 0;
const ENotUpgrade: u64 = 2;

public struct Counter has key {
    id: UID,
    version: u64,
    admin: ID,
    value: u64,
}

public struct AdminCap has key { id: UID }

public fun increment(c: &mut Counter) {
    assert!(c.version == VERSION, EWrongVersion);
    c.value = c.value + 1;
}

entry fun migrate(c: &mut Counter, a: &AdminCap) {
    assert!(c.admin == object::id(a), ENotAdmin);
    assert!(c.version < VERSION, ENotUpgrade);
    c.version = VERSION;
}

Every upgrade increases VERSION. After upgrading, the admin calls migrate, and from then on calls through the old package abort. migrate is entry, not public, so its signature can change in later upgrades. You have to design this in from the first version, since you can't add a field to an existing struct later.

To control who can upgrade and how much they can change, see UpgradeCap and upgrade policies. For the first deploy, see publishing a package.

Sources

More on packages