Skip to content
SupaCovedocs

06 / 13

Storage destinations

Send encrypted backups to your own S3, Cloudflare R2 or Backblaze B2 bucket, from the console or through the API

A destination is a bucket of yours that SupaCove uploads each encrypted backup to. Without one, backups stay on the instance's local disk only.

Once a destination is bound to a database, manual and scheduled backups use it automatically. You configure destinations in the console or through the HTTP API; there is no CLI command or environment variable for them.

Both were run against a local instance with MinIO as the S3-compatible store on 2026-10-08. Cloudflare R2 and Backblaze B2 use the same code path but have not been tested by us against the real services.

In the console

Open Storage in the top bar.

  1. Add destination. Fill in the platform, bucket and credentials (the fields are the same as in the API) and press "Test and add". The bucket is tested before anything is saved; a failure shows the storage provider's error and saves nothing.
  2. Where each database uploads. Pick a destination for each database in the second panel. "None" keeps its backups on the instance only. The change applies from the next backup and is refused while a backup of that database is running.

Each destination row also has Test (write, read back and delete a test object), a reconcile button that compares the bucket with what the instance has recorded, and remove. Removing is refused while a database still uploads there.

The rest of this page describes the same operations through the API.

Sign in from the shell

The API uses the same session as the console: a session cookie, plus a CSRF token on every request that changes something.

BASE=https://backup.example.com/api

# 1. Sign in. The cookies land in cookies.txt.
curl -s -c cookies.txt -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"<your password>"}' \
  "$BASE/auth/login"

# 2. Read the CSRF token out of the cookie file.
CSRF=$(awk '$6 ~ /sb_csrf$/ {print $7}' cookies.txt)

After that:

  • GET requests need only -b cookies.txt.
  • POST, PUT and DELETE also need -H "X-CSRF-Token: $CSRF", and -H 'Content-Type: application/json' when they carry a body.

Things that go wrong here:

ResponseCause
401 unauthenticatedNo session cookie, or the session expired (7 days). Sign in again and re-read the CSRF token.
403 csrfThe X-CSRF-Token header is missing or belongs to another session.
403 cross_originThe request carried an Origin header that does not match the instance. curl sends none by default; do not add one.

The cookies are named __Host-sb_session and __Host-sb_csrf. Only an instance running with SB_INSECURE_COOKIE=1 (plain-HTTP development) drops the __Host- prefix. The awk line above matches both.

Create a destination

The bucket must already exist. SupaCove does not create buckets.

curl -s -b cookies.txt -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' \
  -d '{
    "name": "offsite-s3",
    "platform": "s3",
    "region": "eu-central-1",
    "bucket": "my-backups",
    "prefix": "supacove",
    "accessKey": "<access key id>",
    "secretKey": "<secret access key>",
    "verifyReadback": true,
    "keepRemote": 10,
    "keepDays": 0
  }' \
  "$BASE/destinations"

Before anything is saved, the server writes a small test object into the bucket, reads it back, compares it and deletes it. If any of those steps fails, the response is 422 diagnostic_test_failed with the storage provider's error, and nothing is stored. A 201 returns the destination, including its id.

The credentials therefore need delete permission too, which retention needs anyway.

Fields

FieldRequiredNotes
nameyes1–100 bytes of UTF-8 after trimming spaces (a CJK character takes three), unique
platformnos3 (default), r2 or b2
endpointfor r2 and b2http(s)://host[:port], no path. Optional for s3: set it for an S3-compatible service
regionfor b2Defaults to us-east-1 on s3 and auto on r2
bucketyesA valid S3 bucket name: 3–63 characters, lowercase letters, digits, dots and dashes
prefixnoLetters, digits, /, ., _ and -; .. is rejected. Leading and trailing slashes are trimmed and one trailing slash is added. Empty means the bucket root
accessKey, secretKeyyesCredentials that can put, get, list and delete objects under the prefix
keepRemotenoCopies to keep per database, default 10, minimum 1
keepDaysnoAge limit in days; 0 (default) turns it off
verifyReadbacknoDefault true. Stored and returned; see the note below

How keepRemote and keepDays are applied, and which backups are never deleted: Schedules & retention.

The read-back check cannot be switched off

Every upload is read back from the bucket and checked against the SHA-256 of the ciphertext. That check always runs: verifyReadback is stored and returned, but setting it to false does not skip it.

How credentials are kept

secretKey is encrypted with the instance's master secret before it is written to the database. accessKey is stored as plain text. Neither is ever returned by the API. If the master secret changes, the stored secret can no longer be read and the destination has to be created again.

Bind it to a database

curl -s -b cookies.txt -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' \
  -X PUT -d '{"destinationId": 1}' \
  "$BASE/databases/1/destination"

GET $BASE/databases and GET $BASE/destinations list the ids. A database has at most one destination. From the next backup on, a job counts as succeeded only after the ciphertext and its manifest are committed to the bucket; its remoteState is then committed.

In the bucket each backup is two objects under <prefix>backups/: the ciphertext (….dump.age) and its manifest (….manifest.json).

Check it later

# Run the write / read / delete test again.
curl -s -b cookies.txt -H "X-CSRF-Token: $CSRF" -X POST "$BASE/destinations/1/test"

# Compare the bucket with what the instance has recorded.
curl -s -b cookies.txt -H "X-CSRF-Token: $CSRF" -X POST "$BASE/destinations/1/reconcile"

The reconcile report counts remoteObjects and matched, and lists orphaned (in the bucket, unknown to the instance), missing (recorded, not in the bucket) and uncommitted objects. It only compares. It does not repair or delete anything.

Download a remote backup

curl -s -b cookies.txt "$BASE/tasks/7/download-url"

The response holds a presigned URL for the ciphertext, valid for 15 minutes. The URL points at the destination's own endpoint, so it has to be reachable from the machine that downloads.

Change a destination

There is no update call. Create a new destination, bind the database to it, then delete the old one.

Remove a destination

Unbind first, then delete:

curl -s -b cookies.txt -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' \
  -X PUT -d '{"destinationId": null}' \
  "$BASE/databases/1/destination"

curl -s -b cookies.txt -H "X-CSRF-Token: $CSRF" -X DELETE "$BASE/destinations/1"

What deletion does and does not do:

  • It is refused with 409 in_use while a database is still bound to the destination, and with 409 upload_in_flight while an upload to it is running.
  • It removes the destination from the instance. The objects in the bucket are left untouched.
  • Backups already uploaded there can no longer be fetched through download-url, which answers 409 destination_removed. Get them from the bucket directly.

Error codes

StatusCodeMeaning
400invalid_requestA field failed validation; the message names it
409name_existsAnother destination has that name
422diagnostic_test_failedThe test object could not be written, read back or deleted; the provider's error is included
409not_assignableBinding failed, for example because the destination id does not exist
409in_useDelete refused while a database is bound to the destination
409upload_in_flightDelete refused while an upload is running
409destination_removeddownload-url for a backup whose destination was deleted
404not_foundUnknown destination id

Last updated

On this page