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 followed by a settle that flies a canary flight and runs a self-check, the hourly canary confirms the kept version, the Roll back control returns the engine to the prior version, 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.
The update line
While the “Update available” chip shows, one line also runs across the top of every screen, under the top bar. It names what is available (the engine, the console, or both, each with its recommended version) and has a button that opens the Updates section of Licence and updates. The line does not show while the channel is unconfigured, unverified or up to date.
The tone of the line follows the engine’s support status for the engine version you run:
When the engine is supported, or only the console has an update, the tone is neutral. For an engine update, the line reads “A new release is available: engine 0.3.10.” and then tells you to review and apply it in Licence and updates.
When support for the engine ends in 30 days or fewer, the tone is a warning. The line gives the end date and the version to update to. The date shows as day, full month name and year, for example “25 October 2026”. The Licence and updates screen shows the same date the same way.
When support for the engine has ended, the tone is danger. The line says so and names the version to update to.
The support status describes the engine. A console-only update therefore keeps the neutral tone.
The dismiss button hides the line until the browser session ends. The line shows again when the channel recommends a different version. The line returns too when the support status of your engine gets worse, for example from ending to ended. When the browser blocks session storage, the button hides the line until you reload the page. A dismiss moves focus to the dismiss button of the other line, or to the main content.
When no notification channel is switched on, the strip shows a destination line. It shows on its own when there is no update, and below the update line when there is one. With no channel at all, it reads “No alert destination is set, so alerts reach nobody. Set one in Notifications.”
When channels exist and every one is off, the destination line reads “No alert destination is enabled, so alerts reach nobody. Turn one on in Notifications.” The line links to the Notifications screen and has its own dismiss button. The line stays hidden for the browser session after you dismiss it, and shows again in a new session while no destination is on. The line clears when you save or switch on a channel in Notifications.
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.
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 -> 0.3.7”) and, when the release carries one, a console row (“Console 0.2.6 -> 0.2.8”, or “Console 0.2.6 is 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.
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.
The preview judges the console component against the engine that the same apply installs, not the engine you run now. When the console’s minimum engine version is that new engine version, the console row says that the console component applies after the engine reaches it. A minimum above that version is refused, and the reason names the engine version that the update installs.
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 runs automatically right after, over the console’s service binding, so you paste the token, the new engine version is promoted, and the settle 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 live new version. It flies a canary flight from the new code and runs a self-check on it.
For some seconds after the promote, Cloudflare can still send a request to the previous version. That code cannot prove anything about the new build. When another version answers the settle within 15 minutes of the promote, the engine answers inconclusive with the reason old-version-answering. It records no settle outcome and deploys nothing, and the verification stays pending.
The console then shows “The new version is not answering yet. The console checks every 20 seconds and verifies the update when it answers.” It reads GET /admin/update/status every 20 seconds and settles again when its runningVersion field names the promoted version. It stops after a bounded number of checks. From 15 minutes after the promote, the settle decides on whichever version answers.
| Step | What happens | If it fails |
|---|---|---|
| Already-current short-circuit | If the engine is already on the recommended version, nothing to do | Returns no-update |
| Anti-rollback floor | Refuse a target below the highest version this engine has ever successfully settled, unconditionally, even with downgrade allowed | Refused, nothing changed |
| Forward-only guard | Refuse a channel recommending an older or uncomparable version unless downgrade is explicitly allowed | Refused, nothing changed |
| Migration guard | Refuse a release that needs a Durable Object migration (auto-apply cannot prove a migration safe after the fact) | Refused, nothing changed |
| Compat guard | Refuse a release whose minimum engine version is newer than the running engine, or unparseable | Refused, nothing changed |
| Verify artefact | Recompute the downloaded bundle’s SHA-384 and require an exact match against the signed channel | Refused, nothing changed |
| Record rollback target | Read and store the current live version before any change | Refused, nothing changed |
| Canary baseline | Read the canary’s pre-apply liveness from the Durable Object (no extra flight) | Baseline treated as pending. The settled record (canaryBaselineReadFailed) and the settle’s audit entry both say that the read failed |
| Dry-run stop | On a dry-run (the default) the plan ends here, verified but not deployed | Returns dry-run |
| Upload version | Create a not-yet-live version that preserves every binding and secret, and, for the engine, record the SHA-384 of the uploaded bytes as the plain-text binding DEPLOYED_ARTEFACT_SHA384, which GET /admin/status reports as artefactSha384 unless you set ARTEFACT_SHA384 yourself | Refused, engine unchanged |
| Read-back gate | Read the uploaded version’s bytes back from Cloudflare, deliberately here rather than after the promote, because nothing is live yet so a mismatch is free to refuse. The engine hashes the module bytes that Cloudflare returns and compares the hash with the signed digest. The verdict is “verified” when they match and “mismatch” when they differ. It is “unavailable” when the response cannot be read or its shape is not recognised | In warn mode (the default) the verdict is recorded and the apply proceeds; in enforce mode, any verdict but “verified” means nothing is promoted and the prior version keeps serving |
| Promote | Make the uploaded version the live deployment (atomic) | Refused, prior version still live |
| Canary verdict (settle) | Fly a canary flight from the live new code and run the self-check | Records the verdict. A dead verdict rolls the engine back to the prior version. Any other verdict keeps the verified version |
| Keep | Keep on a singing canary. Keep, with confirmationPending, on an ailing or pending verdict. A dead verdict rolls back instead. An ailing or pending verdict is retried first, see below | The hourly canary clears confirmationPending when it sings on the same version. Use the “Roll back” card on the Licence and updates screen once you apply an update from the console |
The settle flies a canary flight against your configured destination from the new code. The flight writes a probe object to a throwaway verification cell and reads it back, so it never disturbs the scheduled canary’s state. A dead verdict means a byte strayed. It rolls the engine back to the prior version at once, and the engine does not retry the flight. Any other verdict keeps the verified new version. The hourly canary confirms a kept version, and you can roll the engine back at any time from the console.
The engine retries an ailing or pending verdict on a short, bounded schedule. That is two further flights, then one self-check retry, with about three seconds of waiting in total. A verdict that is still ailing or pending after the retries keeps the verified new version, and the record awaits the scheduled canary’s confirmation.
The self-check that runs alongside those retries confirms that the new code boots, answers and reports the expected version. The self-check round-trips a fresh preflight through the scheduler Durable Object. Its result shapes the reason and the confidence of the keep. It does not decide a rollback.
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.
The settled outcomes
The settle does not report a binary applied-or-reverted. It reports which of these outcomes the engine recorded.
| Settled outcome | What it means | What you do |
|---|---|---|
applied | Either the canary flight sang and the update was kept, or the flight did not sing and the engine kept the verified version. The second case carries confirmationPending until an hourly canary flight sings on the same version (decideKeep, engine/src/admin/update-types.ts; settleAfterPromote, engine/src/admin/update-apply.ts) | Nothing. In the second case, read the hourly canary’s result later |
rolled-back | The canary flight read dead on the new version, so the engine reverted to the prior version. Your archives and recovery are unaffected (decideKeep, engine/src/admin/update-types.ts; settleAfterPromote, engine/src/admin/update-apply.ts) | Read the reason on GET /admin/update/status. The prior version is live |
applied-unconfirmed | Cloudflare accepted the promote but the confirmation read could not be performed, so the engine cannot prove the intended version is the live one | Confirm 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 |
To return to the prior version, use the “Roll back” card on the Licence and updates screen once you apply an update from the console. The card rolls the engine back to the recorded prior version with a one-shot deploy token. After a secret change, the rollback uploads the prior code as a new version that keeps the current secrets. Upgrades and rollback holds the upgrade runbook and the rollback rules.
On an atomic apply, a singing verdict ends as applied. A dead verdict ends as rolled-back. An ailing or pending verdict ends as applied, with confirmationPending (decideKeep, engine/src/admin/update-types.ts; settleAfterPromote, engine/src/admin/update-apply.ts).
A gradual ramp settles on one canary flight (settleAfterRamp, engine/src/admin/update-ramp.ts). A gradual ramp rolls back to the prior version at one hundred percent when the canary reads dead. The ramp also rolls back when the hourly canary sang before the ramp and does not sing on the ramped version. The outcome is rolled-back.
If that rollback deploy fails, the outcome is rollback-failed-still-split, below. When the baseline is not singing, a passing self-check keeps the ramp at its percentage as applied. A failing self-check leaves the verification armed as inconclusive.
rollback-failed-still-split 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. 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.
In 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.
| Outcome | What happens | What you see |
|---|---|---|
| Engine refused | The console apply is aborted before it starts, the console is unchanged and the engine’s prior version keeps serving | “The release was not applied; the engine refused and the console is unchanged” |
| Engine settled, console upload or promote failed | The console auto-rolls back to its recorded target; the engine keeps the new version | The partial state (engine on the new version, console rolled back), with the console component retriable alone |
| Both landed | Engine and console are on the new release | Per-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.
| Refusal | What triggers it |
|---|---|
| Below the settled floor | The 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 required | The signed release is flagged as needing a Durable Object migration; apply it through the operator runbook instead |
| Engine too old | The release declares a minimum engine version newer than the one running, so an intervening version must be applied first |
| Hash mismatch | The downloaded artefact’s SHA-384 does not match the signed channel’s declared hash (corruption or tampering) |
| Backward or uncomparable | The channel recommends an older or uncomparable version and you have not explicitly opted into a downgrade-to-recover |
| No verifiable artefact | The 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 owned | The 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 verified | The verdict is “mismatch” when the module bytes differ from the signed digest, and “unavailable” when Cloudflare’s response to the read-back request cannot be read or its shape is not recognised. Enforce mode refuses promotion on anything but a verified read-back, so a mismatch or an unavailable verdict refuses there; the shipped default is warn, which records the verdict and promotes anyway, so by default a verdict that is not “verified” 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.
A rollback runs only in a request that carries your one-shot deploy token. 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. The console messages about a release name the release version. The console shows a Cloudflare version id in two places only: the Details disclosure of an apply, and the instruction to redeploy from the Cloudflare dashboard when a ramped release fails verification and its rollback fails too. The dashboard lists versions by that id.
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 .