Backups you drive

You already run something. Make it back up.

A pipeline, a cron entry, a deploy hook, a script nobody has touched in a year. Whatever already has the credentials and already runs on a schedule can push a backup with one command, or one API call. No agent, no inbound access, no second scheduler. What you are missing is somewhere safe for the output to land.

$ pg_dump "$DATABASE_URL" | saved backup submit prod-db -

No card required. USD 50 credit on every new workspace.

Why this one breaks quietly

Most pipeline backups end in a bucket nobody looks at

The step itself is easy, and almost every team has written it. What is missing is everything after it: whether the file is complete, whether it is reachable by whatever just compromised the database, whether anyone would notice the night it stopped running, and whether anyone can still open it in a year.

ONE BACKUP, ONE YEAR, ONE SQUARE PER NIGHTEIGHT WEEKS WITH NO RUN AND NO ALERTsucceededfailednever ran

A year of nightly backups. The band in spring is the workflow that silently stopped being scheduled. A directory listing shows you the squares on either side of it and never the hole.

The file that is 20 bytes

pg_dump fails, gzip succeeds compressing nothing, and the pipeline exits zero. Every run records a size, so a backup that produced 4 KB instead of 4 GB is visible the next morning rather than in a year.

The copy in the same account

A bucket in the account that runs the database shares its blast radius. Whoever gets production access gets the backups, which is precisely the scenario they existed for.

The archive nobody can open

Artifacts record their own format, compression and encryption. Unpacking one by hand uses tools already on your machine, and the procedure is documented rather than implied.

Before you ask

The four things a platform team says next

“We are not installing an agent.”

You are not. The CLI is a static binary the job downloads and throws away with the runner. Nothing persists between builds, nothing listens, and there is no inbound access to grant.

“Our runner cannot reach the database.”

Then this is the wrong path and a worker is the right one. It runs next to the database on your own hardware and we drive the schedule. Same artifact, same encryption, same retention.

“We do not want another secret in CI.”

It is one API key, scoped to a single workspace with only the permissions you grant it. It cannot read your artifacts, because nothing can: they are encrypted to a key that never enters the runner.

“What stops this silently breaking?”

The job fails, and a red pipeline is a signal you already watch. That is also the honest limit of this path: if the workflow stops being scheduled at all, we have nothing to notice on your behalf.

However you drive it

The same step, wherever it runs

The CLI reads stdin, so its shape does not change between callers. Underneath, all four are the same three calls, and the raw API is there for the cases a shell pipeline cannot express. There is no plugin to install and no marketplace action to trust with your database credentials.

GitHub Actions

.github/workflows/backup.yml

on:
  schedule: [{ cron: "0 2 * * *" }]

jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - run: |
          pg_dump "$DATABASE_URL" \
            | saved backup submit prod-db -
        env:
          SAVED_API_KEY: ${{ secrets.SAVED_API_KEY }}

GitLab CI

.gitlab-ci.yml

nightly-backup:
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
  script:
    - pg_dump "$DATABASE_URL"
        | saved backup submit prod-db -

cron, or any shell

crontab, Jenkins, a Makefile

# it reads stdin, so the shape
# never changes between callers
pg_dump "$DATABASE_URL" \
  | saved backup submit prod-db -

Anything that speaks HTTP

REST API

# ask for a URL, then PUT to it
curl -X POST $API/v1/.../runs
curl -X PUT "$UPLOAD_URL" --data-binary @dump.sql
curl -X POST $API/v1/.../confirm
ci-runner
$ pg_dump $DATABASE_URL | saved backup submit prod-db -
reading stdin
encrypting to 9F2A…C410 (your key)
uploading 4.21 GB ████████████████████ 100%
sealing sha256:7c1e…8b03
✓ run_01JQ8F3K2M sealed, locked until 2026-11-02
$ saved run list --backup prod-db --limit 3
03:12 ✓ 4.21 GB 00:03:12
02:00 ✓ 4.19 GB 00:03:04
01:00 ✓ 4.19 GB 00:03:09

No agent. No inbound port. Ten lines of output you can read.

What leaves your machine

Ciphertext, and nothing else

Encryption happens in the job, with your public key, before anything crosses the network. That is what makes an API key in CI a small thing to grant: it can create runs, and it cannot read a single byte of what those runs produced.

YOUR MACHINEreadable datayour private keynever leavesTHE NETWORKENCRYPTED HEREWHAT WE HOLDciphertextand metadatasizechecksumcreated atkey fingerprinttake all of it and it still does not open

The private half never enters CI

You upload a public key. There is no configuration in which the runner, or we, hold the half that decrypts.

A held artifact stays held

Set a hold and it is stamped onto every artifact written from then on. It can be lengthened and never shortened, so a compromised key cannot loosen the policy and then delete.

Bulk data skips us

The artifact goes straight to object storage against a short-lived URL. Only metadata passes through our orchestration.

For the whole team

Three people have to agree to this

The engineer adding it

How long is this going to take?

  • One step, and it reads stdin so it fits whatever you already produce
  • No machine to provision and nothing to keep running between backups
  • A red build if it breaks, which is a signal you already watch

The reviewer on the PR

What are we granting, exactly?

  • One workspace-scoped API key, with only the permissions you give it
  • Outbound HTTPS only, so no firewall change and no inbound port
  • The key cannot read artifacts, because encryption happens before upload

Whoever owns the budget

What does it cost to run?

  • No seats, and no charge for the pipeline minutes we never used
  • Almost no computation, because the run happened on your infrastructure
  • USD 50 of credit, no card, and nothing auto-charges when it runs out
When to move up

Push until handing over the schedule buys you something

Two things this path cannot do, and both are reasons to add a worker rather than to start with one. You can adopt it later without changing anything you already wrote.

One vaultsame encryption,same retention,same artifact01Your CI/CDcron, pipeline, scriptYOU RUN IT, YOU PUSH IT02Your workeryour hardwareWE SCHEDULE IT03Our workerour infrastructureWE RUN ITMORE HANDED OVER AS YOU GO DOWN. THE RIGHT-HAND SIDE NEVER CHANGES.

Nobody notices a run that never happened

Your pipeline owns the schedule, so a workflow that stops firing produces no signal on our side. A worker gives us an expected run to miss, which is the difference between a schedule and a cron entry that stopped in March.

The runner has to reach the source

A database behind a VPN, on-premise, or with no route from hosted CI is not reachable from a runner. A worker sits next to it and connects locally.

What it costs

The cheapest of the three paths

The run happened on your infrastructure, on your time, so this shape generates almost no computation and no working storage on our side. A bill is archive plus the transit to get the bytes here, which means retention is the lever rather than how often you run.

What you need

  • A runner that can already reach the source
  • An API key stored as a repository secret
  • A GPG public key uploaded to the workspace

That is the whole list. No machine to provision, no network change to request, and nothing left running between backups.

Questions

Add the line, watch one run, then decide

If it does not show up somewhere you like, you have lost a line of YAML. That is the whole risk.