Best Practices
Learn best practices for creating, securing, and verifying wallet backups using KMS, passkeys, and multi-backup strategies for reliable recovery.
Create Backups Immediately
Run backupWallet() right after generateWallet() — in the same onboarding flow. Don't defer it. A wallet without a backup is permanently lost if the signing shares are lost.
const shares = await client.generateWallet();
await storeSigningShares(userId, shares);
// Do this immediately after:
const backup = await client.backupWallet({
body: { generateResponse: JSON.stringify(shares) },
});
await storeBackupShares(userId, backup);Use a KMS for Backup Encryption
For production systems, derive backup encryption keys from a Hardware Security Module or cloud KMS rather than a user passphrase alone. This protects against brute-force attacks if the ciphertext is leaked.
// AWS KMS example
import { KMSClient, GenerateDataKeyCommand, DecryptCommand } from "@aws-sdk/client-kms";
const kms = new KMSClient({});
async function encryptWithKms(plaintext: string): Promise<{ ciphertext: string; encryptedKey: string }> {
const { Plaintext: dataKey, CiphertextBlob: encryptedKey } =
await kms.send(new GenerateDataKeyCommand({ KeyId: process.env.KMS_KEY_ID!, KeySpec: "AES_256" }));
const ciphertext = encryptShare(plaintext, Buffer.from(dataKey!));
return { ciphertext, encryptedKey: Buffer.from(encryptedKey!).toString("base64") };
}Use Passkeys for Consumer Apps
For consumer-facing wallets, consider using WebAuthn passkeys as the backup encryption key. Passkeys are:
- Hardware-backed on modern devices
- Phishing-resistant
- Synced via iCloud Keychain or Google Password Manager
When using passkeys, set status: "STORED_CLIENT_BACKUP_SHARE_KEY" in updateBackupSharePairs().
Verify Backups After Creation
Test recovery works before considering the backup complete. Run a simulated recovery in a staging environment immediately after creating a backup:
// After backup:
const testRecovery = await client.recoverWallet({
body: { backupResponse: JSON.stringify(backup) }, // use plaintext shares
});
if (!testRecovery.SECP256K1?.share || !testRecovery.ED25519?.share) {
throw new Error("Backup verification failed — recovery produced empty shares");
}Multi-Backup Strategy
For high-value wallets, maintain multiple independent backups:
- One backup stored with Portal (Portal-Managed)
- One backup encrypted with a passkey on the user's device
- One backup encrypted with a KMS key on your backend
If isMultiBackupEnabled is true on the environment, Portal supports multiple backup share pairs per wallet.
Backup Share Pair IDs
Always store the backupSharePairId alongside the encrypted ciphertext. You need it to:
- Call
storeEncryptedBackupShare()for Portal-Managed backups - Call
getBackupShareCipherText()during recovery - Call
getEjectableBackupShares()for eject flows
await db.backups.insert({
userId,
curve: "SECP256K1",
backupSharePairId: backup.SECP256K1.id,
encryptedShare: ciphertext,
createdAt: new Date(),
});Checklist
-
backupWallet()called immediately aftergenerateWallet() - Backup shares encrypted before storage
- Encryption key stored separately from ciphertext
-
backupSharePairIdstored alongside ciphertext -
updateBackupSharePairs()called with correct status - Recovery tested in staging before going live
- Backup verified:
getClientDetails()shows backup pair status as"completed"