Seal
Local Seal key servers and live key-server references.
Encrypt and decrypt data with a Seal key server running on your local network. Add seal() to your
stack to start the key server, then build a SealClient from the id and URL it resolves at boot.
Local keygen mode starts the key server and requires a signer account.
import { account, defineDevstack, seal, sui } from '@mysten-incubation/devstack';
const localnet = sui();
const publisher = account('publisher');
const keyServer = seal({
mode: 'local-keygen',
signer: publisher,
});
export default defineDevstack({ members: [localnet, keyServer] });Live modes
Live modes reference existing Mysten key servers. Seal models two kinds of backing, and the per-network methods pick a sensible default for each network:
import { chainIdForNetwork, sealFor } from '@mysten-incubation/devstack';
const live = { mode: 'live', chainId: chainIdForNetwork('testnet') } as const;
// testnet, zero-config: both Mysten independent (Open-mode) key servers, no API key.
const independent = sealFor(live).testnet();
// testnet committee: the 3-of-5 threshold committee + aggregator. Still no API key.
const committee = sealFor(live).testnet({ server: 'committee' });testnet()is zero-config and resolves both Mysten independent key servers (weight 1 each). No API key.testnet({ server: 'committee' })opts into the testnet 3-of-5 committee reached through the testnet aggregator — also no API key.mainnet({ server: 'committee', apiKeyName })resolves the mainnet 5-of-8 committee. Mainnet ships no independent option, so the committee is the default — and it requires API credentials issued through Enoki. You declare only the non-secretapiKeyName(the header name); the factory throws aSealConfigErrorsynchronously ifapiKeyNameis missing, so a misconfigured stack fails before boot rather than at decrypt time.
devstack never carries the secret apiKey value. Both the committed seal.ts and the
world-readable deployment.json (the values it injects into the browser) would expose it, so
devstack emits only the non-secret apiKeyName plus the committee serverConfigs. The app
injects the secret apiKey at runtime — read it from your own env when you build the
SealClient, keyed by the emitted apiKeyName:
// mainnet: declare only the non-secret apiKeyName in the stack config.
const mainnetCommittee = sealFor(live).mainnet({
server: 'committee',
apiKeyName: 'my-app',
});// The app injects the secret apiKey, read from its own env, into the resolved
// serverConfigs keyed by apiKeyName — devstack never carries this value.
const serverConfigs = s.serverConfigs.map((cfg) =>
cfg.apiKeyName === 'my-app' ? { ...cfg, apiKey: process.env.SEAL_API_KEY! } : cfg,
);
const client = new SealClient({ suiClient, serverConfigs });The known ids come from devstack's maintained table (Seal has no @mysten/* SDK constants for
these, unlike Walrus and DeepBook). The object id is the source of truth — the SDK reads each
server's URL from its on-chain key-server object, so devstack no longer carries any hardcoded
endpoint URL. For raw overrides — a private deployment or a hand-tuned committee — pass
sealFor(live).custom({ serverConfigs: [...] }) with the full SDK-ready config array.
Fork stacks point at the upstream network's key servers through
sealFor(forkNetwork).forkKnown({ upstream: 'testnet' }) (add { server: 'committee' } /
{ apiKeyName } to mirror the live selectors — the secret apiKey is injected by the app at
runtime, never carried by devstack); local keygen runs only on local networks.
In a bare stack you can add seal(...) directly to members, as the snippet above does. In a real
app, put only localnet, your app host-service, and dashboard() in members, and order the key
server (and any package action that needs it) via the consuming server's after: list so it boots
first — see
examples/private-content/devstack.config.ts
(after: [localnet, vault, walrusCluster, sealKeyServer, devWallet] on the hostService).
Connect the SDK
The config above runs the key server; the generated bindings carry its resolved
id and URL into your app. Seal is a per-network service: seal.forNetwork(network).seal returns the
config for the network your wallet is connected to, so a runtime switchNetwork flips the
key-server config in lockstep. Build a SealClient from it:
import { useCurrentNetwork } from '@mysten/dapp-kit-react';
import { SealClient } from '@mysten/seal';
import { seal } from '@generated/seal.js';
const network = useCurrentNetwork();
const s = seal.forNetwork(network).seal;
const client = new SealClient({
suiClient,
serverConfigs: [...s.serverConfigs],
// The local key server is self-signed in Open mode, so trust it directly
// for development. Live deployments leave verification on.
verifyKeyServers: false,
});s.serverConfigs is the resolved key-server list — objectId, weight, and aggregator URL — ready
to pass to the client. Set verifyKeyServers: false for the local key server; on live networks
leave it on so the SDK verifies the server's on-chain registration.
The example app splits these two concerns: the seal.forNetwork(network).seal projection lives in
examples/private-content/src/lib/deployment.ts,
which passes the resolved serverConfigs into
examples/private-content/src/lib/seal.ts
for the full encrypt/decrypt flow with session keys.