๐Ÿ”’Backing up a wallet

This guide will walk you through how backups of a user's wallet are set up with Portal.

The MPC backup process enables your users to recover their wallets in the event that their device is lost, stolen, or broken.

Backups are handled in two pieces: user backup shares and custodian backup shares. Your infrastructure will store both of them.

At the time of recovery, a user and a custodian backup share are used together to generate new signing shares, which are then stored on the user's device. This allows the user to migrate their wallet to a new device seamlessly.

Portal supports several backup methods. A user can store a backup of their wallet once per backup method. That means they can potentially have "GDRIVE", "ICLOUD", "PASSWORD", and "PASSKEY" backups simultaneously, which would allow them to easily recover their wallet using any of those backup methods.

Your Database

Before we dive into the backup shares' implementations, we should go over your database schema. Multi-backup requires storing both custodian and user backup shares by their respective backup method in your own database. We recommend the following database model structure:

// Encrypted backup share received from portal.backup().
// Encrypted backup share retrieved from your API to be used in portal.recover().
model UserBackupShare {
  backupMethod String       @default("UNKNOWN")           // (String) The method used for the backup. Defaults to "UNKNOWN" if not specified.
  createdAt    DateTime     @default(now())               // (DateTime) The timestamp when the backup share was created.
  id           String       @id @default(cuid())          // (String) A unique identifier for each backup share
  cipherText   String                                     // (String) The encrypted backup share data.
  userId       String                                     // (Int) The identifier of the user to whom the backup share belongs.

  user User @relation(fields: [userId], references: [id]) // (User) A relation field that links to the corresponding user in the User model.

  @@unique([userId, backupMethod])                        // A unique constraint ensuring that each user has only one backup share per method.
}

// Raw backup share received from the POST /backup webhook.
// Raw backup share retrieved from the POST /backup/fetch webhook.
model CustodianBackupShare {
  backupMethod String       @default("UNKNOWN")           // (String) The method used for the backup. Defaults to "UNKNOWN" if not specified.
  createdAt    DateTime     @default(now())               // (DateTime) The timestamp when the backup share was created.
  id           String       @id @default(cuid())          // (String) A unique identifier for each backup share
  share        String                                     // (String) The raw backup share data.
  userId       String                                     // (String) The identifier of the user to whom the backup share belongs.

  user User @relation(fields: [userId], references: [id]) // (User) A relation field that links to the corresponding user in the User model.

  @@unique([userId, backupMethod])                        // A unique constraint ensuring that each user has only one backup share per method.
}

// User model, often referred to as "Client" in our documentation.
model User {
  clientApiKey   String  @unique               // (String) The unique Client API Key assigned to the user.     
  clientId       String  @unique               // (String) The unique identifier for the client that Portal uses.
  id             String  @id @default(cuid())  // (String) The primary key for the user

  userBackupShares      UserBackupShare[]      // (UserBackupShare[]) A list of the user backup shares related to the user.
  custodianBackupShares CustodianBackupShare[] // (CustodianBackupShare[]) A list of the custodian backup shares related to the user.
}

We won't go into depth just yet, but it's worth calling out that you can see a couple associations:

  1. A user has many raw custodian backup shares unique by backup method.

  2. A user has many encrypted user backup shares unique by backup method.

    1. The encryption key is stored differently depending on the backup method used.

Perfect, let's dive right into user backup shares.

User Backup Shares

There's a few steps to store your user's backup shares. First, our SDK creates the raw user backup share and then encrypts it. Next, the encryption key is stored using the backup method you specify (e.g. "GDRIVE", "ICLOUD", "PASSWORD", "PASSKEY", etc.). Finally, the encrypted user backup share must then be saved in your infrastructure. Be sure to store it for the user by backup method, as a user can only have 1 backup per backup method.

In order to support user backup shares, three main dependencies must be met:

  1. You must configure 1 or many backup methods when initializing Portal.

  2. You must be able to store the encrypted user backup share by backup method.

  3. You must be able to retrieve the encrypted user backup share by backup method.

Using Google Drive or iCloud Backups

In order to enable Google Drive or iCloud backups, you must first configure one or more cloud storage adapters with your Portal instance.

You can use @portal-hq/gdrive-storage or @portal-hq/icloud-storage (iOS only).

Storing User Backup Shares

After running portal.backupWallet(), you will receive a cipherText as a return value (we will get to that part in a moment). This is the encrypted user backup share. On your own API server, we recommend creating an endpoint to accept the encrypted user backup share and store it.

server.ts
/*
 * This endpoint can be defined by you however you want
 */
app.post('/users/:userId/user-backup-share', async (req, res) => {
    const backupMethod = req.body['backupMethod']
    validateBackupMethod(backupMethod)

    const cipherText = req.body['cipherText']
    validateCipherText(cipherText)

    const userId = req.params['userId']
    const user = await db.user.findById(userId)

    // Remove any existing user backup shares for this backup method.
    await db.userBackupShare.deleteMany({
      where: {
        backupMethod,
        userId: user.id,
      },
    })

    // Store the user backup share.
    await db.userBackupShare.create({
      data: {
        backupMethod,
        cipherText,
        userId: user.id,
      },
    })

    res.status(200).json({ message: 'Successfully stored client backup share' })
})

Sending shares to your server

In order to add support for user backup shares to your mobile app, you must perform three steps:

  1. Generate an encrypted user backup share using portal.backupWallet().

  2. Send the encrypted user backup share to your API and store it.

  3. Call portal.api.storedClientBackupShare() to notify Portal that the encrypted user backup share was saved successfully. (Alternatively you can make an HTTP request to our API directly.)

BackupButton.tsx
import axios from 'axios'
import React, { FC } from 'react'
import { BackupMethods, usePortal } from '@portal-hq/core'
import { Button, View } from 'react-native'

const BackupButton: FC = () => {
  const portal = usePortal()
  
  const handleBackup = async () => {
    // Get an encryped user backup share from running backup.
    const cipherText = await portal.backupWallet(BackupMethods.GoogleDrive)
    
    try {
      // Send the user backup share to your API and store it.
      await axios.post('{your_server}/users/[userId]/user-backup-share', {
        data: {
          backupMethod: "GDRIVE",
          cipherText,
        },
      })

      // โœ… Notify Portal that the user backup share was stored! ๐Ÿ™Œ
      await portal.api.storedClientBackupShare(true, "GDRIVE")
    } catch (error) {
      // โŒ Notify Portal that the user backup share was not stored.
      await portal.api.storedClientBackupShare(false, "GDRIVE")
    }
  }
  
  return (
    <View>
      <Button onPress={handleBackup} title="Back up your wallet" />
    </View>
  )
}

export default BackupButton

Custodian Backup Shares

Storing a custodian backup share is done by Portal generating a custodian backup share and sending the share โ€“ via webhook โ€“ to be stored within your infrastructure.

In order to support custodian backup shares, two main dependencies must be met:

  1. Register a webhook on the Portal Admin Dashboard for your environment (development or production).

  2. Implement the /backup webhook endpoint on your server to store the custodian backup shares.

Registering Your Webhook

You can register your webhook for both /backup and /backup/fetch endpoints in the Portal Admin Dashboard. Reach out to us if you still need to be invited to your organization's dashboard and we can help.

Implementing the /backup Webhook

Portal will send the raw custodian backup share to POST [webhookBaseURL]/backup. The request will include a X-Webhook-Secret header (you can set this in the Portal Admin Dashboard). The body of this POST request will contain three fields:

  • backupMethod - The backup method and curve used to create the backup share.

  • clientId - The Portal ID of the user. We recommend keeping track of this.

  • share - A JSON stringified version of the custodian backup share.

server.ts
app.post('/webhook/backup', async (req, res) => {
  const backupMethod = req.body['backupMethod']
  validateBackupMethod(backupMethod)
  
  const share = req.body['share']
  validateShare(share)

  const clientId = req.body['clientId']
  const user = await this.getUserByClientId(clientId)

  // Remove any existing custodian backup shares for this backup method + user.
  await db.custodianBackupShare.deleteMany({
    where: {
      backupMethod,
      userId: user.id,
    },
  })

  // Store the custodian backup share.
  await db.custodianBackupShare.create({
    data: {
      backupMethod,
      share,
      userId: user.id,
    },
  })

  res.status(204).send()
})

When Portal makes a request to your /backup webhook, another immediate request is made to /backup/fetch right after in order to validate the custodian backup share was stored successfully. The /backup/fetch webhook is explained in the "Handling recovery" doc.

Next steps

Great! You have now set up backups for your users. Before we talk about how to put those backups to use, let's cover all the backup methods your users can make use of.

Last updated