Mysten Incubation
Configure your stack

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.

devstack.config.ts
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 with opts.publisher as the signer, and parses the on-chain package id, upgrade cap, and any captured object ids from the publish output. It also discovers coins the package mints (treasury cap + metadata), so coin.fromPackage(...) and funding work without extra wiring. Source comes from sourcePath (a local checkout) or git (a remote repo cloned and cached per url/rev). Publishing is cached by content + chain, so an unchanged package is reused across boots rather than republished.

  • knownPackage(name, opts) — verify. Devstack reads opts.packageId from 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

OptionPurpose
sourcePathPath to a local Move package root. Mutually exclusive with git.
gitRemote source: { url, subdir?, rev? }. Cloned and cached per url/rev.
publisherThe account that signs the publish (an account(...) ref). Required.
captureMap keys to Move type suffixes ({ boardId: '::board::Board' }) to grab object ids from the publish output.
mvrPlaceholderOverride the default @local/<slug> placeholder.
excludeFromCodegenKeep 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:

src/generated/config.ts
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.mvrOverrides is 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>.packageId is for introspection (logging, debugging, reading the id directly). App code routes through mvrOverrides, not packageId.

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.

On this page