Skip to content

Sealed Storage — SDK setup ​

User docs → | API reference →

Sealed Storage encrypts data client-side with Seal threshold encryption and stores the ciphertext on Walrus. A Seal key server releases its key share only if an on-chain policy (sealed_storage::<module>::seal_approve*) succeeds in a dry run with the requester as sender, so only addresses that satisfy the policy can decrypt.

@meddleware/sealed-storage-client is the client half:

  • Policy providers — one per Move policy module. Each builds the Seal identity for new content and the approve call for decryption. Built in: nft-gate (holders of an access-gate pass) and time-lock (anyone, after a time).
  • PolicyRegistry — the set of providers an app offers (createDefaultRegistry() registers both built-ins).
  • SealController — threshold encrypt / decrypt over a key-server committee, and the per-address session key (one wallet signature, cached until it expires).

Install ​

bash
npm install @meddleware/sealed-storage-client @mysten/seal @mysten/sui

@mysten/seal and @mysten/sui are peer dependencies.

Create a controller ​

ts
import { SuiGrpcClient } from '@mysten/sui/grpc'
import { createDefaultRegistry } from '@meddleware/sealed-storage-client'
import { SealController } from '@meddleware/sealed-storage-client/controller' // also on the main entry; the subpath lets apps lazy-load @mysten/seal
import { sealPoliciesDeployment } from '@meddleware/sealed-storage-client/deployments'
import { accessGateDeployment } from '@meddleware/access-gate-client/deployments'

const suiClient = new SuiGrpcClient({ network: 'testnet', baseUrl: 'https://fullnode.testnet.sui.io:443' })

// originalId: identities are bound to it (never changes); publishedAt: the latest version, the
// target of seal_approve calls; policyConfigId: the shared version gate every seal_approve reads.
// All three come from the package's recorded deployments.
const { originalId, publishedAt, policyConfigId } = sealPoliciesDeployment('testnet')
// The access_gate package that sealed_storage links: with it set, `encrypt('nft-gate', …)` reads the gate
// first and refuses one that is not a Gate of that package, or whose passes are transferable.
const accessGateOriginalId = accessGateDeployment('testnet').originalId

const registry = createDefaultRegistry(accessGateOriginalId)

const seal = new SealController(
  {
    suiClient,
    originalId,
    publishedAt,
    policyConfigId,
    accessGateOriginalId,
    threshold: 2,
    serverConfigs: [
      // Mysten testnet committee (decentralised server, via the aggregator) + two independent servers
      { objectId: '0xb012378c9f3799fb5b1a7083da74a4069e3c3f1c93de0b27212a5799ce1e1e98', weight: 1,
        aggregatorUrl: 'https://seal-aggregator-testnet.mystenlabs.com' },
      { objectId: '0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75', weight: 1 },
      { objectId: '0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8', weight: 1 },
    ],
  },
  registry,
)

Seal to soulbound gates only

With accessGateOriginalId set, encrypt('nft-gate', …) refuses a gate that mints transferable passes (allowTransferableGates: true opts in). The holder of a transferable pass can freeze or share it, after which anyone can present it and the policy approves — the content is then effectively public. Seal to a soulbound gate, and leave accessGateOriginalId unset only if you check this yourself: without it the controller does not read the gate.

These are the committee defaults sealed-storage-ui uses on testnet. On mainnet the Mysten committee aggregator requires an API key; see the Seal docs.

The flow ​

  1. Encrypt — seal.encrypt(policyType, params, bytes) returns { id, ciphertext }. id (hex) is the Seal identity; it contains a random nonce, so it cannot be recomputed later.
  2. Store — upload ciphertext to Walrus and keep a manifest that points to it.
  3. Decrypt — seal.decrypt(policyType, params, id, ciphertext, { address, signPersonalMessage }) builds the policy's approve transaction, obtains key shares from the committee and decrypts locally.

The manifest ​

SealedManifest is the interchange format between encrypting and decrypting apps. It contains no secrets and can be stored anywhere:

ts
interface SealedManifest {
  policyType: string              // 'nft-gate' | 'time-lock' | …
  id: string                      // Seal identity (hex, no 0x) — required verbatim to decrypt
  blobId: string                  // Walrus blob id of the ciphertext
  network: string                 // where the policy package + key servers live
  params?: Record<string, unknown> // non-secret policy params, e.g. { gateId }
  label?: string
}

Validate untrusted manifests with parseSealedManifest(raw, registry) (throws SealedManifestError; the registry also validates the policy's params), then check network matches your app.

See the integration guide for end-to-end code and Writing policies for adding a policy.

2026 Meddleware. All jams preserved.