---
title: "Recover"
description: "Open an artifact with no account, no CLI, and no saved.sh."
url: "https://saved.sh/docs/recover"
---

A backup you can only open through a vendor is a dependency, not a backup. This page exists
so that sentence is testable.

Everything below uses tools already on your machine. None of it involves us.

## What an artifact is [#what-an-artifact-is]

An artifact is an ordinary file, produced by a pipeline with no proprietary step:

```
dump → compress → encrypt → upload
```

Reversing it is the same steps backwards, with `gpg`, `gunzip` and whatever tool made the
dump. There is no saved.sh container format, no custom codec, and no index we hold that you
need.

## 1. Get the bytes [#1-get-the-bytes]

While you have an account, the fastest route is the CLI or the dashboard:

```bash
sctl artifact get <artifact-id>
sctl artifact download <artifact-id> --output ./artifact
```

`sctl artifact get` prints what you need to open it: the filename, the size, the checksum,
whether it is compressed and encrypted, and **which key** it was encrypted to.

<Callout>
  Downloads are never gated on billing state. A workspace that is behind on payment can still
  read and download every artifact it has. Your backups are not leverage.
</Callout>

### Without our API [#without-our-api]

Where the bytes are depends on how the backup was configured to deliver them.

| Where the artifact lives                                 | How you get it without us                             |
| -------------------------------------------------------- | ----------------------------------------------------- |
| **Your own bucket** ([delivery](/docs/backups/delivery)) | Directly, with any S3 client and your own credentials |
| Our storage                                              | Through a presigned URL from our API                  |

If keeping a copy you can reach without us matters, that is what a **destination** is for.
Point a backup at a bucket you own, and the artifact lands there under
`<workspace-id>/<backup-id>/<run-id>/artifact`, readable with `aws s3 cp` and no involvement
from us at all.

<Callout type="warn">
  Artifacts in **our** storage are not directly listable by you: we hand out a presigned URL
  rather than credentials to the bucket. That is the correct security posture, and it is also a
  dependency. If your recovery plan must survive us being unreachable, deliver to your own
  bucket as well.
</Callout>

## 2. Decrypt [#2-decrypt]

With the private key you generated, the half we never had. The command is identical on
Linux, macOS and Windows once `gpg` is installed; see
[installing gpg](/docs/security/key-management#installing-gpg).

```bash
gpg --decrypt app.dump.gz.gpg > app.dump.gz
```

```powershell
# Windows PowerShell: redirect as raw bytes, not text
gpg --output app.dump.gz --decrypt app.dump.gz.gpg
```

<Callout type="warn">
  On Windows, use `gpg --output <file>` rather than `>`. PowerShell's redirection re-encodes
  the stream as text and will corrupt a binary dump.
</Callout>

<Callout type="error">
  If this key is gone, stop. There is no recovery path, no support request and no override. We
  store ciphertext and we do not have your key. Verify you still hold it *before* you need it.
</Callout>

<Callout type="warn">
  **This does not decompress.** Compression is applied before encryption, so decrypting a
  compressed artifact gives you a gzip file and step 3 is not optional. `gpg` does undo the
  PGP message's own internal compression, which is a different layer and not the one named in
  the filename.
</Callout>

## 3. Decompress, if it was compressed [#3-decompress-if-it-was-compressed]

Whether or not it was also encrypted. A compressed artifact is a plain gzip file once any
PGP wrapper is off:

```bash
gunzip restored.gz
```

The filename records the layers in the order they were applied, so `app.dump.gz.gpg` peels
right to left: `gpg` first, then `gunzip`. Check `sctl artifact get` if the name has been
lost, or just look at the
[magic bytes](/docs/recover/artifact-format#identifying-an-artifact-from-its-bytes): `1F 8B`
is gzip.

## 4. Restore [#4-restore]

Now it is a plain dump, and the tool that made it is the tool that reads it:

| Source                  | Command                                       |
| ----------------------- | --------------------------------------------- |
| `postgres`              | `pg_restore -d mydb app.dump`                 |
| `mysql`                 | `mysql -u root -p app < app.sql`              |
| `redis`                 | Stop Redis, copy the RDB into place, start it |
| `s3`                    | `tar -xf archive.tar`                         |
| `folder`                | `unzip folder.zip`                            |
| `file`, `script`, `web` | It is already the file                        |

## Doing it with the CLI instead [#doing-it-with-the-cli-instead]

`sctl restore` does all four steps in one command, on your machine.

**The target type is the subcommand**, and each takes only the flags that type needs.

```bash
# Replayed into a database
sctl restore postgres <artifact-id> --host localhost --database scratch --user postgres
sctl restore mysql <artifact-id> --database scratch --user root

# Written to a path
sctl restore file <artifact-id> --path ./restored.tar
sctl restore folder <artifact-id> --path ./restored.zip
```

| Subcommand          | Flags                                                                             |
| ------------------- | --------------------------------------------------------------------------------- |
| `postgres`, `mysql` | `--database` (required), `--host`, `--port`, `--user`, `--password`, `--ssl-mode` |
| `file`, `folder`    | `--path` (required)                                                               |

`--key` applies to all four: a private key file, defaulting to your gpg keyring or agent. If
you pass it, the key is imported into a temporary keyring that is deleted when the command
exits.

Restore is entirely client-side: download, decrypt with your key, decompress, reconstruct.
Neither the target nor its credentials are ever sent to us.

<Callout>
  You name the target yourself, so **nothing is read from the backup definition**. A restore
  works when the definition has been deleted but the artifact has not: you need the artifact
  and your key, and nothing else.
</Callout>

## Do this on a normal day [#do-this-on-a-normal-day]

The procedure above is worth performing **once, deliberately, while nothing is wrong**,
against a scratch database, on a laptop, with the CLI logged out.

That rehearsal is what turns a backup into a recovery plan. It also catches the two failures
that only ever surface under pressure: a private key nobody actually kept, and a dump that
restores into a schema that has since moved on.

<Cards>
  <Card href="/docs/recover/rehearsal" title="Rehearsal" description="The drill, and how often to run it." />

  <Card href="/docs/recover/artifact-format" title="Artifact format" description="Every layer, and how to identify it from the bytes." />

  <Card href="/docs/recover/restore-postgres" title="Restore Postgres" description="From artifact to running database." />

  <Card href="/docs/recover/restore-files" title="Restore files" description="Unpacking file, folder and script artifacts." />
</Cards>
