Going to production
Take a devstack app to production — declare the network set, scaffold typed deployments, switch networks in dapp-kit, and ship a build with no local artifacts.
Ship your app to a real network using the same config that runs your local stack. You declare which
live networks the app supports, scaffold a typed deployments/<net>.ts from an actual deploy, let
dapp-kit switch between networks at runtime, and produce a build that talks only to the live network
— no local RPC, no dev wallet.
It's one config flowing through a handful of steps. The connect-four example ships a worked devnet deployment you can follow, and it's referenced throughout below.
1. How the live network set is declared
A live network is declared by a committed deployments/<net>.ts file. There's no config field and
no manual wiring — codegen derives the live-network set from the deployments/*.ts filenames.
localnet is implicit (it's your dev stack), so the supported set is [localnet, ...<files>].
my-app/
├── deployments/
│ └── devnet.ts ← a deployments/<net>.ts file ⇒ "devnet" is a live network
├── src/
│ └── generated/
└── devstack.config.tsYou don't hand-author that file — you generate it from a real deploy in the next step, then commit
it. Once it's present, src/generated/deployment.ts reflects exactly the files present:
/** The LIVE network names this app ships — the `deployments/*.ts` filenames. */
export type ProvidedNetwork = 'devnet';
/** The full network-name set, local-first: `[<local>, ...<provided>]`. */
export const NETWORK_NAMES = ['localnet', 'devnet'] as const;2. Generate a typed deployment from a real deploy
You don't hand-type 0x-strings. The flow is two steps: publish your package to the target network,
then run dump-deployment to scaffold a typed deployment file from the resolved stack.
First publish to the target network, which writes the real package id:
sui client switch --env devnet
sui client test-publish move/connect_four --build-env devnetThen scaffold the typed file. dump-deployment --network <net> does not publish — it resolves the
stack for that network and emits the deployment file from it:
pnpm exec devstack dump-deployment --network devnetThis writes deployments/devnet.ts as typed TypeScript —
export const deployment = {…} satisfies AppNetworkDeployment — sourced from the resolved
envelope's devnet entry:
import type { AppNetworkDeployment } from '../src/generated/deployment.js';
const CONNECT_FOUR_PACKAGE_ID =
'0xca3763f3b9b94436b2964bc89a5384583a9cd99dfc2ad3d326483b75b9f7a846';
export const deployment = {
network: 'devnet',
rpc: 'https://fullnode.devnet.sui.io:443',
chainId: '5ea2c653',
faucet: null,
graphql: null,
packages: {
connect_four: { id: CONNECT_FOUR_PACKAGE_ID },
},
mvrOverrides: {
packages: {
'@local/connect-four': CONNECT_FOUR_PACKAGE_ID,
},
types: {},
},
} satisfies AppNetworkDeployment;From here you edit and complete it like any source file, then commit it — the committed
deployments/<net>.ts is the authoring surface for that network. The
satisfies AppNetworkDeployment ties it to the app's strict generated type
(Config & deployments): packages must cover every
declared package, and mvrOverrides.packages must carry every @local/<slug> placeholder. A
missing or mistyped id is a type error, so you catch it at tsc rather than in the running app.
The file has no accounts field — dev identities are network-agnostic and don't belong in a
committed deployment.
3. Wire the Vite plugin
The Vite plugin bundles every supported network — keyed
by name, plus the default — into the config your app reads. It discovers deployments/*.ts on its
own, so the standard config needs nothing extra:
import { devstackVitePlugin } from '@mysten-incubation/devstack/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({ plugins: [react(), devstackVitePlugin()] });What the app sees depends on the Vite command:
- Dev (
serve) — your committed networks plus the live local stack, with the local network as the default. The app lists[localnet, devnet]. - Production (
build) — the committed live networks only; the local network is dropped.
Each committed file is validated when the config loads, so a malformed deployment surfaces as a
build error. To use a custom directory or explicit paths, pass the deployments option a
per-network map of () => import('./…') thunks, which replaces auto-discovery.
4. Switch networks in dapp-kit
Let users switch networks at runtime: dapp-kit builds a client per network and flips between them.
The wiring reads straight off config:
export const dAppKit = createDAppKit({
networks: [...config.networkNames],
defaultNetwork: config.defaultNetwork,
// `autoConnect` is omitted here (it defaults off) so a production build shows
// the real connect prompt; the [Get started](/devstack/) snippet enables it
// in dev for fast local iteration. dApp Kit discovers the dev wallet via
// wallet-standard either way — `autoConnect` only controls reconnect, not
// whether the wallet is present.
createClient(network) {
const net = config.forNetwork(network);
return new SuiGrpcClient({
network,
baseUrl: net.rpc,
mvr: { overrides: net.mvrOverrides },
});
},
});dapp-kit calls createClient once per network, so switchNetwork('devnet') from a local dev server
repoints the client to devnet's rpc and ids together. The dev wallet stays connected across the
switch — only the active client changes — so once you've funded its accounts on the live network you
can sign a real tx with it.
5. Build for production
A production build runs the same config with the local network dropped. vite build ships only your
committed live networks — no local RPC, no dev wallet, accounts: {}:
# build with only the committed deployments/* networks; no local stack
pnpm buildThe resulting bundle talks only to the live network's rpc using the committed package ids. This is the path to ship.
6. Verify the prod build
Confirm the bundle carries no local or dev-wallet artifacts. Build it and grep:
pnpm build
# the bundle ships only the committed live networks — grep it:
grep -r "127.0.0.1\|localhost\|dev-wallet\|__DEVSTACK_DEPLOYMENT_LIVE__" dist/ || echo "clean"A clean grep confirms the localnet RPC, the dev wallet, and the dev-only globals are all gone. For a full check, serve the bundle and confirm it connects to the live network's RPC, reads the real package id, and lands a tx signed by an external wallet.
Worked example: connect-four on devnet
connect-four ships the full path end to end. deployments/devnet.ts (above) is its committed live
network, published to Sui devnet. Two browser scenarios prove it:
- Switch
(
tests/browser/network-switch.spec.ts) — connect on the live localnet, thenswitchNetwork('devnet'). dApp Kit's current network flips to devnet, the dev wallet stays connected across the switch, and the app repoints to the devnet client. - Real devnet tx
(
tests/browser/devnet-tx.spec.ts) — after the switch, fund the dev wallet's account on the public devnet faucet and sign a realcreate_lobbyagainst the committed devnet package, asserting the tx landed on devnet.
For the per-network types, the deployment loader, and the full dapp-kit handoff surface, see Config & deployments. For pointing devstack at a forked or live network during development, see Live networks and Forking.