Spin up a fully-local stack with devstack (incl. local Seal)
A one-command, all-local Sui Stack Messaging stack via @mysten-incubation/devstack: local Sui node +
a local Seal key server + the messaging Move package + a browser dev-wallet + the chat-app. The
payoff is that message decryption works fully locally — the testnet-first
spin-up-e2e-stack can't decrypt on localnet (Seal has no localnet;
the canonical key servers can't authorize localnet group objects).
This was validated end-to-end (create → send → DECRYPT). The chat-app-side integration is documented
in chat-app/docs/DEVSTACK.md; the friction log + upstream doc
links are in reference/NOTES.md.
Requirements
- Docker, running. sui / seal run as containers; first boot pulls images (slow).
- Node >= 24 (devstack
engines). With nvm:nvm install 24 && nvm use 24. - pnpm 10.x for installs. On pnpm v11 a fresh install trips the esbuild build gate and the
minimumReleaseAgefloor on devstack's fresh transitive deps. Usenpx pnpm@10 install. Seedocs/pnpm-v11-troubleshooting.md. - No native localnet on
:9000. Stop anysui startfirst. - The incubation deps stay chat-app devDependencies only (never the canonical SDK).
Run
cd chat-app
npx pnpm@10 install # devDeps: @mysten-incubation/devstack@0.1.1, @mysten-incubation/dev-wallet@0.3.0, @mysten/signers@1.0.5
pnpm devstack up # sui + local Seal + publish + codegen + serve (add --renderer plain for clean logs)
# offline sanity check (no Docker): pnpm devstack config
Open the printed http://dev.chat-app-local.chat-app.localhost:5175, connect the Dev Wallet
(funded publisher/alice/bob), create a group, send, refresh → it decrypts. Send/decrypt also need
the relayer (below).
How it fits together
devstack up brings up: Sui node → publisher account → messaging package publish → local-keygen Seal →
dev-wallet server → chat-app (Vite), writing typed config to chat-app/src/generated/ (gitignored).
The chat-app consumes it through vite.config.ts (devstackVitePlugin() aliases @generated; a
virtual:devstack-app-config shim keeps it dev-only) and src/lib/devstack-config.ts. Full walkthrough:
chat-app/docs/DEVSTACK.md.
The load-bearing, non-obvious bits (all handled in chat-app/devstack.config.ts + devstack-config.ts):
- Publishing the package. devstack does one
Transaction.publish({ modules }), which merges bundled unpublished deps into one package.sui_groupsships a committedPublished.toml, so the-e testnetbuild links its testnet id (absent on localnet) instead of bundling. Fix: materialize a gitignored copy ofsui_groupswithPublished.toml/Move.lockstripped + a local Move dep, patchsuinsMVR→git, strip messaging'sPublished.toml. Net: messaging + sui_groups merge into one local package id (somessagingandpermissionedGroupsconfigs share it). The app recovers the namespace/version singletons + the groups id from the publish tx via GraphQL at bootstrap. - Seal: one server,
sealThreshold: 1. Two local-keygen servers collide in codegen in 0.1.1. - Dev-wallet: register it yourself.
devstackVitePlugin()only aliases@generated— it does NOT inject a wallet. devstack runs the wallet server (funded accounts); the app builds aDevstackSignerAdapterfrom@generated/dapp-kit/configand hands it to dApp Kit viadevWalletInitializer({ mountUI: true })increateDAppKit({ walletInitializers }). Serialize sign calls (the DevWallet allows one pending sign; dApp Kit doesn't queue).
Relayer
devstack does not run the reference relayer; send/fetch go through it. Run it separately
(spin-up-relayer) on the host, pointed at the direct host-published
validator port (not the Traefik-routed :9000, which 400s on gRPC):
docker port <devstack-…-sui-validator> 9000 # e.g. 127.0.0.1:51000
# relayer/.env: SUI_RPC_URL=http://127.0.0.1:51000 ; GROUPS_PACKAGE_ID=<merged id from src/generated/packages.ts>
cd relayer && cargo run # :3000 ; the chat-app reads VITE_RELAYER_URL (default localhost:3000)
Walrus (optional)
Walrus is off the critical path for the create → send → decrypt loop: decryption never touches it (delivery = relayer store + on-chain refs; Walrus is recovery / attachments only), so leaving it out doesn't compromise local decryption. Only message attachments need it.
The seam that stays non-local is archival — the reference relayer archives to its default
testnet Walrus. Making that local is a tracked follow-up (not done here): devstack offers
walrus({ local: { nodeCount, shards } }) + walCoin, but the relayer + chat-app talk HTTP to a
publisher/aggregator gateway (not the Walrus SDK) — whether devstack's local Walrus exposes those is
the open question — and it also needs repointing the relayer's WALRUS_PUBLISHER_URL/AGGREGATOR_URL,
enabling attachments (off in devstack mode), and wiring the indexer + SDK RecoveryTransport. To
experiment now, add walrus({ local }) + walCoin to devstack.config.ts and wire VITE_WALRUS_*.
Gotchas (full list in reference/NOTES.md)
- First boot is slow (image pulls).
.devstack/+src/generated/are gitignored (regenerated eachup). - Reset:
devstack wipe --yes; hard Docker resetdocker rm -f $(docker ps -aq --filter name=devstack)(devstack pruneonly removes idle groups). Re-emit codegen:devstack apply. - "A signing request is already pending" = concurrent signs vs the DevWallet's one-pending model → serialize app-side.
- Connect hangs at "Confirm connection in the wallet" = no approval UI —
mountUI: truemissing indevWalletInitializer(...), orwalletInitializersnot passed tocreateDAppKit. - Relayer "grpc-status header missing, HTTP 400" = gRPC through Traefik → use the host-published port.
Cross-links
- Testnet-first / manual localnet path:
spin-up-e2e-stack. - Run the relayer:
spin-up-relayer. - Why Seal can't work on a localnet + testnet-Seal hybrid:
debug-encryption-flow(Stage 3). - devstack docs: https://github.com/MystenLabs/ts-sdks-incubation/tree/main/packages/docs/content/devstack · dev-wallet docs: https://github.com/MystenLabs/ts-sdks-incubation/tree/main/packages/docs/content/dev-wallet