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 after generateWallet()
  • Backup shares encrypted before storage
  • Encryption key stored separately from ciphertext
  • backupSharePairId stored alongside ciphertext
  • updateBackupSharePairs() called with correct status
  • Recovery tested in staging before going live
  • Backup verified: getClientDetails() shows backup pair status as "completed"