Skip to content
downpipes docs

downpipe CLI command reference

downpipe is the standalone, MIT-licensed Go command line for downpipe archives. It is the tool a break-glass recoverer runs to verify and restore an encrypted Cloudflare backup straight from the destination bucket bytes and the customer-held offline keys, with neither Cloudflare nor any vendor service in the loop. This page is the per-command reference for everyone who runs it: the recoverer on the reader path, and the operator who uses the same binary to stand up or upgrade the engine.

The binary carries two distinct paths with two distinct trust postures, and keeping them separate is the mental model that makes the rest of the page read cleanly. The reader path (keygen, inspect, keys, verify, attest, restore, selftest) makes no network call to Cloudflare or any vendor at all; when it reads from an S3-compatible bucket it issues GET requests only and refuses every HTTP redirect, so a hostile endpoint cannot bounce the read to another host. The provisioner path (setup, preflight, init, update) is the only path that talks to api.cloudflare.com, and only with the operator’s own API token, read from the environment and never sent to the vendor or the in-account console.

The deep, step-by-step offline-recovery walkthrough lives on this page, under recover a bucket end to end. Other entry points (the break-glass offline overview, the restore flow) are shorter and point here for the exact commands.

Install the tool truthfully

The quickest install is go install, which needs Go 1.26 or newer and network access to fetch the module:

go install github.com/downpipes/downpipe/cmd/downpipe@latest
downpipe --help

Run --help straight after installing and confirm prune, recombine and unseal-export are all listed before you rely on the binary. @latest resolves to whatever the newest git tag names, currently v0.2.0, which carries all three commands, and a tag-drift check in the downpipe repository now fails CI if a tag is ever cut without them. That check protects the tag, not an install you already made: a cached module resolution or an install done before a fix landed can still leave you with an old binary, which is exactly the trap this page used to warn about (v0.1.1, tagged before prune, recombine and unseal-export existed). Confirming the command set yourself takes five seconds and removes the doubt either way.

For a build with no network dependency at all, clone the repository instead:

git clone https://github.com/downpipes/downpipe
cd downpipe
go build -mod=vendor ./cmd/downpipe
./downpipe version

Every dependency the reader needs, including the post-quantum signature module, is vendored into ./vendor, so the build itself downloads no module and makes no network call. That offline-from-source property is what underpins the recovery promise: if both the vendor and Cloudflare are gone, and even if go install can no longer reach a module proxy, you can still rebuild the recovery tool from a held copy of this repository alone, provided the recovery machine’s own Go already satisfies the toolchain pin below.

go.mod pins toolchain go1.26.5, a deliberate security decision (the pin forces a patched toolchain rather than letting a stale local Go build a reader with known, fixed vulnerabilities). Under Go’s default GOTOOLCHAIN=auto, if the recovery machine’s installed go is older than the pin and that exact toolchain is not already cached locally, the vendored build tries to download it and, with no network available, fails outright rather than silently falling back to the older Go. Keep a go binary meeting the pinned version alongside your recovery kit, or confirm ahead of time it is already cached on the machine you intend to recover with, rather than discovering the gap during a real recovery. GOTOOLCHAIN=local builds with whatever Go is already installed, without the toolchain pin’s security-patch guarantee; treat it as a last resort, not a substitute for holding the right toolchain in advance.

The tool version is a different thing from the archive format version. The binary stamps its own build version (dev for go install or an unstamped local build, since neither runs the maintainer’s release pipeline), while the on-disk archive format is the literal string downpipe/0.1.0. The two move independently: a new tool build never changes the format string, and a change to a byte-level format rule would be a new format version, not a tool release. The reader accepts exactly the downpipe/0.1.x line, so it opens any archive in the current format and refuses one that names a different identity.

The command set at a glance

downpipe <command> [flags] dispatches fourteen subcommands across the two paths, plus three small built-ins. The reads-versus-writes column is the honest summary, and each command has its own section below.

Command Path Reads or writes
keygen reader Writes four key files to a local directory; refuses to overwrite an existing key file
inspect reader Reads only; shows the cleartext root manifest, and the encrypted detail when given an identity
keys --which reader Reads only, keyless; groups an archive’s runs by the recipient identity that opens them
verify reader Reads only; proves a run whole and genuine and restores nothing
attest reader Reads only, keyless; needs no break-glass identity and materialises no plaintext
restore reader Dry run by default; writes to the target only under --apply, and never overwrites existing state
selftest reader Builds and recovers a sample archive in a temporary area, end to end
prune reader Offline retention prune, for a break-glass-only posture where the SCHEDULED pass cannot run unattended. The console’s break-glass prune panel runs the same prune on demand without a terminal; this command is the offline alternative. Dry run by default; deletes only under --apply. --receipt <path> writes a JSON record of the pass, written before the first delete so an interrupted prune still leaves an account of itself. Because the offline prune holds only the signer public key, it cannot mark a superseded RUNLOG entry the way the engine’s own prune does, so a later attempt to open that run reports it listed but its tree entirely absent, worded deliberately as ambiguous: that is both the expected state after a completed prune and what an attacker’s deletion of the run would look like, and nothing in the archive tells the two apart
recombine reader Rebuilds identity.key from the console’s custody artefacts. WRITES a complete key to disk, which is the whole difference from combining in memory
unseal-export reader Opens a sealed control-plane export offline with the break-glass identity, and writes the recovered plaintext export JSON
preflight provisioner GET-only account checks against api.cloudflare.com; provisions nothing
setup provisioner Dry run by default; under --apply it WRITES Access, Secrets Store and binding changes
init provisioner Prints a runbook only; makes no Cloudflare call
update provisioner Prints a runbook only; makes no Cloudflare call
version, spec, help built-in Print the tool version, point at the format spec, or print usage

version (also --version and -v) prints the build version. spec points at docs/format/SPEC.md. help (also -h and --help) prints the usage text. Running the tool with no arguments prints usage and returns 6, the usage code, the same as an unrecognised command; see exit codes and verification outcomes.

Source selection (reader path)

The five reading commands (inspect, keys, verify, attest, restore) take the same source flags, so you point them at the bucket bytes the same way every time. Choose exactly one backend.

Flag Purpose
--archive <dir> Read from a local copy of the whole bucket tree
--s3-endpoint <url> Read from an S3-compatible bucket (R2, Backblaze B2, Wasabi, MinIO, AWS S3) instead of --archive
--s3-bucket <name> The bucket name, required with --s3-endpoint
--s3-region <region> The region with --s3-endpoint (defaults to auto)

S3 credentials come from the standard AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables, never from a flag, so a key never lands in a process listing or shell history. The S3 reader issues GET requests only and refuses every redirect, so the read cannot be bounced to a host the bucket did not name.

Reader commands

keygen

keygen generates the offline key material in your browser-free, air-gapped path. It writes four files into the --out directory (default the current directory): identity.key (the break-glass private key, the only thing that can decrypt, so keep it offline and on a printed recovery sheet), recipient.pub (the recipient public the writer encrypts to), signer.pub (the operator signer public the reader pins, which verify and restore both require, so keep a copy of this one too), and signer.key (the signer private the writer signs runs with). Each file is created at owner-only permissions and the command refuses to clobber an existing key file, so a re-run can never silently overwrite a break-glass key.

inspect

inspect --run <runId> reads the cleartext bootstrap manifest and prints what the run holds: the format version, the run id, the creation time, the envelope suite, the declared record count, shard count, whether a break-glass recipient is present, and the recipient set with fingerprints. It needs no identity, so it is the first look you can take without touching a key. Pass --identity together with --signer to open the encrypted preamble as well, which adds the downpipe name, cadence, source type, the captured window and the per-source restore descriptors a record carries (KV expiration and metadata, R2 metadata, the secret binding wiring, the D1 format).

inspect is a diagnostic viewer, not a verifier, and it now says so on screen rather than leaving you to infer it. Run without --identity it opens nothing at all, so no signature, no record hash and no RUNLOG is checked, and every line it prints is the bucket’s own account of itself; the command states this in its output. The declared signer fingerprint is a claim by the archive rather than proof, since the manifest is signed under the very key that line names, so it answers “is this the ceremony my recovery sheet describes” and never “is this archive authentic”.

Run with --identity and --signer it does genuinely verify the root signature, the master capsule, the key commitment, the recipient set, the record count and the Merkle root before printing the decrypted detail. On the anti-rollback freshness check it deliberately does not enforce: it waives the gate so it can show you a rolled-back or unverifiable run rather than refusing the archive you are trying to diagnose. It prints the freshness verdict on every open, whether or not it passed, so the line’s absence never has to be interpreted, and it points you at verify. It offers no override, because it has already applied one. When you need a run refused rather than shown, that is verify.

keys

keys --which is a keyless diagnostic over an archive’s public signed root manifests and the RUNLOG. It answers “which offline key opens which runs, and across what date range” across key rotations: the recipient fingerprints and roles are recorded in the clear in each run’s signed root, so the command reads the RUNLOG to enumerate the runs, reads each run’s root, and prints the runs grouped by recipient identity (break-glass first, then by fingerprint), each identity’s run count and time window, and the run ids it opens. It needs no --identity and unwraps nothing. A run whose root cannot be read is skipped and reported to stderr rather than aborting the whole index, except for runs whose tree has been pruned away: those are counted and summarised on one line, because on an estate under retention they are the normal state of a healthy archive rather than something to warn about once per run. That count is worth comparing against the retention you actually ran.

verify

verify proves a run is whole and genuine, and restores nothing. It recovers the run master from the master capsule using the break-glass identity, derives every file key, and then checks the hybrid signature against the supplied signer, every shard hash, the recipient set, the break-glass binding, the key commitment, the Merkle root over every record hash, the declared record count, and the RUNLOG freshness. Because the Merkle root binds each record hash, which binds its plaintext hash, that chain covers every record’s integrity by construction, but by default verify is shallow: it checks the signed root, the shard manifests and the RUNLOG only, and never reads a seg/ data object itself, so a flipped ciphertext byte in a shard passes a shallow verify. Pass --deep to close that gap: it runs the same decrypt-and-discard path as restore --sink discard, reassembling every record, AEAD-tag verifying every chunk and checking each record’s plaintext SHA-384, and writing nothing.

Flag Purpose
--run <runId> The run to verify (required)
--identity <file> The break-glass identity file (required, unless you supply the custody artefacts below)
--signer <file> The operator signer public-key file (required)
--allow-stale Proceed despite this run’s age and nothing else: the RUNLOG verified against --signer, is internally consistent, and says either that this run is not the latest for its downpipe or that its maximum index is below your --min-runlog-index pin. It does not waive the RUNLOG’s signature, presence or self-consistency, so it covers only part of exit 5
--allow-unverified-runlog Proceed despite the RUNLOG itself being untrustworthy: absent, unreadable, unparseable, empty, not carrying this run, failing to verify against --signer, disagreeing with the signed root manifest, or internally contradictory. This is the acknowledgement for the rest of exit 5. Nothing is then established about this run’s recency, and a bucket someone has rolled back or replaced looks exactly like this
--allow-unverified Proceed despite a missing or invalid signature, recording the true outcome. Implies both acknowledgements above
--min-runlog-index <n> Reject a RUNLOG whose maximum index is below this anti-rollback pin
--acknowledge-no-rollback-pin Silence the stderr reminder printed when --min-runlog-index is left unset. Rollback protection stays off either way; this only suppresses the reminder, for a genuine first recovery before a recovery sheet entry exists. The receipt still records the pin as unset
--check-bundle Verify the in-bucket recovery bundle against its signed SHA384SUMS
--deep Also decrypt and hash-verify every record’s segment bytes (the full restore path, writing nothing); without it, verify checks the signed manifests only, never a seg/ data object
--receipt <path> Write a signed verify receipt to this path (- writes to stderr)
--receipt-signer <file> Sign the receipt with this signer private-key file

Leaving --min-runlog-index unset prints a one-line stderr warning on every run, naming the missing flag and what to pass instead; it never blocks the run and never touches stdout. Pass --acknowledge-no-rollback-pin to silence that reminder once you are certain there is no recovery sheet entry yet to pin against.

The verdict and a one-line summary go to stderr, and the process exit code is the machine-readable result. The full code contract, and exactly what the salvage overrides change, is on exit codes and verification outcomes.

attest

attest --run <runId> is the keyless check: it verifies the root signature and the structural completeness of the signed root (the shard hashes and the shard count), without the break-glass identity. It never unwraps the master capsule, opens a shard, or decrypts a record, so it needs no --identity and materialises no plaintext. The --signer flag is optional: with a signer the signatures are cryptographically verified against the operator-pinned key; without it the attestation is fully keyless and reports the signature as unchecked. Either way, attest catches a tampered shard, a missing shard, and a malformed or absent signature.

Freshness is a separate question, and worth stating precisely rather than folding it into the paragraph above. A plain attest with no --min-runlog-index checks only that the run is present in the RUNLOG, never its freshness, so a rolled-back RUNLOG that still lists the run passes clean. Pass --min-runlog-index <n> to close that gap. With --signer, the pin is a full cryptographic anti-rollback check, exactly as verify and restore’s. Without --signer, the pin is honoured only structurally: it reads the RUNLOG’s own claimed index, which is not itself authenticated in keyless mode, so it catches an accidentally stale or wholesale-replayed bucket but not a targeted adversary who edits the unsigned RUNLOG’s stated index along with the rollback. The printed report always states which of the two it was.

Flag Purpose
--run <runId> The run to attest (required)
--signer <file> The operator signer public-key file (optional; without it the attestation is keyless)
--min-runlog-index <n> Reject a RUNLOG whose maximum index is below this pin: a full cryptographic anti-rollback check with --signer, a structural, unauthenticated one without it
--acknowledge-no-rollback-pin Silence the stderr reminder printed when --signer is set and --min-runlog-index is left unset. The reminder fires only when a signer is pinned, since that is the only case where the RUNLOG signature is actually checked

Record-level completeness needs the identity, so for that you run verify or restore.

restore

restore is a dry run by default. It plans the writes and reports any conflict with state already present in the target, and writes nothing. Pass --apply to opt in to writing. On a real apply it reassembles each record’s value, verifies it against its signed plaintext hash as it writes, and never overwrites an existing destination key (a key that appears between the plan and the write fails loudly rather than clobbering).

Flag Purpose
--run <runId> The run to restore (required)
--identity <file> The break-glass identity file (required, unless you supply the custody artefacts below)
--signer <file> The operator signer public-key file (required)
--sink <kind> The restore target: file, env or discard (defaults to file)
--out <dir> The directory to restore record values into (required with --sink file)
--apply Write to the target; without it the restore is a dry run that plans only
--allow-stale Proceed despite this run’s age and nothing else: the RUNLOG verified against --signer, is internally consistent, and says either that this run is not the latest for its downpipe or that its maximum index is below your --min-runlog-index pin. It does not waive the RUNLOG’s signature, presence or self-consistency, so it covers only part of exit 5
--allow-unverified-runlog Proceed despite the RUNLOG itself being untrustworthy: absent, unreadable, unparseable, empty, not carrying this run, failing to verify against --signer, disagreeing with the signed root manifest, or internally contradictory. This is the acknowledgement for the rest of exit 5. Nothing is then established about this run’s recency, and a bucket someone has rolled back or replaced looks exactly like this
--allow-unverified Proceed despite a missing or invalid signature, recording the true outcome. Implies both acknowledgements above
--min-runlog-index <n> Reject a RUNLOG whose maximum index is below this anti-rollback pin
--acknowledge-no-rollback-pin Silence the stderr reminder printed when --min-runlog-index is left unset. Rollback protection stays off either way; this only suppresses the reminder, for a genuine first recovery before a recovery sheet entry exists. The receipt still records the pin as unset
--check-bundle Verify the in-bucket recovery bundle against its signed SHA384SUMS
--receipt <path> Write a signed restore receipt to this path (- writes to stderr)
--receipt-signer <file> Sign the receipt with this signer private-key file
--fetch-concurrency <n> Parallel shard-prefetch window (defaults to 1, strictly sequential and bounded memory)
--max-memory <size> Caps the bytes held in the prefetch buffer, for example 512MiB or 1GiB (defaults to 0, which honours GOMEMLIMIT instead)

There are three sinks and no stdout sink. The file sink writes one file per record under --out at owner-only permissions. The env sink writes dotenv KEY='value' lines to stdout, treats the names already in the process environment as occupied so it never redefines a set variable, and rejects a name that is not a valid POSIX environment-variable name. The discard sink is a restorability check, not a write: it runs every record through the full restore path so each value is decrypted and hash-verified, then writes nothing, printing an attestation (records, bytes, failures) and a one-way restore digest over the verified plaintext with no plaintext in any output. Because the discard sink decrypts to verify, it always applies regardless of --apply.

The plan and the result print to stderr

For the file and env sinks, the plan and the apply result are written to stderr so stdout stays clean for the env sink’s data. The plan reports counts, names, keys, sizes and conflict reasons only, never a value.

selftest

selftest builds a sample archive with the post-quantum hybrid envelope and recovers it from disk using only an offline key, end to end, with nothing else in the loop. It is the fastest way to confirm a freshly built binary works before you point it at a real bucket.

Flag Purpose
--keep <dir> Write the sample archive and keys to this directory and keep them, instead of using and discarding a temporary directory

Split custody: using shares without writing a key

If your organisation holds the break-glass key as M-of-N shares rather than as one file, every command that takes --identity also takes the custody artefacts directly:

downpipe prune --archive ./archive --signer ./keys/signer.pub --keep 30 \
  --share ./custody/share-1.txt --share ./custody/share-3.txt --share ./custody/share-4.txt \
  --envelope ./custody/wrapped-identity.txt
Flag Purpose
--share <file> A share file, repeatable. Either the labelled download or a file holding the bare emailed share body
--envelope <file> The wrapped-identity file the console offered for download. Required with --share or --wrapping-key
--wrapping-key <file> The wrapping-key file, used instead of --share
--threshold <n> The ceremony’s M. Needed only when every share you supply is the bare emailed form, which carries no threshold

The shares are combined in memory, used for that one command, and wiped. No complete copy of the key is written anywhere, so the M-of-N control covers the whole operation rather than ending the moment someone reassembles the key.

That distinction is the point. recombine still exists and still writes identity.key, which is the right answer when you deliberately want a key file, but reaching for it in order to run a command means the complete break-glass key sits on one operator’s disk until they remember to destroy it, and on a copy-on-write or journalled filesystem the bytes outlive the deletion. Supplying the artefacts to the command directly avoids ever creating that file.

Supplying both --identity and the custody artefacts is refused rather than resolved by precedence. If you pass both you have a belief about which key is being used, and a silent rule would be right only half the time.

Below the threshold the command fails before it reads the archive, so a quorum that has not been met cannot be worked around by trying a command that needs fewer bytes.

The commands that take an identity, and therefore take these flags, are inspect, verify, restore, prune and unseal-export. The last one is worth naming: it opens a sealed control-plane export offline, without a console at all. The console’s own estate-import form does the identical unseal in your browser when a console is available, covered on recovering downpipes itself, so this command is for the console-unavailable case, and it is the one where materialising a key file first would be hardest to undo.

Recover a bucket end to end

This is the canonical offline-recovery procedure. It is the path the format exists to keep open: a holder of the destination bucket bytes and the customer’s own offline break-glass key recovers the data with neither Cloudflare nor the vendor available. You need the whole bucket tree (or a local copy of it), the offline break-glass identity, and the operator signer public key, both written to your recovery sheet at the key ceremony before any backup ran.

  1. Build a reader you trust

    Clone the repository and build from the vendored source, as in install the tool truthfully. Keep a copy of the repository alongside your recovery key so a reader is always to hand, or re-implement a clean-room reader from docs/format/SPEC.md and the conformance vectors. Confirm the binary runs with ./downpipe selftest.

  2. Find the run id

    A runId is the 26-character identifier of one backup run. It is not on your recovery sheet: the sheet carries fingerprints, a posture and a blank anti-rollback line, and its own command templates print <runId> as a placeholder for you to fill in. Find one with downpipe keys --which --archive ./bucket, which is keyless, needs no identity, and lists the runs an archive holds grouped by the key that opens them. Listing the bucket’s run/ prefix works too when you hold the tree locally. downpipe inspect is the next step rather than the first one: it needs no identity and touches no key, but --run is required, so it reads a run you have already named, and it shows a run rather than vouching for it. You normally recover the newest good run.

  3. Read your anti-rollback pin off the recovery sheet

    Before you verify or restore anything, take the latest trusted RUNLOG index from the anti-rollback line on your printed recovery-sheet.txt (see the key ceremony and recovery kit). Pass it as --min-runlog-index <n> on every verify and restore command below. Without it, a bucket that has been rolled back to an older, validly-signed run, whether by an attacker or by an operator error, restores cleanly with no warning: the pin is what tells the reader that a run older than the one you last trusted must be refused rather than served. This step is not optional; skipping it is the one way to follow this walkthrough exactly and still restore a rolled-back archive.

  4. Verify the run before you write anything

    Prove the run is whole and genuine first. It restores nothing, so it is safe to run as often as you like.

    downpipe verify --archive ./bucket --run <runId> \
        --identity ./keys/identity.key --signer ./keys/signer.pub \
        --min-runlog-index <n>

    A zero exit means verified and complete. Any non-zero exit is a specific verdict (a signature problem, incomplete coverage, a plaintext mismatch, a freshness problem, or a usage error); read exit codes and verification outcomes for what each one means and what to do. Exit 5 with a rollback reason means --min-runlog-index just did its job: stop and investigate before you go anywhere near --allow-stale.

  5. Restore to a file sink (the usual case)

    Restore is a dry run by default, so run it once to read the plan, then again with --apply to write. Prefer a fresh, empty --out directory so there is nothing to clobber.

    downpipe restore --archive ./bucket --run <runId> \
        --identity ./keys/identity.key --signer ./keys/signer.pub \
        --min-runlog-index <n> \
        --out ./restored
    
    downpipe restore --archive ./bucket --run <runId> \
        --identity ./keys/identity.key --signer ./keys/signer.pub \
        --min-runlog-index <n> \
        --out ./restored --apply

    The apply reassembles each record, verifies its plaintext hash as it writes, and refuses to overwrite an existing file. A per-record failure is reported and the apply continues, so a partial restore is never dressed up as a success.

  6. Or restore straight from the S3-compatible bucket

    To read the destination directly instead of a local copy, supply the endpoint and bucket and put the credentials in the environment. The reader issues GETs only and refuses redirects.

    export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...
    downpipe restore --s3-endpoint https://<account>.r2.cloudflarestorage.com \
        --s3-bucket <bucket> --run <runId> \
        --identity ./keys/identity.key --signer ./keys/signer.pub \
        --min-runlog-index <n> \
        --out ./restored --apply
  7. Keep a signed receipt

    Add --receipt ./receipt.json to write a machine-readable receipt of the outcome, and --receipt-signer ./keys/signer.key to sign it if you hold the signer private. The receipt records the true outcome and flags whether the written values were hash-verified, so a drill leaves an auditable trail. A scheduled break-glass drill, ideally with the discard sink on an isolated machine, is the only test that proves rather than assumes the bucket and the offline key are enough on their own. See prove recoverability.

The construction is post-quantum hybrid, so an archive stays safe if either the classical half or the post-quantum half is later broken. The envelope is interoperable with nothing off the shelf, which is why recovery is delivered by this documented reader of the downpipe format rather than a third-party tool.

Provisioner commands

The provisioner path is what an operator runs from their own machine to stand up or upgrade the engine in a Cloudflare account. Every command in this path that talks to Cloudflare reads its token from CLOUDFLARE_API_TOKEN, keeps it on the machine, and sends it only to api.cloudflare.com; CLOUDFLARE_ACCOUNT_ID is read only by setup, preflight instead takes the account as the --account <id> flag, and init and update are plan printers that read no environment variable and make no Cloudflare call at all. None of these commands is on the recovery path above.

preflight

preflight --account <id> runs the read-only, deploy-time account checks an onboarding needs before the engine is deployed. It lists the account’s zones, verifies a chosen --domain sits on an active zone, reports Secrets Store headroom against the documented ceiling, looks for a Workers Paid subscription, reports whether a Logpush job already ships Workers logs to a SIEM, and lists R2 buckets as destination candidates. It is GET-only and provisions nothing. Add --json for machine-readable output. A check the token cannot read degrades to “unknown” rather than failing, but an invalid token aborts loudly; the command exits with the preflight code (deliberately outside the normative reader set) when a check failed or a requested --domain could not be verified.

setup

setup is the local Cloudflare provisioner, and it is not read-only. It is a dry run by default: without --apply it validates the inputs, builds the ordered plan, prints exactly what it would create, and makes no network call at all. Under --apply it takes the live path, the only path that contacts Cloudflare, and it WRITES.

setup --apply writes to your account

Older README and changelog prose describes setup as making read-only or GET-only calls. That is an erratum. Under --apply, setup creates a Cloudflare Access self-hosted application, attaches a One-Time PIN allow policy scoped to your email allowlist, PUTs Secrets Store entries, and PUTs the engine’s source and destination Worker bindings. Only the dry run (the default) is genuinely no-write.

Flag Purpose
--apply Execute the plan against Cloudflare; without it setup prints the plan and creates nothing
--config <file> A JSON file describing the deployment (Access, email, secrets, bindings); authoritative when present
--app-name <name> The Access application name when not using --config (defaults to downpipes)
--access-domains <list> Comma-separated console and engine custom hostnames to gate with Access
--allow-emails <list> Comma-separated allowed email addresses for the One-Time PIN policy
--allow-email-domains <list> Comma-separated allowed email domains for the One-Time PIN policy
--email-from <addr> Outbound sender address on a custom domain (records the manual sending-domain step)
--engine-script <name> The Worker script the bindings attach to (defaults to downpipe-engine)

Secret values are never passed as flags. Each declared secret’s value is read from the environment under DOWNPIPE_SECRET_<NAME> (the name uppercased, non-alphanumeric mapped to underscore) on the apply path only, so a value never appears in the plan or a log. An allowlist is mandatory: setup refuses to build an unrestricted Access allow rule, which is the documented footgun. Hostnames must be real custom domains; a workers.dev address is rejected. The email sending-domain onboarding is the one step the API cannot do, so it is surfaced as a note rather than an API call.

init

init prints the full least-privilege deploy runbook in order and then stops. It is a plan printer, not a live provisioner: it makes no Cloudflare call and creates nothing, and you run the printed steps yourself. The runbook walks the three privilege states from engine/docs/CLOUDFLARE-PERMISSIONS.md: create a scoped, short-lived deploy token (the exact Cloudflare permissions are printed), provision the resources, deploy the engine and console, run the key ceremony in the console, wire the first source, verify readiness, dispose of the one-time bootstrap token, and finally delete the scoped deploy token so the running engine holds no Cloudflare API token and reaches data only through bindings. Flags let you set the engine domain, console domain, zone and archive bucket names that appear in the printed plan; a workers.dev domain is rejected.

init --execute is not implemented

The --execute flag is a deliberate refusal, not a live path. init is print-only by construction, with no branch that contacts Cloudflare, so --execute refuses with a usage error and points you at the printed steps.

update

update prints the ordered upgrade runbook for an already-deployed stack and then stops, the same plan-printer contract as init. It walks the privilege-state-3 upgrade: confirm the available version is signature-verified (pulled and verified by the engine against the pinned release-signer key, reviewed in the console; the vendor cannot push a deploy), re-create the same scoped short-lived deploy token, deploy the reviewed new version of the engine and console, verify readiness, and delete the scoped token again so no standing deploy credential is left behind. Flags set the version label and the engine and console domains that appear in the plan. Like init, its --execute flag is unimplemented and refuses, so the command can never make a live call.

Where this fits

For the meaning of every exit code and what the salvage overrides change, read exit codes and verification outcomes. For the conceptual recovery postures behind the break-glass key, read recovery postures and the key ceremony and recovery kit. For the in-console restore journey that the engine drives (as opposed to this offline binary), read the restore flow and what restore can and cannot write back. For the provisioner path in operational context, read deploy and Cloudflare token scopes.

Last updated .