# Restore without backupd (/docs/restoring/without-backupd)

Every backup is a standard dump, compressed with zstd and encrypted with [age](https://age-encryption.org). Each file is encrypted to backupd's key for your workspace, so restores and restore tests work in the app, **and to every key you add** in **Settings → Encryption**. With your key and free tools you can restore a backup on your own machine, without backupd.

<Steps>
<Step>

### Add a key [#add-a-key]

In **Settings → Encryption**, click **Add key**:

- **Generate in browser:** the key pair is created on your device. The secret key (`AGE-SECRET-KEY-1…`) is shown once and can be saved with **Download key file** as a `.agekey` file. Only the public key (`age1…`) is sent to backupd.
- **Use an existing key:** paste a public key. To make one with the age tools:

  ```sh
  age-keygen -o backupd.agekey     # writes the secret key, prints the public key
  age-keygen -y backupd.agekey     # prints the public key again
  ```

New backups are encrypted to the key from then on. See [Encryption keys](/docs/security/encryption-keys).

</Step>
<Step>

### Get the file [#get-the-file]

- **From the app:** open a backup and click **Download encrypted**. You get a `.zst.age` file.
- **From your own bucket** (Pro): backups are copied there byte for byte. See [Your own bucket](/docs/storage/destinations) for the file layout. Each file has a `.manifest.age` file next to it with its table counts and checksums.

</Step>
<Step>

### Install the tools [#install-the-tools]

You need `age`, `zstd`, and the client tools for your engine: `pg_restore` and `psql`, `mysql`, `mongorestore`, or the Convex CLI (`npx convex`).

</Step>
<Step>

### Restore [#restore]

Decrypt with your key, decompress, and pipe the dump into the engine's restore tool. Nothing is written to disk in between, except for Convex.

</Step>
</Steps>

## Commands per engine [#commands-per-engine]

The examples use `backupd.agekey` for your secret key file and `app.….zst.age` for the downloaded backup.

```sh title="PostgreSQL 17 and later"
age -d -i backupd.agekey app.pgdump.zst.age | zstd -d \
  | pg_restore --no-owner --single-transaction -d "$TARGET_URL"
```

```sh title="PostgreSQL 9.2 to 16"
# Backups are made with pg_dump 18, which sets transaction_timeout.
# Servers before 17 reject that setting, so filter it out and restore with psql.
age -d -i backupd.agekey app.pgdump.zst.age | zstd -d \
  | pg_restore --no-owner -f - | grep -v '^SET transaction_timeout' \
  | psql --single-transaction -v ON_ERROR_STOP=1 "$TARGET_URL"
```

```sh title="MySQL and MariaDB"
age -d -i backupd.agekey app.sql.zst.age | zstd -d \
  | mysql --defaults-extra-file=target.cnf target_db
```

```sh title="MongoDB"
age -d -i backupd.agekey app.archive.zst.age | zstd -d \
  | mongorestore --archive --uri "$TARGET_URI"
# Full-instance (replica set) backups include the oplog: add --oplogReplay.
```

```sh title="Convex"
age -d -i backupd.agekey app.zip.zst.age | zstd -d > snapshot.zip
npx convex import --replace-all snapshot.zip
```

For MySQL, `target.cnf` is an option file with the target's connection details, so the password stays off the command line:

```ini title="target.cnf"
[client]
host=db.example.com
user=restore
password=choose-a-strong-password
ssl-mode=REQUIRED
```

## Check a file before restoring [#check-a-file-before-restoring]

Each backup's [manifest](/docs/restoring/manifest) records the SHA-256 of the dump. Compare it with the dump you decrypt:

```sh
age -d -i backupd.agekey app.pgdump.zst.age.manifest.age | jq .plaintextSha256
age -d -i backupd.agekey app.pgdump.zst.age | zstd -d | sha256sum
```

The two values should match.

## Revoking a key [#revoking-a-key]

Revoking a key stops new backups from being encrypted to it. Files already written keep it in their header, and their encryption can't be changed without rewriting them. To fully retire a key, revoke it **and** delete any copies that key should no longer open.
