Configure Seal session keys
Every Seal decryption call requires a short-lived, certified session key that authorizes the SDK to ask the key servers to release the DEK. The session key is one-per-(user, package) and lives only in memory. The SDK abstracts the lifecycle through three tiers — picking the right one and tuning TTL is one of the highest-impact decisions in any Builder integration.
The three tiers — pick by your auth surface
// All three live under `encryption: { sessionKey: ... }` in the messaging client config.
Tier 1 — signer-based (recommended default)
encryption: { sessionKey: { signer: keypair } }
The SDK derives the address from signer.toSuiAddress(), creates a SessionKey, and certifies it automatically. Works with:
Keypairdirectly (Node/server-side, scripts).@mysten/dapp-kit-core'sCurrentAccountSigner.- Enoki's
EnokiSigner.
Use this whenever you have a Signer instance. Zero ceremony.
If the wallet rejects concurrent sign requests (the dev-wallet does; dApp Kit does not queue),
subclass CurrentAccountSigner and serialize signPersonalMessage with a promise chain — worked
example: chat-app/src/lib/queued-signer.ts.
Tier 2 — callback-based
encryption: {
sessionKey: {
address: '0x...',
onSign: async (message: Uint8Array) => {
// Your wallet adapter signs the personal message and returns the signature string.
return signPersonalMessage(message);
},
},
}
The SDK creates the session key, then calls onSign() with the personal-message bytes. Use this when you only have a "sign personal message" surface — not a full Signer — to integrate with.
UX note: onSign triggers a wallet popup. The first decryption per session will prompt the user. Tune ttlMin (below) to avoid re-prompting.
Tier 3 — consumer-managed SessionKey
encryption: { sessionKey: { getSessionKey: () => myManagedSessionKey } }
You own creation, certification, expiry, and replacement of the SessionKey. The callback may return SessionKey or Promise<SessionKey>. The manager caches the returned instance and re-calls your callback only when it detects the cached instance is approaching its own ttlMin expiry. Useful for sharing one SessionKey across multiple SDK instances, persisting/rehydrating across page reloads, or wiring in custom creation logic.
Note: Tier 3's config variant excludes ttlMin, so the DEK cache falls back to the 10-minute default regardless of your SessionKey's own TTL — a 30-min consumer-managed session will still see DEK entries flush every 10 min.
TTL and refresh-buffer tuning (Tier 1 & 2)
| Option | Default | Description | When to change |
|---|---|---|---|
ttlMin |
10 (minutes) |
Session-key lifetime; also the DEK cache TTL (same value backs both for Tier 1/2) | Increase to reduce wallet prompts and Seal round-trips; decrease for tighter access-revocation latency |
refreshBufferMs |
60000 (60 sec) |
Refresh proactively this many ms before expiry | Increase if you've seen "key expired during long fetches"; decrease to push TTL boundary |
mvrName |
undefined |
MVR (Move Registry) name for Seal policy resolution | Set only if you've registered your package under MVR; otherwise leave undefined |
Trade-off: longer ttlMin = fewer wallet popups, but a longer window where a stolen session key can decrypt. The default of 10 minutes is a reasonable starting point for most consumer dapps. For high-frequency / power-user flows where popups are painful, 30–60 minutes is reasonable.
Note: the session key only authorizes decryption requests to Seal key servers — it does not authorize on-chain mutations. Even with a long TTL, every state-changing tx still requires the underlying signer / wallet.
Lifecycle: what happens behind the scenes
Whichever tier you pick, the SDK's SessionKeyManager (ts-sdks/packages/sui-stack-messaging/src/encryption/session-key-manager.ts) handles:
- Lazy creation. No session key is created until the first encryption / decryption call needs one. Avoids prompting the wallet on idle pages.
- Single-flight refresh. When the current key is within
refreshBufferMsof expiry, the next call triggers creation of a new one. Concurrent callers during creation queue on the same promise — so you don't get multiple simultaneous wallet popups. Callers that already grabbed aSessionKeyreference before refresh continue with the still-valid old key. - DEK cache TTL is set from
ttlMin. The DEK cache (aTtlMapinenvelope-encryption.ts, notdek-manager.ts) uses the configuredttlMinas its TTL. Each entry stamps its ownexpiresAt = insertion_time + ttlMinand is lazily evicted on next access — independent of any specific SessionKey instance. Session-key refresh does not invalidate the cache; entries only expire on their own per-entry TTL or via an explicitclient.messaging.encryption.clearCache(groupId?).
Key rotation under load — a Builder gotcha
client.messaging.rotateEncryptionKey(...) creates a new EncryptionHistory version on-chain (client.ts). For the combined "remove member + rotate" flow there is also client.messaging.removeMembersAndRotateKey(...) — prefer that when revoking access, so the removal and rotation land atomically.
Messages sent after rotation use the new keyVersion; messages sent before still reference the old version. Old DEKs remain accessible to current group members via Seal — rotation does not break history.
What rotation does break: members who lost MessagingReader permission before rotation can still decrypt history (they have access to old DEK versions). To revoke a member's access to future messages, rotate after removing their permission (or use removeMembersAndRotateKey to do both atomically). See docs/sui-stack-messaging/Security.md for the full forward-secrecy story.
If you're rotating under load (high message throughput), expect a brief window where in-flight sends may race with the rotation — the SDK handles this internally but adopters should test their dapp's UX through a rotation event.
Verification
The SessionKeyManager (encryption/session-key-manager.ts) is internal — there is no client.messaging.encryption.getSessionKey() on the public API surface. To verify session-key wiring you must trigger an operation that actually uses Seal. The public surface on client.messaging.encryption is encrypt(...), decrypt(...), and clearCache(groupId?) (envelope-encryption.ts).
Safety — read before running any of the steps below.
Every operation in this section that exercises Seal does so by performing a real on-chain or relayer action:
createAndShareGroupmints a shared group object,sendMessagewrites ciphertext to the relayer,rotateEncryptionKeyappends a newEncryptionHistoryversion on-chain (gas-paid by your signer), andremoveMembersAndRotateKeydoes both of the above plus removes members. None of these are reversible. Rotation in particular cannot be undone — you can only roll forward with another rotation, and every existing group member is affected.Run these verifications only against throwaway dev groups on testnet (or localnet), never against a production / live group. Concrete recommendation:
- Use a dedicated dev signer with testnet SUI from the faucet.
- For each verification run, create a fresh disposable group (
uuid: \session-test-${Date.now()}``) inside the test, and treat it as garbage after. Do not reuse a group across runs.- Never call
rotateEncryptionKeyorremoveMembersAndRotateKeyagainst a group that has real members, real history, or that any user-facing dapp is reading from.- If you're integration-testing in a CI environment, scope tests to localnet (
pnpm test:integrationuses a localnet Docker stack — seets-sdks/packages/sui-stack-messaging/test/integration/localnet/) so nothing leaks to a shared network.
With those guardrails in place:
- Tier 1 end-to-end (Node script, dev signer, throwaway group): on a fresh disposable test group, call
await client.messaging.createAndShareGroup({ signer, ... })thensendMessage. The first decrypt-needing operation implicitly creates + certifies the session key. No throw → signer reachable + session-key flow works. Mutating: creates a new on-chain group object and writes one message via the relayer; costs gas. - Tier 2 sanity (in your dapp, against a dev group only): trigger any operation that fetches + decrypts an existing message (e.g.
client.messaging.getMessages({ groupRef, limit: 1 })). This is read-only — safe against any group your dev signer can read. ConfirmonSignis called once with aUint8Array; confirm subsequent decryptions withinttlMindo not re-triggeronSign. - TTL behavior (read-only): set
ttlMin: 1for a manual test. After ~60s, trigger anothergetMessagescall against a dev group (Tier 1: silent refresh; Tier 2: one newonSigncall). Reset to your productionttlMinafterwards. Non-mutating. - DEK cache miss on a new key version (mutating — disposable test group only): on a dev group you have explicitly created for this test, call
client.messaging.rotateEncryptionKey({...})(client.ts). The cache is keyed by(groupId, keyVersion), so the nextgetMessagesinvolving the new version is a cache miss → one fresh Seal round-trip; entries for the old version stay until their per-entry TTL expires. Subsequent fetches for the new version hit the cache. To force-clear cache entries without mutating chain state (preferable when iterating), callclient.messaging.encryption.clearCache(groupId?)— this is the right tool for cache-behavior tests that don't actually need a rotation.
If you're seeing unexpected wallet popups, the most common causes are: (a) ttlMin too short, (b) Tier 2 onSign is being called for every decryption because the implementation isn't actually returning a valid signature, (c) you're constructing a new client per render (each construction creates its own SessionKeyManager). All three are app-side issues — don't reach for rotateEncryptionKey as a diagnostic step; it doesn't help with session-key issues and it permanently mutates the group.
Where this fits
- Bigger integration story:
integrate-sui-stack-messaging. - Decryption failures and how to diagnose them:
debug-encryption-flow. - Authoritative docs:
docs/sui-stack-messaging/Setup.md§ "Session Key Tiers".docs/sui-stack-messaging/Encryption.md§ "Session Key Management".docs/sui-stack-messaging/Security.md— trust model.
- Implementation:
ts-sdks/packages/sui-stack-messaging/src/encryption/session-key-manager.ts.