Delivery
Where artifacts land, saved.sh storage or a bucket you own.
View as MarkdownBy default an artifact is written once, to our storage. A delivery plan changes that: you can add copies in buckets you own, and you can tell us not to keep one of our own.
Every copy is the same finished object, compressed and encrypted exactly as ours is. Your bucket never receives a raw dump.
The model
| Noun | Lives on | Holds |
|---|---|---|
| Destination | The workspace | Provider, endpoint, region, bucket, prefix, and one delete grant |
| Delivery plan | The backup | Which destinations, and the three switches |
| Location | The artifact | One row per copy: where it went, and whether it got there |
A run that delivers to our store and two of your buckets produces one artifact with three
locations. keep_last counts artifacts, not copies, and a copy that failed is visible
rather than rounded away.
Linking a destination
Only S3-compatible stores are accepted: AWS S3, Cloudflare R2, Backblaze B2, Wasabi, MinIO, DigitalOcean Spaces. Google Cloud Storage and Azure Blob are not S3-compatible and are out of scope.
| Field | Notes |
|---|---|
name | How you select it on a backup |
bucket | Required |
endpoint | For non-AWS stores. A bare host or a full URL both work |
region | Optional |
prefix | Optional. Artifacts land under it, in the same tree layout as ours |
usePathStyle | For MinIO and most self-hosted stores |
allowDelete | Whether retention may delete from this bucket. Off by default |
Credentials are stored in our vault and never returned by the API.
Linking proves the bucket works
A destination is not trusted on your word. Linking writes a small probe object, reads it back, compares the bytes, and deletes it.
All three steps matter. A policy granting PutObject but not GetObject would pass a
write-only check and then strand every artifact you ever sent it. One forbidding
DeleteObject leaves the probe behind forever.
| State | Meaning |
|---|---|
unverified | Not checked yet |
active | The probe round-tripped |
failed | It did not, and the reason is stored |
A failed probe is stored with its reason rather than refused, because "wrong key" is fixed by editing the destination, not by creating it again.
The minimum policy is PutObject and GetObject on the prefix, plus DeleteObject for the
probe. Grant DeleteObject permanently only if you also want allowDelete.
The three switches
backups:
- name: prod-db
kind: cloud
delivery:
destinations: [prod-archive-eu]
skip_permanent: false
keep_permanent_on_failure: false
second_copy: true| Switch | Meaning | Requires |
|---|---|---|
skip_permanent | Do not keep our own copy | At least one destination |
keep_permanent_on_failure | If a destination fails, keep our copy anyway | skip_permanent |
second_copy | Also keep a redundant copy in our independent second space | Not skip_permanent |
Each requirement is enforced rather than ignored:
keep_permanent_on_failurewithoutskip_permanentis refused, because we always keep a copy in that case, so accepting it would be a setting that silently does nothing.second_copywithskip_permanentis refused, because the second copy is made from our copy and there would be nothing to replicate.
Destinations are deduplicated, and naming one twice is not an error.
What a run actually does
Your buckets are written first, one at a time, each with its own retries. Our copy is decided afterwards.
inspect → compress → encrypt → plan → [dest 1 … dest N] → ours? → finalizeThe order is what makes the fallback possible: only once your buckets have been tried is it
known whether skip_permanent should hold.
| Outcome | Artifact record | Run result |
|---|---|---|
| Every copy stored | Written, with every location | Succeeds |
| Some stored, some failed | Written, locations say exactly which exist | Fails |
| Nothing stored anywhere | Not written | Fails, before finalize |
A partially delivered run fails even though the artifact exists. A destination you selected did not get your data, and reporting success would be a claim we cannot back. Check the artifact's locations to see which copies you actually have.
When nothing is stored, no artifact record is written and the temporary object is left alone, so a retry still has the bytes.
Two situations override the plan rather than honouring it, and both are logged:
- The backup definition is gone mid-run. We deliver one copy to our own storage.
- Every selected destination has been unlinked. We drop
skip_permanentand keep our copy, rather than discarding an artifact to honour a plan that no longer exists.
What changes when you own the bucket
Download
With skip_permanent, the bytes only ever went to your bucket. Download is refused, and
the API tells you where the artifact is instead.
That is a deliberate limit on ourselves. We hold write credentials to your bucket, not a mandate to read your data back out of it.
Delete
Deleting an artifact through our API removes only our copy. Copies in your buckets stay where they are.
The retention sweep touches your bucket only where allowDelete is on, read live at sweep
time. Turning it off stops the sweep touching artifacts that already exist. See
Retention.
Unlinking
Unlinking a destination a backup still selects is refused. Doing it silently would turn a backup that promised three copies into one that makes two, with nothing saying so until a restore needed the missing one.
Remove it from the backups first, then unlink.
The second space
Not available yet. The second space is still being built. second_copy is accepted by
the API and the manifest, but no replica is made and nothing is charged for one. This
section describes what it will do, not what it does today.
second_copy keeps a redundant copy on a separate provider and a separate account of
ours. It is for the failure where our primary object store, or our account on it, is the
thing that is gone.
The copy is made by a replicator outside the run, so it is not something the run waits on.
Billing
BYOB and the second space add no meter of their own. What changes is which of the four base meters you are charged on.
| Situation | Charged |
|---|---|
| Our copy kept (the default) | computation, storage, and archive while it is held |
skip_permanent | computation and transit only. Your bytes never touch our storage |
| Any destination written | transit for the egress, once per successful copy |
second_copy | transit once for the upload, then archive at double the bytes |
skip_permanent is therefore the cheapest way to run us, and the one where we can do least
for you. That trade is yours to make.