Mysten Incubation

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.

devstack.config.ts
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-secret apiKeyName (the header name); the factory throws a SealConfigError synchronously if apiKeyName is 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',
});
src/lib/seal.ts (runtime)
// 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:

src/lib/seal.ts
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.

On this page