Skip to content
downpipes docs

Updating your deployment safely from the console

This is the primary guide to updating a self-hosted downpipes deployment to a newer, vendor-signed release, from the console, without touching a terminal. It is written for the self-hoster who runs the engine in their own Cloudflare account. A release can carry more than the engine: the one signed channel document can describe each component of a release (the engine, and the console as a bundle of its shell worker and static assets), and a single apply updates them in a fixed safe order. The whole design exists so that one click cannot brick your platform: every component’s bytes are verified against the signed channel before anything goes live, the engine promotion is proved healthy by a real canary flight, a version whose canary comes back dead is rolled back automatically, and a console that fails to land is rolled back on its own while the engine stays healthy.

Two facts frame everything below. The vendor serves a signed channel at the singular update.downpipes.io. On the shipped base config the signer is pinned and the URL is set; the live confirmation is GET /admin/updates reporting configured: true and verified: true, which is when the console tile reads “Signer verified”. The generated wrangler.deploy.toml used at deploy time carries the committed signer and hostname forward unchanged (it only appends the live source bindings so a deploy cannot drop them), so it reads configured: true the same as the committed file.

The second fact is what the deploy step guarantees: an apply reads the target script’s live settings before it uploads anything, carries every binding and secret forward, and refuses the upload outright rather than promote a version it cannot prove will keep them (classifyBindings and the unknown-type guard in engine/src/admin/cf-deploy.ts). What the drivers do and what they refuse is set out in full below. This page covers applying an update through the controlled update channel; activation is covered separately on activating updates, and the trust and recovery model in depth on update trust and rollback.

Why a click cannot brick the platform

An update only ever replaces the moving parts: the engine Worker, and, when the release carries a console component, the console Worker. The two things that actually matter are independent of the running Workers and cannot be harmed by a bad deploy.

Your data is the append-only archive objects in your own bucket, and a deploy does not change the archives you already have. Your recovery is the self-describing, signed archive plus the standalone reader and your recovery kit, which restore without the engine running at all. So the worst a bad release can do is a brief availability blip on the live engine or console Worker. Cloudflare lets you roll it back. The apply’s job is to not leave you on a broken live version.

That is also why the brick-safety properties are structural rather than incidental. The apply logic is pure orchestration over injected interfaces (a deploy driver per component and a health gate), so the ordering, the guards and the refusals all sit in that orchestration rather than in the code that talks to Cloudflare. The only parts that talk to Cloudflare are the deploy drivers, and what they guarantee is set out below.

Before you can apply: the channel must be on

The apply control needs a channel that both reports configured: true and actually verifies. On the shipped base config the signer is pinned and the channel URL is set, and the vendor serves a signed channel at update.downpipes.io; the live confirmation is GET /admin/updates reporting configured: true and verified: true, and that is when the console tile reads “Signer verified”. The generated wrangler.deploy.toml used at deploy time is the committed wrangler.toml with the live source bindings appended, so it keeps the same signer and hostname and reads configured: true too. While a channel is unconfigured or unverified (for example, when the pinned signer does not match the served channel) the read-only update facts still render, but the apply control does not, because nothing applies until the channel is both configured and verified.

When the channel is on and verified, the engine compares its own running version to the channel’s recommended version and the console surfaces what is available. The status the console reads carries the recommended version, an updateAvailable flag, and a compatible verdict. The compatible verdict lets the console disable the apply when a release declares a minimum engine version newer than yours (applying it would be refused server-side anyway).

When the verified channel recommends a newer engine or a newer console than what you are running, an “Update available” chip appears in the console’s top bar on every screen and takes you to the update section of the Licence and updates screen. The engine computes updateAvailable for the engine row only, because it cannot know which console build your browser is running; the console stamps its own version at build time and compares itself against the channel’s console component in the browser.

What the operator does

The flow is review, preview, then apply. Reviewing the release is a read: any authenticated role can see it, and the read is not rate-limited. Preview, Update now and the Settle that follows are owner-only and rate-limited, and the deploy token you paste is used once and never stored.

  1. Review the component rows and What's in this update

    The Licence and updates card is component-aware. A release shows one row per component: the engine row (for example “Engine 0.3.5 to 0.3.6”) and, when the release carries one, a console row (“Console 0.2.6 to 0.2.7”, or “up to date” when only the engine moves). Alongside the rows sits a “What’s in this update” panel built from the signed release metadata: the structured changelog, the impact lines, any required steps, and the release’s risk class. Because the whole channel is signed, this metadata is tamper-evident with no extra trust surface. A required step marked blocking is shown with a distinct “Required” badge, and the console gates on it: Update now stays disabled until you tick an acknowledgement for each blocking step, so you cannot sleepwalk past an action the release needs. That gate is the console’s own, and the engine deploys only what it verifies regardless. A non-blocking step is shown for information and does not gate. A release tagged migration or breaking is shown with extra care, and an unlabelled release is treated as needing care rather than as routine.

  2. Run a Preview (a dry-run)

    Preview runs the same verify-and-plan path the real apply runs, but it is a dry-run by default: it downloads and verifies each component’s artefact against the signed channel and stops. Nothing is uploaded, nothing is promoted, and a preview needs no token. Because it carries no token, a preview usually cannot read the current live version to record what the rollback target would be; that happens on Update now, which does carry a token. The plan renders per component: the engine row shows the verified artefact, and the console row shows the verified bundle and how many assets it would upload, which is the whole bundle, not only the ones that changed. A preview that returns a clean plan tells you the bytes match the signed channel and the apply would proceed; a preview that returns a refusal tells you exactly why before you have changed anything.

  3. Update now with a one-shot deploy token

    Going live is an explicit opt-in. When you choose Update now, the console asks you to paste a one-shot Cloudflare deploy token. Start from the “Edit Cloudflare Workers” template (Workers Scripts, Workers KV Storage and Workers R2 Storage edit, plus Workers Routes edit and Account Settings read); the one token covers the whole apply, every component included. It is the narrow scope the apply needs to read a script’s settings, upload a version and deploy it; it is not your full account deploy credential. It is used for this apply and is never stored or logged. The settle step that proves health runs automatically right after, over the console’s service binding, so you paste the token, the new engine version is promoted, and the canary verdict follows without further action. When the release carries a console component, the console applies only after the engine’s verdict is a keep, in the same apply, on the same token.

    If your engine binds a Secrets Store secret as a backup source, add Secrets Store edit to the token as well. The “Edit Cloudflare Workers” template does not include Secrets Store, so a token built from the template alone is refused at the upload-version step on an engine with a Secrets Store binding, even though the secret’s value is not changing and nothing is uploaded before the refusal. A KV, R2 or D1 binding does not need this: the template already covers KV and R2, and re-uploading an existing D1 binding needs no extra permission.

    If the release is tagged migration or breaking and dual control is switched on, the first Update now press does not deploy: it queues the apply for a second owner’s approval and returns without asking for the token, nothing is changed. Once another owner approves, come back and press Update now again with your one-shot deploy token to complete it. The engine queues a routine release this way only when the account’s update-approval policy requires a second owner for every update. Dual control off skips the detour entirely.

The token is supplied to the apply and to the settle that follows, and to a rollback if one is needed, and it is never persisted on any of those paths. If you close the page between promote and settle, the verification is left pending and the standalone rollback control stays offered so you can finish or revert deliberately. Once a settle decides, the outcome is recorded before any rollback deploy is issued, so the console can always re-read what actually happened from GET /admin/update/status rather than guessing from a lost response.

The apply lifecycle, in order

Ordering is the safety. The engine runs these steps in this sequence, and nothing goes live until everything before it has passed. Phase one runs on the current (old) engine version, because the request that triggers the update is served by the live code, which is the old one until the promote lands. Phase two (settle) runs on the now-live new version so the canary it flies exercises the new code.

StepWhat happensIf it fails
Already-current short-circuitIf the engine is already on the recommended version, nothing to doReturns no-update
Anti-rollback floorRefuse a target below the highest version this engine has ever successfully settled, unconditionally, even with downgrade allowedRefused, nothing changed
Forward-only guardRefuse a channel recommending an older or uncomparable version unless downgrade is explicitly allowedRefused, nothing changed
Migration guardRefuse a release that needs a Durable Object migration (auto-apply cannot prove a migration safe after the fact)Refused, nothing changed
Compat guardRefuse a release whose minimum engine version is newer than the running engine, or unparseableRefused, nothing changed
Verify artefactRecompute the downloaded bundle’s SHA-384 and require an exact match against the signed channelRefused, nothing changed
Record rollback targetRead and store the current live version before any changeRefused, nothing changed
Canary baselineRead the canary’s pre-apply liveness from the Durable Object (no extra flight)Baseline treated as pending
Dry-run stopOn a dry-run (the default) the plan ends here, verified but not deployedReturns dry-run
Upload versionCreate a not-yet-live version that preserves every binding and secretRefused, engine unchanged
Read-back gateRead the uploaded version’s bytes back from Cloudflare and compare them to the signed channel’s hash, deliberately here rather than after the promote, because nothing is live yet so a mismatch is free to refuseIn warn mode (the default) the verdict is recorded and the apply proceeds; in enforce mode nothing is promoted and the prior version keeps serving
PromoteMake the uploaded version the live deployment (atomic)Refused, prior version still live
Canary verdict (settle)Fly a real canary on the now-live new codeDrives keep-or-rollback
Keep or auto-rollbackKeep on a singing canary; roll back to the recorded version on a dead one (an undecided verdict is retried first, see below)Reverts automatically where the rollback deploy succeeds, with the outcome persisted first. Where it does not, the outcome is rollback-failed and the engine is still on the new version, which is the one case below that needs you

The canary that gates the keep decision is a real flight: it exercises the new build’s whole write, seal, read, restore and verify path against your configured destination, using a throwaway verification cell so it never disturbs the scheduled canary’s state. If the canary sings on the new version, the update is kept. Only a dead canary, a bit that strayed on the new code, rolls the engine back immediately. A verdict that is neither alive nor dead is retried on a short, bounded schedule before anything is decided (two further flights, then one self-check retry, around three seconds of waiting in total), because the settle runs in the window right after the code swap has reset the engine’s Durable Objects, where a transient failure is expected rather than alarming. A verdict that is still incomplete after retries keeps the verified new version instead of a rollback, and the record awaits the scheduled canary’s confirmation. The self-check that runs alongside those retries confirms the new code boots, answers and reports the expected version, and that a fresh preflight round-trips the scheduler Durable Object; its result shapes the reason and the confidence of the keep, and it does not itself decide a rollback, because a health check sitting inside the code swap’s own blast radius must not.

A live apply with no destination configured is refused outright rather than settled without a canary: the engine answers “updates verify themselves with a canary flight to your destination; add a destination first, then apply this update” and nothing is uploaded.

A rollback can itself fail, and it says so

An automatic rollback is a deploy, and a deploy can fail. The settle therefore does not report a binary applied-or-reverted. It reports which of these actually happened, and two of them are not the happy path.

Settled outcomeWhat it meansWhat you do
appliedThe canary sang on the new version and it was keptNothing
rolled-backThe canary did not sing, and the rollback deploy succeeded. You are back on the prior versionNothing, though the release wants investigating before you retry it
rollback-failedThe canary did not sing, the automatic rollback was attempted, and the rollback deploy itself failed. Your engine is still running the version the canary declared deadAct. On the Licence and updates screen, the “Roll back the engine” card offers a one-click rollback to the recorded prior version, with a one-shot deploy token. You can also re-deploy that version from the Cloudflare dashboard. The engine states the exact version in its reason string
applied-unconfirmedCloudflare accepted the promote but the confirmation read could not be performed, so the engine cannot prove the intended version is the live oneConfirm the live version yourself before trusting it. It is reported apart from applied because the engine has no proof that the intended version is live

rollback-failed is the one outcome on this page that leaves you worse off than before the apply, and it is reported as its own state so it cannot hide inside rolled-back. If the badge, the pack and the console all said “reverted” while an engine was live on a dead build, an operator would have no reason to look. Whatever the outcome, your data and your recovery are unaffected either way: no update rewrites an archive, and restore is out-of-band through the standalone reader and your recovery kit, so backups stay safe and restorable while you sort the running version out.

The same distinction is kept for a gradual ramp, where the equivalent state is reported as rollback-failed-still-split: the ramped version did not pass and the rollback to the prior version at one hundred percent failed, so traffic is still split across both. The console handles that state explicitly rather than folding it into a generic failure.

However the verdict lands, the settled outcome is written down before it is acted on, with the reason, persisted before any rollback deploy is issued and reported by GET /admin/update/status. That ordering matters because a rollback deploy swaps the running code out from under the very request that reports the settle, so the response you are waiting on can lose that race. The console therefore re-reads the recorded outcome after every settle instead of asserting one, and what it shows you is the recorded outcome. The settle decides, the decision is persisted first, and the console shows the recorded outcome.

When a release carries the console too

One signed release describes each component it moves: the engine as a worker module, and the console as a static-assets bundle, a single self-describing artefact carrying the console’s shell worker, every static asset it serves with a per-asset digest, and its static config. One signature covers everything, and each component’s bytes are verified against the channel-signed hash before anything is uploaded. The channel also carries an engine-only artefact list, which the engine reads when the component map has no engine entry. A console artefact is never placed in that list, so the engine can never mistake console bytes for engine bytes.

The ordering is fixed, and the ordering is the safety: the engine applies first and must settle to a keep verdict before the console component applies. A new console may call new engine API, and engine API changes are additive, so engine-new with console-old is the direction that is always safe; the reverse order could not make that guarantee. One canary flight slower, safer by construction.

Before the engine touches the console script at all, it demands proof that the script is its own console: the target script’s live bindings must include the ENGINE service binding pointing back at this engine. CONSOLE_WORKER_NAME names the console script (default downpipe-console), and the binding proof is what makes that name safe, because a mistyped or hostile value can never redeploy an unrelated Worker in your account. The apply preserves the console script’s live bindings the same way the engine deploy preserves its own.

OutcomeWhat happensWhat you see
Engine refused or rolled backThe console apply is aborted before it starts“The release was not applied; the engine rolled back and the console is unchanged”
Engine settled, console upload or promote failedThe console auto-rolls back to its recorded target; the engine keeps the new versionThe partial state (engine on the new version, console rolled back), with the console component retriable alone
Both landedEngine and console are on the new releasePer-component confirmation, then the automatic reload below

Engine-new with console-old is the compatibility direction the product guarantees, so the partial state in the middle row is safe to sit in: retry the console component when you are ready, alone, without re-applying the engine.

After the console component promotes, the operator’s browser is the real canary. The console build stamps its own version, and the page you are on fetches its own origin’s build stamp with caching bypassed and requires the new version to be serving before it declares success; on a mismatch or a failure it offers a one-click console rollback, driven by the engine to the recorded target with the same one-shot token. On success the console reloads itself after a brief pause, because the app you are looking at is still the old bundle until it does; the pause is there so you see the success first. The engine deliberately does not probe the console over HTTP as its gate: a customer console sits behind Cloudflare Access, so the engine would be measuring the Access challenge, not the console; the engine-side gate is the deployments API confirming the new console version live at one hundred percent.

Two scoping rules complete the picture. The gradual ramp stays engine-only, because a static-assets swap is atomic at promote and has no traffic-percentage concept, so a ramp naming the console is refused. And dual control gates the live apply as a whole, one approval per apply regardless of how many components it carries. Risk classes drive tone and gating exactly as for an engine-only release. An advanced disclosure on the card lets you apply or roll back a single component alone if needed; Update now applies everything the release recommends.

The refusal cases: when nothing is deployed

A refusal is a feature. In each of these cases the engine aborts before going live and your engine is left exactly as it was.

RefusalWhat triggers it
Below the settled floorThe recommended version is below the highest version this engine has ever successfully settled, or cannot be compared to it; this refuses even with downgrade allowed, and the standalone Roll back control is the way back to a recorded known-good version
Migration requiredThe signed release is flagged as needing a Durable Object migration; apply it through the operator runbook instead
Engine too oldThe release declares a minimum engine version newer than the one running, so an intervening version must be applied first
Hash mismatchThe downloaded artefact’s SHA-384 does not match the signed channel’s declared hash (corruption or tampering)
Backward or uncomparableThe channel recommends an older or uncomparable version and you have not explicitly opted into a downgrade-to-recover
No verifiable artefactThe signed channel lists no artefact for the recommended version, or it lacks the URL and hash needed to download and verify a bundle
Console not provably ownedThe release carries a console component but the target console script’s live bindings do not include the ENGINE service binding pointing back at this engine, so the engine refuses to deploy to it
Read-back not byte-exactThe version Cloudflare holds after the upload does not read back byte-exactly against the signed channel. This is a refusal only in enforce mode; the shipped default is warn, which records the verdict and promotes anyway, so by default this surfaces as evidence on the record rather than as a refusal

Backward motion is meant to be system-controlled through the rollback control, never driven silently by a correctly-signed channel that happens to pin an older build. That is why the forward-only guard refuses by default and a deliberate downgrade is a separate, explicit opt-in. The guard is kept per component: the engine and the console each remember the newest version they have applied, and a channel recommending anything older is refused for that component.

What the deploy step guarantees

The deploy drivers are the parts that actually call Cloudflare: the engine driver (engine/src/admin/cf-deploy.ts) speaks the versions-and-deployments API, and the console driver (engine/src/admin/cf-assets-deploy.ts) speaks the static-assets upload API. Both read the target script’s current settings before they upload anything, re-send each non-secret binding verbatim, list the secret binding types so Cloudflare keeps the secrets and their values, and carry over the runtime compatibility fields. Cloudflare replaces the binding set on every version rather than merging it, so re-sending the live set is what stops a new version coming up without its archive, its Durable Objects or its signing secret.

Any binding type the pipeline cannot prove it can carry forward is a refusal before the upload, not a risk taken: the driver names the offending type, tells you to use the wrangler deploy path for that update, and nothing is uploaded. A clean refusal is always safer than a version that drops a binding and bricks a Worker.

Two platform properties bound what a failure can cost, and both are why the ordering above is arranged as it is. An upload creates a version that is not live, so an upload that is refused or fails changes nothing that serves. And Cloudflare deploys are atomic, so a promote that fails leaves the previously-live version serving rather than half-applying. Every opaque Cloudflare failure is turned into a message that names what was changed and what was not, so you never have to infer the state from an HTTP status. Where the engine refuses a release outright, the operator deploy path (covered on upgrades and rollback) is how you apply it instead.

Auto-rollback only happens inside the apply or the settle request. The engine does not self-heal a bad deploy out of band: the scheduled canary can detect an unhealthy live version and raise a critical alert, but the engine holds no standing deploy credential, so it cannot revert itself. If a settle is interrupted before it decides, the pending verification and the one-click rollback control are what bring you back; once it has decided, read the recorded outcome on GET /admin/update/status, even if the response that carried it was lost. The engine reports a Cloudflare version id (cfVersionId) on its status, but no console screen surfaces it, so use the version names the apply and rollback flows show you rather than expecting a Cloudflare version id in the console.

Where this fits

Turn the channel on first with activating updates. Understand the brick-safety and trust model, the standalone rollback, the opt-in gradual ramp and the governance rules on update trust and rollback. For the operator-CLI upgrade path and the migration and schema-epoch rules that bound a rollback, see upgrades and rollback. For how the vendor signs a release in the first place, see publishing a signed release.

Last updated .