Deploy the engine and console into your own Cloudflare account
This is the first-deploy walkthrough for standing the engine and console up in your own Cloudflare account, from an empty account to a working engine that reports ready: true. It is for the operator who runs the deploy: an SRE or platform engineer with access to the Cloudflare account, working from a trusted machine.
A deploy writes a Worker, so it is one of the two deliberate operator command-line actions in downpipes (the other is an upgrade). Everything after the deploy, attaching sources, setting a destination, running a restore, is done from the console with no terminal. The guided deploy script front-loads as much as it can: on a fresh engine it generates your keys on your own machine, installs the public halves as secrets, drops the offline recovery key into a local folder, and prompts once for the first owner’s email. This page is the written reference behind that flow; the console mirrors the same steps interactively.
Before you begin
This walkthrough uses a custom domain for the console (for example console.example.com), never a workers.dev host, which is disabled by policy. You provision and run these steps with a scoped, short-lived Cloudflare API token that you create now and revoke at the end. The exact permissions that token needs, and the proof that the running engine holds no token afterwards, are on the deploy token scopes page.
Prerequisites
Confirm all of these before you deploy, rather than discovering a gap from a failed run later. The engine’s own preflight (downpipe preflight before deploy, and GET /admin/preflight after) verifies most of them, but the plan and the domains are yours to arrange first.
| Prerequisite | Why it is required |
|---|---|
| Cloudflare account on Workers Paid | The deploy itself fails without it: the engine sets a raised [limits] cpu_ms that an unpaid account refuses, and the free plan’s 50-subrequest cap would break runs regardless. Activate Workers Paid before deploying. |
| Durable Objects enabled | The scheduler Durable Object owns the schedule, the run lock and the run-log index, and the seal Durable Object carries large runs between cron ticks. |
| R2 enabled | The default in-account destination, and a common source. An S3-compatible destination is the alternative. |
| A custom domain for the console | The console is the product’s only public surface in the shipped topology, and is custom-domain only. A second custom domain for the engine is needed only if you choose the split topology. |
wrangler authenticated against the account |
Confirm with npx wrangler whoami before you start. |
Activate Workers Paid first
The engine’s wrangler.toml raises the per-invocation CPU ceiling, which an unpaid account rejects at deploy time. This is deliberate: it turns “is this account on the right plan” into a clear deploy-time failure instead of a subtle one during a large backup. Activate Workers Paid on the account before you run the deploy.
The deploy, step by step
Get the source
The engine and the console are separate repositories, and you deploy each into your own account. Clone both now:
git clone https://github.com/downpipes/engine git clone https://github.com/downpipes/consoleThey are source-available under the Elastic License 2.0, so your security team can read exactly what will run in your account before you deploy it.
downpipes/downpipeis a third repository and is not part of this: it is the Go offline reader you use to recover archives with no engine and no vendor in the loop, covered in break-glass offline recovery.Create a scoped, short-lived deploy token
Create a Cloudflare API token scoped to the single account you are deploying into, to the relevant zone for the DNS record, and ideally to the named scripts. Give it a short expiry as a backstop. This token can write Workers, create KV namespaces and R2 buckets, write Secrets Store entries and edit DNS, so it is powerful and short-lived by design. The exact permission list is on the deploy token scopes page.
Export it for this deploy session only, alongside your account id, and never write it to a file that outlives the session:
export CLOUDFLARE_API_TOKEN=<the token> export CLOUDFLARE_ACCOUNT_ID=<your account id>The token stays on this machine and is sent only to
api.cloudflare.com. It is never sent to the vendor and never pasted into the console for routine use. You will revoke it in the final step.Set the console origin
The engine scopes its CORS to your console’s origin. In the engine’s
wrangler.toml, setCONSOLE_ORIGINto your console’s custom domain so the browser is allowed to call the engine:[vars] CONSOLE_ORIGIN = "https://console.example.com"Leave the source bindings out of
wrangler.toml. The engine declares no sources there on purpose, because you attach every source from the console later, and the deploy path preserves them. The reasoning is on the deploy safety and bindings page.Deploy with npm run deploy
Always deploy the engine with
npm run deploy, never a barewrangler deploy. The npm script runs a binding reconcile (scripts/sync-bindings.mjs) before it deploys, which reads the live worker’s bindings and deploys a superset, so a code deploy can never silently drop a source you attached from the console. A barewrangler deploywould replace the worker’s binding set with exactly whatwrangler.tomllists, dropping every console-attached source. This matters on every later deploy, but it shapes the first deploy too.On a first deploy nothing is attached yet, and the reconcile handles that by itself: it sees the worker does not exist, reports that there is nothing to preserve, and continues. So the command is just:
cd engine npm run deployDo not reach for DOWNPIPE_ALLOW_BINDING_RESET
You do not need
DOWNPIPE_ALLOW_BINDING_RESET=1on a first deploy, and you should not learn it as part of one. That flag does not mean “allow an empty reconcile”. It means “if I cannot read the live bindings, deploy anyway”, so on a later redeploy, when the reconcile cannot read them (an expired or revoked deploy token, for instance), it shipswrangler.tomlas-is and silently drops every source you attached from the console. Leave it unset and let a failed reconcile stop the deploy, which is the behaviour that protects your sources.The deploy creates the
downpipe-engineWorker, the scheduler Durable Object, the per-run seal Durable Object, and the*/15reconciliation cron that drives new runs. The console is a separate repository with its own deploy, and itswrangler.tomlpins arouteyou must change to your own custom domain. For the full rationale and the recovery path if a source ever goes missing, see why never a bare wrangler deploy.Let the deploy generate your keys on this machine
On a fresh engine (when the signer secret is not yet set),
npm run deploygenerates the full default key set on your own machine, usingscripts/generate-keys.ts. Nothing is sent to the vendor at any point. It produces three kinds of key material, with very different custody.The signer key pair (Ed25519 with ML-DSA-87) signs every run. Its private half is installed to the engine as the
SIGNER_PRIVATEsecret. The break-glass recipient key pair (X25519 with ML-KEM-1024) is what archives are sealed to. Only its public half goes to the engine, asBREAK_GLASS_PUBLIC; the private half is your offline recovery key. The operational recipient pair is generated and installed by default (both halves), which is what lets the engine prove its own backups restore through scheduled restore tests, drills and in-console restores. Strict break-glass-only custody, where the engine cannot read its own archives, is the deliberate opt-out you can choose later on the console’s Keys screen.The script installs the public and signer-private halves as Worker secrets for you, then writes the things you must keep into a local
./recovery-kit/folder:recovery-kit/identity.key the break-glass PRIVATE key: the only thing that can decrypt an archive recovery-kit/recovery-sheet.txt a printable sheet: public fingerprints and custody notes recovery-kit/recipient.pub the break-glass PUBLIC key, safe to keep anywhere; the engine has it recovery-kit/signer.pub the signer PUBLIC key: keep it, the reader refuses to run without itTwo of those four are must-keeps and they are must-keeps for different reasons.
identity.keyis the only thing that can decrypt an archive, andsigner.pubis the only thing that can verify one.downpipe verifyanddownpipe restoreboth take--signeras a required flag, and--allow-unverifieddowngrades a bad signature rather than waiving the flag, so it is not a way out. No copy ofsigner.pubsurvives in the archive or on the printed sheet, which carries its fingerprint and not the key.The engine never sees
identity.key. There is no upload path for it anywhere, and there is no server-side copy.Move the recovery kit offline
This is the one irreversible custody step, so do it deliberately. Move the
recovery-kit/folder to offline storage (an encrypted USB drive, a corporate password manager, a printed sheet in a safe) and remove it from this machine. Move the whole folder rather than the key alone.identity.keyis the only thing that can decrypt your backups if the engine or the account is ever lost, andsigner.pubis what the reader checks their signatures against. Neither can be regenerated from the engine, anddownpipe restorerefuses to run without both. Consider splitting custody across more than one holder; the console’s Keys screen explains the M-of-N pattern.The break-glass private key opens your archived data offline. It is a different thing from the admin recovery codes you will save in a later step, which are only for signing back in to administer the engine.
Set a one-time admin token to bootstrap the first owner
The admin token is a one-time bootstrap credential for claiming the first Owner. It is the lower-assurance break-glass path: anyone with the engine URL and the token can administer the engine, and a token caller is not attributable in the audit log, so you retire it as soon as a better path exists.
This happens during Step 4’s
npm run deploy, not as a separate manual step. When neitherBOOTSTRAP_OWNER_EMAILnorADMIN_TOKENis already set,deploy.shprompts you to choose: an emailed set-up link, or a no-email admin token. On the admin-token path the script generates the value itself withopenssl rand -base64 32, sets it as theADMIN_TOKENsecret, and prints it once:First-Owner set-up. Choose how you will claim the first Owner account: 1) Email link (needs Cloudflare Email Sending) 2) Admin token (no email; one-time break-glass bearer) Enter 1 or 2 [2]:Store the printed value in your secrets manager; it is never echoed by wrangler and there is no copy to recover later. The first Owner is claimed once and once only: the engine latches a flag the moment the first Owner exists, so the token can never mint a second Owner afterwards, even if the role table is later emptied.
Claim the first Owner, enrol a passkey, and save recovery codes
Open the console and sign in once with the admin token to claim the first Owner. Then either register your Owner passkey or claim the first Owner through Cloudflare Access. When you complete passkey enrolment the console shows you a set of recovery codes, once. Save them offline immediately, the same way any application’s recovery codes are saved. The engine stores only salted hashes of them, never the codes, so there is no way to see them again later; if you lose the set you regenerate a fresh one, which invalidates the old.
These recovery codes are your ongoing admin break-glass: if you ever lose your passkey, you enter a code at the sign-in screen to get back in, then enrol a new passkey and regenerate the codes. Recovery-code sign-ins are hard rate-limited and every attempt is audited, so misuse is loud.
The console’s in-browser key ceremony and this on-machine deploy path both exist and are both real. The deploy script’s on-machine generation is the default for a fresh deploy; the in-browser ceremony on the Keys screen is the rotation and advanced path, including setting up an M-of-N custodian split. The byte formats are identical, so a key from either path is read the same way by the engine and the offline tool.
Confirm ready:true
Check
GET /admin/status(the console’s onboarding reads it for you). The engine reportsready: trueonce the signer is present, the break-glass public key is present, and a destination resolves. If you set the destination from the console rather than inwrangler.toml, the status reflects that console-set choice, exactly as a run would resolve it.GET /admin/status{ "service": "downpipe-engine", "engineVersion": "<version>", "signerConfigured": true, "breakGlassConfigured": true, "operationalConfigured": { "public": true, "private": true }, "destConfigured": true, "destKind": "r2", "ready": true }Field Meaning signerConfiguredThe SIGNER_PRIVATEsecret is present. Presence only, never validity: a malformed value still reads true here, and preflight is what parse-checks it.breakGlassConfiguredThe BREAK_GLASS_PUBLICrecipient key is present.operationalConfigured.privateThe operational private half is present, so the engine can run drills and in-account restores. Its absence is a deliberate higher-assurance posture, not a fault. destConfigured/destKindA destination resolves, and which kind. An ambiguous configuration (both an R2 binding and S3 settings with no DEST_KIND) reports not configured rather than guessing.readyTrue when the signer, the break-glass public key and a destination are all present. This is the minimum to run a backup. The licence and update channel are not part of it. The status response never returns a secret, a key fingerprint, a destination endpoint or a downpipe name. It is presence booleans, the destination kind, the engine version and a count, so it is safe to poll during onboarding.
Probe the live prerequisites with preflight
Where status reports presence,
GET /admin/preflightprobes the runtime prerequisites live and affirmatively. It does a real Durable Object round-trip, confirms the cron has genuinely ticked recently, does a read-only existence check against the destination to prove it is reachable and authorised, parse-checks the keys, and enumerates every configured source binding to confirm it is present. Each item is verified, configured, unconfigured or failed, with the observed evidence and a remediation that names the Cloudflare product to enable. Do not consider the deploy finished over a red required item.GET /admin/preflightOne item is honestly not directly observable from inside a Worker: the Workers plan itself, because the engine holds no account API token to introspect it. Preflight says so, and names the deploy-time gate that covers it (the deploy refuses the configured CPU limit on an unpaid account). That honesty about what is probed versus what is gated at deploy is the same discipline the run path applies.
Revoke the deploy token
As soon as the engine is confirmed healthy, revoke the deploy token from the Cloudflare dashboard. After this the system runs with no Cloudflare API token anywhere: the engine holds none by design, and there is no stored deploy credential left to leak. You re-create the same scoped token only when you next deploy, upgrade, or rotate a secret, and revoke it again each time. The deploy capability is never left standing.
After the deploy
Two follow-ups close out a clean deployment.
Retire the admin token and prefer attributable identity. Once your Owner passkey works (or Cloudflare Access is wired) and your recovery codes are saved, dispose of the bootstrap token. You can retire it in the console’s Security Centre, which is immediate and needs no redeploy: the engine stops honouring the token bearer the instant you retire it. The engine cannot delete its own Worker secret, because by design it holds no standing Cloudflare token, so the retire flag is how it stops honouring the string; deleting the secret with wrangler is the equivalent. The engine refuses to retire the token until a way back in exists (recovery codes for an Owner, or a second Owner), so disposing of it can never strand you. Wiring Cloudflare Access then turns the amber shared-token verdict into the green per-email one. The full procedure is on the identity and access page.
Expect email to be inert until you onboard a sending domain. Outbound engine email (alerts, expiry warnings, the role-invite, the first-run set-up link) needs Workers Paid plus a separate send-email binding and dashboard onboarding of a verified sender domain. Until you complete that, every send is a clean, logged no-op rather than an error: notifications simply skip the email channel. So do not treat a quiet inbox after the first deploy as a fault; it is the fail-open default until you enable sending. The mechanics are covered in the day-2 notifications material.
Choosing a destination and attaching your first source
The destination is where sealed archives land, and you choose exactly one. The engine refuses an ambiguous configuration (both an R2 binding and S3 settings) and reports it as not configured rather than guessing.
For an R2 bucket in your own account there are no credentials on the wire; you bind it in wrangler.toml:
[[r2_buckets]]
binding = "DEST_R2"
bucket_name = "<your-archive-bucket>"For an S3-compatible destination you set the endpoint, bucket and region as vars and put the access keys as secrets. You can also set the destination from the console’s Destinations screen, where the engine write-probes it; a console-set destination wins over the deploy-time configuration, the same precedence a run applies.
A backup source is either a Workers binding (a KV namespace, an R2 bucket, a D1 database, or a Secrets Store secret) or one of four account-wide types read over the REST API through the engine’s read-only discovery token rather than a binding: Cloudflare configuration (cf-config), Workers scripts, Stream and Images. You attach sources from the console’s Sources screen with no terminal: for a binding source the engine adds the binding to its own live worker through the Cloudflare API and proves the change drops nothing; for a discovery-token source it registers the source with no binding change. Then you create a downpipe, which pairs a source with a schedule, and the first run seals that source to the destination. Scope the bindings to what genuinely needs backing up rather than reflexively binding the whole account, because a binding is standing read access for the life of the deployment. The reasoning is on the connect a source page and in the token scopes page.
Deploying with an AI assistant
These docs are served as an MCP server at https://docs.downpipes.io/mcp, so an assistant such as Claude Code can read the authoritative steps with its search_docs tool rather than guess at them. There is a copy-paste prompt on the deploy page of the website that sets this up for you.
An assistant is genuinely useful here, because most of a deploy is repetitive command-line work. But the engine’s first deploy is not assistant-safe, and that is a fact about the script rather than a matter of taste.
An assistant can safely do these. Clone both repositories and run npm ci in each, confirm npx wrangler whoami points at the right account, show you the wrangler.toml changes you need (the console’s custom-domain route, the engine’s CONSOLE_ORIGIN, and your destination), and deploy the console with npm run deploy.
Run these yourself, and do not delegate them.
- The engine’s first deploy. On a fresh engine,
npm run deploygenerates your keys on this machine and writes the break-glass PRIVATE key torecovery-kit/identity.key. It also prints a one-time, owner-equivalent admin token to the terminal, and it asks you an interactive question about how to bootstrap the first Owner. Run it in a terminal you control. Anything an assistant runs, it can read. - The recovery kit. Tell your assistant never to read, copy, print or commit anything under
recovery-kit/. That break-glass key is your universal way back into your archives, the one that still works when every other system is gone, and it belongs offline with you. - The admin API. Do not have an assistant call
/admin/*. The only credential it could authenticate with is your admin token, which resolves to Owner unconditionally. Finish in the console instead: the destination, your sources, your downpipes and notifications are all console screens. - The dashboard steps. Onboarding the sending domain under Compute, Email Service, Email Sending, and creating the Zero Trust Access application, are browser tasks. An assistant should give you the exact clicks and then wait for you to confirm each one is done.
The order to work in is the order of this page: clone, install and configure, then run the engine deploy yourself and move the recovery kit offline, bootstrap the first Owner, deploy the console, then attach a destination and sources from the console. Outbound email and Cloudflare Access are configuration you add to a working engine afterwards, so leave them until the engine reports ready: true.
Where this fits
This page is the spine the operations overview hands off to. For the exact token permissions and the create-use-revoke lifecycle, read deploy token scopes. For why npm run deploy is mandatory and how the reconcile keeps your sources, read deploy safety and bindings. To retire the admin token and wire Cloudflare Access once a passkey works, see identity and access. To choose between the routeless and split layouts, see topology. The conceptual grounding for the keys you generated here is in the key ceremony and recovery kit.
Last updated .