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: since channel schema v2, first published with the 0.1.3 release, 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 that does not sing 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 update channel is live as of the ceremony date: the owner ceremony is complete and the vendor serves a signed channel at the singular update.downpipes.io. On the shipped base and demo 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. And treat the first live update you apply on your own deployment as supervised: the real Cloudflare deploy drivers are correct by the documented API shapes and defensively written, and they are validated against a stubbed Cloudflare by design. The engine driver has now also been exercised live exactly once: on 2026-07-03 a promote and an automatic rollback both ran against a real Cloudflare account, proving the upload, the binding and secret preservation, the atomic promote and the rollback for real. One flight is not a track record, and the console component’s static-assets driver has not run against a live account at all, so the supervised-first stance stands, hardest for a release that carries the console. This is the canonical reference for 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 immutable, append-only archive objects in your own bucket; an engine version cannot rewrite or delete them as part of a deploy. 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, which Cloudflare lets you roll back. The harness’s entire job is to not leave you on a broken live version.
That is also why the apply logic is testable without a real account: it is pure orchestration over injected interfaces (a deploy driver per component and a health gate), so every brick-safety property is proven by the validator with fakes. The only parts that talk to Cloudflare are the deploy drivers, covered under the supervised-first caveat 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 and demo config the signer is pinned and the channel URL is set, and as of the ceremony date 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, before the owner ceremony has run) 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 so the console can disable the apply honestly when a release declares a minimum engine version newer than the one you are running (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.
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.1.2 to 0.1.3”) and, when the release carries one, a console row (“Console 0.1.0 to 0.1.3”, or an honest “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 it gates the apply: Update now stays disabled until you tick an acknowledgement for each blocking step, so you cannot sleepwalk past an action the release needs. 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 of its assets would actually change. 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.
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. The token needs only the “Edit Cloudflare Workers” template (Workers Scripts: Edit on the engine’s account), and 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 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. A routine release is never queued this way, and 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 safe-apply lifecycle, in order
Ordering is the safety. The harness 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.
| 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 |
| 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 | Refused, engine unchanged |
| Read-back gate | Read 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 refuse | In warn mode (the default) the verdict is recorded and the apply proceeds; in enforce mode 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 real canary on the now-live new code | Drives keep-or-rollback |
| Keep or auto-rollback | Keep 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, 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; and with a previously-singing baseline, a verdict that is still incomplete after the retries falls through to the self-check rather than triggering a rollback. When the canary cannot gate the deployment at all (no destination configured), the self-check is the gate: it confirms the new code boots, answers, reports the expected version and passes a fresh preflight that round-trips the scheduler Durable Object before the version is trusted.
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 outcome | What it means | What you do |
|---|---|---|
applied |
The canary sang on the new version and it was kept | Nothing |
rolled-back |
The canary did not sing, and the rollback deploy succeeded. You are back on the prior version | Nothing, though the release wants investigating before you retry it |
rollback-failed |
The 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 dead | Act. Re-deploy the recorded prior version from the Cloudflare dashboard. The engine states the exact version to re-deploy in its reason string |
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. This deliberately does not claim applied, which would claim a proof that was never obtained |
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 precisely 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: archives are immutable 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 truth. An earlier revision of this page implied that an unresolved settle simply leaves the new version live for the hourly canary to judge later; that is not the model. The settle decides, the decision is persisted first, and the recorded outcome is authoritative.
When a release carries the console too
Channel schema v2 lets one signed release describe 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. Engines older than 0.1.3 ignore the component map and keep reading the engine-only artefact list, so an old 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, meaning a keep verdict from the canary, or from the self-check on an engine whose canary cannot gate, 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 or rolled back | The 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 failed | The console auto-rolls back to its recorded target; the engine keeps the new version | The exact partial state, reported honestly, with the console component retriable alone |
| Both landed | Engine and console are on the new release | Per-component confirmation, then the reload prompt 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, with risk classes driving 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 when you need to; Update now applies everything the release recommends.
Existing installs: the first console-capable rollout is manual, once
A console deployed before the 0.1.3 component-aware feature contains none of this UI and cannot update itself from the channel. The first console-capable rollout on an existing install therefore requires one manual console redeploy (npm run build then npx wrangler deploy from the console package, per the self-host runbook). Every console fix after that arrives through the signed channel like an engine one. There is no way around this with a static single-page app, and this page does not pretend otherwise.
The refusal cases: when nothing is deployed
A refusal is a feature. In each of these cases the harness 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 byte-exact | The 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 today 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.
The supervised-first reality of a live apply
Everything above is proven by the validator with fakes. The live 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 are made correct by the documented API shapes and defensive about every opaque failure: they read the target script’s current settings to preserve every binding, re-send each non-secret binding verbatim, list the secret binding types so Cloudflare keeps the secrets and their values, carry over the runtime compatibility fields, and refuse the upload outright on any binding type they cannot guarantee to carry forward (a clean refusal is always safer than a version that drops a binding and bricks a Worker).
The live record, stated precisely: the engine driver has run against a real Cloudflare account exactly once, on 2026-07-03, and that one apply exercised the whole arc, upload, binding and secret preservation, atomic promote, settle, automatic rollback, all of which executed correctly. That flight is also what taught the settle its patience: the rollback it ran was triggered by a transient verdict read in the post-swap reset window, exactly the class of failure the bounded retry above now absorbs before deciding. The console driver has not run against a live account at all.
The practical guidance is unchanged: supervise the first live apply on any given deployment, and treat the first console-carrying release as the most supervised of all. The upstream harness gates everything before a driver can act, and a bad deploy is at worst a brief availability blip that is rollback-able, but the first time a driver runs against your real account is the first time those API shapes are exercised for you. Watch it, and keep the operator deploy path (covered on upgrades and rollback) as the fallback for any release the harness refuses.
Auto-rollback only happens inside the apply or the settle request. The engine does not self-heal a bad deploy out of band: the hourly 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, the recorded outcome on GET /admin/update/status is the truth to trust, 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 today, 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 .