Archive and Recovery
Messages in the Messaging SDK flow through an offchain relayer for real-time delivery. To provide durability and cross-device access without requiring centralized backups, the relayer archives messages to Walrus and the SDK provides a discovery indexer that allows clients to recover them.
Message history is typically fetched from the relayer for real-time use and recovered from Walrus only when rebuilding state or restoring devices.
This page describes the archive and recovery pipeline, the reference implementations, and their current limitations.
How archival works
The relayer's WalrusSyncService runs in the background and periodically uploads pending messages to Walrus. Sync is triggered by either:
- A timer (default: every hour, configurable through
WALRUS_SYNC_INTERVAL_SECS) - A message count threshold (default: 50 new messages, configurable through
WALRUS_SYNC_MESSAGE_THRESHOLD)
Whichever fires first triggers a sync cycle.
Batching
Messages are grouped into Walrus quilts (a quilt is a collection of named patches stored as a single blob). Each message becomes a patch named msg-{messageId}. Messages from different groups can be batched into the same quilt.
Batch size is configurable through WALRUS_SYNC_BATCH_SIZE (default: 100, hard-capped at 666 by the Walrus quilt limit). Each sync cycle queries the storage backend for messages with a pending sync status and uploads up to WALRUS_SYNC_BATCH_SIZE of them. If more messages are pending than the batch size, the remaining messages stay in their pending state and are picked up by the next sync cycle. Multiple consecutive cycles run until all pending messages are drained. This means that under high message volume, the effective archival throughput is batch_size / sync_interval.
Tagging
Each patch carries metadata tags in the quilt index:
| Tag | Value | Purpose |
|---|---|---|
source | "sui-messaging-relayer" | Identifies patches from the messaging relayer |
group_id | Group object ID | Allows filtering by group |
sender | Sender's Sui address | Allows filtering by sender |
order | Message order number | Preserves ordering |
sync_status | Status string | Indicates message state |
These tags are readable from the quilt index without downloading patch content, which enables efficient discovery.
Sync status lifecycle
Every message tracks its archival state:
New message --> SYNC_PENDING --> SYNCED
Edited --> UPDATE_PENDING --> UPDATED
Deleted --> DELETE_PENDING --> DELETED
Edited messages are re-uploaded as new patches. Deleted messages are uploaded as tombstone records so that recovering clients know the message was intentionally removed.
How discovery works
The walrus-discovery-indexer is a reference service that watches Sui checkpoints for BlobCertified events from Walrus, inspects the blobs for messaging patches (by checking the source: "sui-messaging-relayer" tag), and stores the results in a queryable index.
It serves a REST API:
| Endpoint | Description |
|---|---|
GET /v1/groups/:groupId/patches | Message patches for a group (paginated) |
GET /v1/patches | All discovered patches across groups |
GET /health | Health check with last processed checkpoint |
Patch metadata (group ID, sender, order, sync status, blob ID) is extracted from quilt index tags without downloading the actual encrypted message content.
See the walrus-discovery-indexer README for deployment and configuration.
How recovery works
The SDK provides a RecoveryTransport interface for fetching messages from an alternative backend:
interface RecoveryTransport {
recoverMessages(params: RecoverMessagesParams): Promise<FetchMessagesResult>;
}
When configured, the client exposes a recoverMessages() method:
const result = await client.messaging.recoverMessages({
groupRef: { uuid: 'my-group' },
limit: 50,
});
Recovered messages go through the same decryption and sender verification pipeline as real-time messages. Messages that fail decryption are silently dropped.
See Extending for implementing a custom RecoveryTransport.
Reference implementation
A reference WalrusRecoveryTransport is provided in examples/recovery-transport/. It:
- Queries the walrus-discovery-indexer for patches belonging to the group
- Fetches patch content from the Walrus aggregator
- Converts the Walrus wire format to SDK
RelayerMessageobjects - Returns them sorted by order for the SDK to decrypt and verify
Wiring it up
const client = createMessagingGroupsClient(baseClient, {
encryption: { sessionKey: { signer: keypair } },
relayer: { relayerUrl: 'https://your-relayer.example.com' },
recovery: myWalrusRecoveryTransport,
});
// Real-time messages (from relayer)
const messages = await client.messaging.getMessages({ ... });
// Recovered messages (from Walrus)
const recovered = await client.messaging.recoverMessages({ ... });
Current limitations
Both the relayer and the discovery indexer are reference implementations. Keep these limitations in mind when building for production:
-
In-memory storage on the relayer. The default relayer storage backend holds everything in memory. Messages created between sync cycles are lost on restart. For production, implement a persistent storage backend (for example, PostgreSQL) against the relayer's
StorageAdaptertrait, or accept that Walrus serves as the durable store with a sync-window gap. -
In-memory storage on the indexer. The
walrus-discovery-indexeralso uses in-memory storage by default. On restart, it re-processes checkpoints but might miss blobs from before its start. Implement a persistentDiscoveryStorefor production. -
Recovery ordering is best-effort. The relayer assigns monotonically increasing
ordervalues per group. Recovery preserves this ordering within a single relayer's output. However, if multiple relayers archive to Walrus independently, theirordervalues are assigned independently and do not align. In multi-relayer recovery, sort bycreated_attimestamp for best-effort chronological order. -
Sender verification on recovery. Recovered messages carry
signatureandpublicKeyfields, and the SDK verifies them during decryption (populatingsenderVerifiedon each message). Applications should check this field, especially for recovered messages, because Walrus storage is open and anyone can store quilts with the relayer's tagging convention. -
Optional publisher filter. The indexer supports a
WALRUS_PUBLISHER_SUI_ADDRESSfilter to only process blobs from a specific sender. This reduces noise but cannot be used when you want cross-relayer discovery. Without the filter, tag-based inspection is the sole mechanism for identifying messaging blobs.