Packages
Declaring local Move packages, how devstack records their ids per stack, and how package ids reach app code.
Declare your Move packages with localPackage(...) and devstack publishes them on boot, then hands
their on-chain ids to your app automatically — so you reference packages by name and never paste a
0x… address. Each package's id is recorded per stack, mapped to an MVR placeholder, and injected
into the generated config at runtime.
import {
account,
coin,
defineDevstack,
localPackage,
sui,
wallet,
} from '@mysten-incubation/devstack';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const localnet = sui();
const alice = account('alice');
const managedCoin = localPackage('managed_coin', {
sourcePath: resolve(HERE, 'move/managed_coin'),
publisher: alice,
});
export default defineDevstack({ members: [localnet, managedCoin], stackName: 'token-studio' });Publish + discover, vs verify
localPackage(...) and knownPackage(...) are the two ways a package enters a stack — devstack
either publishes it for you, or verifies one that already exists.
-
localPackage(name, opts)— publish + discover. Devstack builds the Move source, publishes it withopts.publisheras the signer, and parses the on-chain package id, upgrade cap, and anycaptured object ids from the publish output. It also discovers coins the package mints (treasury cap + metadata), socoin.fromPackage(...)and funding work without extra wiring. Source comes fromsourcePath(a local checkout) orgit(a remote repo cloned and cached perurl/rev). Publishing is cached by content + chain, so an unchanged package is reused across boots rather than republished. -
knownPackage(name, opts)— verify. Devstack readsopts.packageIdfrom the network to confirm it exists and is the right kind of object — nothing is built or published. Use it for modules that already live on-chain. See Known packages.
localPackage options
| Option | Purpose |
|---|---|
sourcePath | Path to a local Move package root. Mutually exclusive with git. |
git | Remote source: { url, subdir?, rev? }. Cloned and cached per url/rev. |
publisher | The account that signs the publish (an account(...) ref). Required. |
capture | Map keys to Move type suffixes ({ boardId: '::board::Board' }) to grab object ids from the publish output. |
mvrPlaceholder | Override the default @local/<slug> placeholder. |
excludeFromCodegen | Keep the package out of generated Move bindings (still in config). |
One config, many stacks
The same config works across stacks with no edits. Each package id is tracked per stack under the
name you give it ('managed_coin'), so switching stacks with DEVSTACK_STACK republishes into a
fresh stack and yields a fresh id — your config reads whichever stack is live.
How package ids reach app code
Each package gets an MVR placeholder — by default @local/<slug>, where the slug is the name
lower-cased with underscores dashed (managed_coin → @local/managed-coin). That placeholder is
the stable handle you reference; the on-chain id is resolved against it at runtime. The generated
config.ts exposes both, keyed off the active network's deployment:
import { loadDeployment, requireId } from './config-runtime.js';
const __deployment = loadDeployment();
const dep = __deployment.forNetwork(__deployment.defaultNetwork);
export const config = {
forNetwork: __deployment.forNetwork,
mvrOverrides: {
'@local/managed-coin': requireId(dep, '@local/managed-coin'),
},
packages: {
managed_coin: {
mvr: '@local/managed-coin',
packageId: requireId(dep, '@local/managed-coin'),
},
},
} as const;config.mvrOverridesis what app and dapp-kit code hands to the SDK — it maps each placeholder to the active network's package id, so MVR-named calls resolve to the package your stack published.config.packages.<name>.packageIdis for introspection (logging, debugging, reading the id directly). App code routes throughmvrOverrides, notpackageId.
requireId reads the injected deployment and throws DevstackConfigMissingError when a package's
id is missing. Packages declared with capture also expose their captured object ids at
config.packages.<name>.objects.<key>.
The deployment that backs requireId / forNetwork — the per-network unit, the envelope, and the
strict generated type used for production — is documented in
Config & deployments.