Skip to content
downpipes docs

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). A first install runs that one action twice, once in the engine checkout and once in the console checkout, because they are separate Workers in separate repositories. Everything after both deploys, 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 asks you two questions. One is the key posture. The other is how you will claim the first Owner.

This page is the written reference behind that flow; the console mirrors the same steps interactively.

Before you begin

You can serve the console on a Cloudflare workers.dev hostname or on a custom domain you own. Choose one before you start:

  • Start on workers.dev. You need no zone and no DNS. The shipped wrangler.toml files use a custom domain, so you change three settings: the console’s workers_dev, the console’s route, and the engine’s CONSOLE_ORIGIN. Email sending and invite links need a sending domain you own, so they need a custom domain.
  • Use a custom domain (recommended for production). The shipped wrangler.toml files use this path. A zone you control lets you set Minimum TLS 1.2, which is free. It lets you add the Modern cipher suite preset, which is optional and needs the paid Advanced Certificate Manager add-on. Cloudflare Access gets a hostname of your own, and email and invite links get a domain.

The steps that differ are marked workers.dev path and Custom domain path. A step with no mark applies to both. You provision and run these steps with a scoped, short-lived Cloudflare API token that you create now and revoke at the end. The permissions that token needs, and the evidence 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.

PrerequisiteWhy it is required
Cloudflare account on Workers PaidThe 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 enabledThe 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 enabledThe default in-account destination, and a common source. An S3-compatible destination is the alternative.
A custom domain for the console (optional, recommended for production)The console is the product’s only public surface in the shipped topology. On the workers.dev path it is served at https://<console worker name>.<your account's workers.dev subdomain>.workers.dev and you need no zone. A custom domain adds a zone you control, a hostname for Cloudflare Access, and a sending domain for email and invite links. A second custom domain for the engine is needed only if you choose the split topology.
Edge TLS posture on the console’s zone (custom domain path only)Minimum TLS 1.2 is free on a zone. The Modern cipher suite preset is optional hardening: with it, the console negotiates only recommended, forward-secret suites. A cipher preset needs Cloudflare’s paid Advanced Certificate Manager add-on on the zone. Set before you claim the first Owner; see the note below. A workers.dev hostname has no zone of yours, so you cannot set these on that path.
wrangler authenticated against the accountConfirm with npx wrangler whoami before you start.
Node.js 22.18 or later (on the 23 line, 23.6 or later)npm run deploy runs the engine’s TypeScript files directly, and an earlier Node refuses them. The script checks the version before anything else runs, and stops with a message that names 22.18 and the version it found.

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.

Set the console's edge TLS posture before you claim the first Owner

This note applies to the custom domain path. On the workers.dev path you have no zone, so you cannot set the edge TLS floor or cipher suites.

On the custom domain path the console is served in your own Cloudflare zone, so its edge TLS posture is a zone setting you set yourself. downpipes has no standing or deploy-time Cloudflare credential that can touch it: the deploy token on the deploy token scopes page carries no Zone Settings Edit permission. Cloudflare’s zone default, the “Legacy” cipher preset, enables DES-CBC3-SHA, static-RSA suites such as AES128-SHA, and CBC suites such as ECDHE-ECDSA-AES128-SHA. None of them is a recommended suite.

Under SSL/TLS > Edge Certificates on the zone that carries your console domain, set:

  • Minimum TLS Version 1.2 (PATCH /zones/{zone_id}/settings/min_tls_version with value "1.2"). Free on the zone.
  • Cipher suites: the Modern preset (optional; PATCH /zones/{zone_id}/settings/ciphers with the Modern list, or set per-hostname under Edge Certificates). This preset needs the Advanced Certificate Manager add-on. It admits only forward-secret AEAD suites. With it set, TLS 1.3 negotiates TLS_AES_256_GCM_SHA384 and TLS 1.2 negotiates an ECDHE suite, the strongest suite each protocol version offers.

Verify the settings from a workstation, against your own console domain. If you did not set the Modern preset, skip the first check, because it relies on that preset:

openssl s_client -connect console.example.com:443 -servername console.example.com -tls1_2 -cipher 'ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:AES128-SHA:@SECLEVEL=0' </dev/null

This must end in alert handshake failure (alert 40): the zone refuses the connection because no recommended suite matches. The list holds CBC suites for an ECDSA certificate and for an RSA certificate, and the Modern preset excludes all of them. Keep the ECDSA suites in the list. A static-RSA suite such as AES128-SHA needs an RSA certificate, so a hostname that serves only an ECDSA certificate refuses it under every preset. AES128-SHA alone is therefore not a test of the preset. Then confirm a forward-secret suite still works:

openssl s_client -connect console.example.com:443 -servername console.example.com -tls1_2 -cipher 'ECDHE:@SECLEVEL=0' </dev/null

This must complete and report an ECDHE cipher.

The deploy, step by step

  1. Get the source

    The engine and the console are separate repositories, published at github.com/downpipes-io, and you deploy each into your own account. Clone both before you start this page:

    git clone https://github.com/downpipes-io/engine
    git clone https://github.com/downpipes-io/console

    They 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-io/downpipe is a third, separately published repository and is not part of this: it is the MIT-licensed Go offline reader you use to recover archives with no engine and no vendor in the loop, covered in break-glass offline recovery.

    Check out the release tag named on the changelog rather than an arbitrary branch tip. A release tag is v<version>. Engine 0.3.10 is v0.3.10, and console 0.2.10 is v0.2.10. Check out the tag in each repository:

    cd engine
    git checkout v0.3.10
    cd ../console
    git checkout v0.2.10

    Run npm run deploy from that checkout. For a source-available product this is what deploying the release means: npm run deploy always builds from the checkout in front of it.

    The release workflow builds the signed channel’s artefact from the tagged commit. The build is reproducible: a rebuild of the tagged source gives the same bytes, which verify a release shows how to check. There is no separate “download the pre-built release” step to look for.

  2. Create a scoped, short-lived deploy token

    Create a Cloudflare API token scoped to the single account you are deploying into, 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 and write Secrets Store entries, so it is powerful and short-lived by design. The permissions it needs depend on your path. The full list, with the reason for each, is on the deploy token scopes page.

    workers.dev path: the token needs no Zone permission. It needs six Account permissions: Workers Scripts: Edit, Workers KV Storage: Edit, Workers R2 Storage: Edit, D1: Edit, Secrets Store: Edit and Account Settings: Read. These six complete both deploys.

    Custom domain path: add two Zone permissions to those six: Workers Routes: Edit and DNS: Edit. Limit them to the zone that holds the console’s domain.

    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.

  3. Set the console origin

    The engine allows one exact origin for CORS, cookie-borne requests and passkeys. In the engine’s wrangler.toml, set CONSOLE_ORIGIN to your console’s origin so the browser is allowed to call the engine.

    Custom domain path: use your custom domain, for example console.example.com.

    [vars]
    CONSOLE_ORIGIN = "https://console.example.com"

    workers.dev path: use the console’s workers.dev origin, https://<console worker name>.<your account's workers.dev subdomain>.workers.dev. The console worker name is the name in the console’s wrangler.toml (downpipe-console by default).

    [vars]
    CONSOLE_ORIGIN = "https://<console worker name>.<your account's workers.dev subdomain>.workers.dev"

    You need the subdomain before the engine deploy, because CONSOLE_ORIGIN goes into the engine’s deploy. The Workers and Pages section of the Cloudflare dashboard shows your account’s workers.dev subdomain. The API returns it too, from GET /accounts/{account_id}/workers/subdomain. If the account has no workers.dev subdomain, register one in the dashboard before you deploy.

    The header comments in the shipped wrangler.toml files describe the custom-domain default. The workers.dev edits on this page still apply.

    The engine keeps workers_dev = false on both paths. The console reaches it only through the service binding.

    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.

  4. Deploy with npm run deploy

    Always deploy the engine with npm run deploy, never a bare wrangler 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 bare wrangler deploy would replace the worker’s binding set with exactly what wrangler.toml lists, 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 ci
    npm run deploy

    npm ci installs the dependencies that package-lock.json pins. The preflight stops the deploy if the installed dependencies do not match that file.

    The script checks the checkout before it changes anything. This preflight has four steps. npm run lint:deps-installed checks that the installed dependencies match package-lock.json. The other three are npm run typecheck, a wrangler deploy --dry-run build into a temporary directory, and npm run validate:customer, the checks that guard your data and keys. The preflight took 299 to 738 seconds (5 to 12 minutes) when measured, at load averages from 15 to 110. The script deploys nothing until every step passes.

    • Exit 1 means that a check found a fault in the code. The output names the check.
    • Exit 2 means that a check could not read what it checks. The usual causes are dependencies that do not match package-lock.json (run npm ci) and a check that could not reach something on this machine. The output names the check.

    After exit 1 or exit 2, the script has deployed nothing, set no secret and made no key. Correct the cause and run npm run deploy again.

    Do not reach for DOWNPIPE_ALLOW_BINDING_RESET

    You do not need DOWNPIPE_ALLOW_BINDING_RESET=1 on 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 ships wrangler.toml as-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.

    On a fresh engine the script asks two questions, one after the other. Read each before you press Enter.

    The first, Enter 1 or 2 [1]:, asks for the key posture. Option 1 is break-glass-only, which a bare Enter keeps. Option 2 is the two-recipient posture.

    The second, Enter 1 or 2 [2]:, asks how you will claim the first Owner. Option 1 is an emailed set-up link. Option 2 is the admin token, which a bare Enter keeps. A bare Enter at both prompts gives break-glass-only custody and an admin token.

    The deploy creates the downpipe-engine Worker, the scheduler Durable Object, the per-run seal Durable Object, and the */15 reconciliation cron that drives new runs. The console is a separate repository with its own deploy, and its wrangler.toml ships with a custom-domain route. On the custom domain path, change it to your own domain. On the workers.dev path, remove it, as the console step below shows. For the full rationale and the recovery path if a source ever goes missing, see why never a bare wrangler deploy.

  5. Let the deploy generate your keys on this machine

    On a fresh engine (when the signer secret is not yet set), npm run deploy generates the full default key set on your own machine, using scripts/generate-keys.ts. Nothing is sent to the vendor at any point. The key material it produces has 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_PRIVATE secret. 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, as BREAK_GLASS_PUBLIC; the private half is your offline recovery key. The operational recipient pair exists only if you choose the two-recipient posture, and then the engine holds both halves.

    In both postures it also generates the configuration recovery key pair and the configuration wrap key, and installs them to the engine. The configuration recovery private opens only this engine’s own sealed configuration export, never an archive. The wrap key encrypts the credentials the engine stores. Neither is written to the recovery kit.

    It buys less than the name suggests: it buys the UNATTENDED work, the scheduled restore tests, the scheduled drills and the in-account retention pruning that need a key at a moment when nobody is present to supply one. Verification at seal and the hourly canary do not need it. Nor does an in-console restore, which works in either posture; without the operational key you supply your break-glass key to the browser for that restore and it is wiped when the restore finishes.

    Both paths ask you to choose a posture before they generate any key, and neither lands you in the two-recipient posture unless you choose it. Strict break-glass-only custody, where the engine holds no key that can read an archive, is where a terminal deploy lands by default: a bare Enter keeps it at the npm run deploy prompt. The console has no default and waits for you to choose, with break-glass-only listed first. The operational key is something you opt into on either path. You can relax break-glass-only later with no re-key, because the Keys screen adds an operational key; the reverse does not fully undo, because archives sealed while that key was present stay readable by it.

    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 a pinned signer

    Two of those four are must-keeps and they are must-keeps for different reasons. identity.key is the only thing that can decrypt an archive, and signer.pub is the only thing that can verify one. downpipe verify and downpipe restore both refuse to run without a pinned signer, and --allow-unverified downgrades a bad signature rather than waiving that, so it is not a way out.

    The printed sheet carries its fingerprint, not the key. The bucket that your engine seals runs to holds a copy of the current signer’s signer.pub that the sheet’s fingerprint can pin (reader 0.3.4, --signer-fingerprint). Replication copies the recovery bundle to each destination that holds runs, and retries a copy that has not landed at the next cron tick. After a re-key the copy is the new signer’s, so keep the file.

    The engine never sees identity.key. There is no upload path for it anywhere, and there is no server-side copy.

  6. 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.key is the only thing that can decrypt your backups if the engine or the account is ever lost, and signer.pub is what the reader checks their signatures against. The engine cannot regenerate identity.key. The bucket holds only the current signer’s signer.pub, so the kit file is the only copy for runs sealed before a re-key. downpipe restore refuses to run without the key and a pinned signer. 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 differs from the admin recovery codes you save in a later step, which are only for signing back in to administer the engine.

  7. Set an admin token to claim the first Owner

    The admin token claims the first Owner. It has no expiry and no use count. It stays valid until an Owner retires it, you set ADMIN_TOKEN_DISABLED, or you delete or rotate the secret. 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. It is the second of the two prompts in that step, after the key posture prompt. When neither BOOTSTRAP_OWNER_EMAIL nor ADMIN_TOKEN is already set, deploy.sh prompts 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 with openssl rand -base64 32, sets it as the ADMIN_TOKEN secret, 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; break-glass bearer, valid until retired)
    Enter 1 or 2 [2]:

    On the email-link path, pressing the send button again while the link is valid sends the same link. The engine mails it at most once every 60 seconds (mintBootstrapInvite, engine/src/sched/scheduler-do-passkey-invites.ts; BOOTSTRAP_RESEND_COOLDOWN_MS, engine/src/admin/passkey.ts).

    Store the printed value in your secrets manager; wrangler never echoes it, and no copy exists 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.

  8. Deploy the console

    Nothing so far has put a console in your account, and the next step opens one, so deploy it now. The console is a separate repository and a separate Worker, with a service binding to the engine. Set its address in console/wrangler.toml to match the origin you put in the engine’s CONSOLE_ORIGIN in step 3, then deploy from the console checkout.

    Custom domain path: set the pattern of the route to your domain. The shipped file already has workers_dev = false.

    route = { pattern = "console.example.com", custom_domain = true }

    workers.dev path: set workers_dev = true, and remove the route line or comment it out.

    workers_dev = true
    # route = { pattern = "console.downpipes.io", custom_domain = true }

    On either path you can set preview_urls = false in both wrangler.toml files to switch off per-version preview hostnames. Run the deploy:

    cd console
    npm ci
    npm run deploy

    As in the engine, npm ci installs the pinned dependencies first. npm run deploy here is the console’s guided scripts/deploy.sh: it builds the single-page app into public/app.js, runs a blocking preflight that stops the deploy on any failure, then runs wrangler deploy against console/wrangler.toml. The preflight runs npm run lint:deps-installed, npm run typecheck and npm run validate:customer, the checks that guard your keys in the browser. It took about 36 to 75 seconds when measured. Never deploy the console with a bare wrangler deploy, which skips that preflight. This is the deploy an assistant can safely run for you, unlike the engine’s; see deploying with an AI assistant below.

    Unlike the engine, the console has no keys, no secrets and no source bindings of its own, so redeploying it is not the hazard a bare engine deploy is. The full runbook, including the licence pin note, is on self-host.

  9. Claim the first Owner, enrol a passkey, and save recovery codes

    Open the console you just deployed 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 per source and per email from each source, and every attempt is audited, so misuse is loud (recoveryRecover, engine/src/sched/scheduler-do-recovery.ts).

    Your keys already exist by the time you sign in here, because the deploy generated them. The console’s own first-run ceremony, and the custody posture you have landed in, are set out in which ceremony generated your keys.

  10. Confirm ready:true

    Check GET /admin/status (the console’s setup reads it for you). The engine reports ready: true once the signer is present, the break-glass public key is present, and a destination resolves. A fresh engine reports ready: false until you set a destination. If you set the destination from the console rather than in wrangler.toml, the status reflects that console-set choice, exactly as a run would resolve it.

    GET /admin/status

    This is the response of a default deploy (break-glass-only posture, admin token, R2 destination set from the console). It shows the fields this step reads. The response carries further fields, which the table below covers in part.

    {
      "service": "downpipe-engine",
      "engineVersion": "<version>",
      "signerConfigured": true,
      "breakGlassConfigured": true,
      "operationalConfigured": { "public": false, "private": false },
      "operationalRetired": true,
      "configKeysConfigured": {
        "recipient": true,
        "recipientHalves": { "public": true, "private": true },
        "wrapKey": true
      },
      "destConfigured": true,
      "destKind": "r2",
      "ready": true,
      "adminTokenConfigured": true,
      "bootstrapConsumed": true
    }
    FieldMeaning
    signerConfiguredThe SIGNER_PRIVATE secret is present. Presence only, never validity: a malformed value still reads true here, and preflight is what parse-checks it.
    breakGlassConfiguredThe BREAK_GLASS_PUBLIC recipient key is present.
    operationalConfigured.privateThe operational private half is present. While operationalRetired is false, the engine uses it to run drills and restore tests unattended, and to restore with no key from you. Its absence is a deliberate higher-assurance posture, not a fault.
    operationalRetiredThe break-glass-only marker OPERATIONAL_RETIRED is set. The engine then seals to the break-glass key alone and opens no run with an operational key. When operationalConfigured also reports a half as present, an old operational key is still set in your account, and the switch to break-glass only on the Keys screen deletes it.
    configKeysConfiguredrecipient is true when both CONFIG_RECIPIENT_PRIVATE and CONFIG_RECIPIENT_PUBLIC are present, and recipientHalves names each half. wrapKey is true when CONFIG_WRAP_KEY is present. Presence only. The Keys screen reads it to offer the add for an engine keyed without them.
    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.
    artefactSha384 / artefactSha384SourceAfter npm run deploy, the SHA-384 of the main module that the deploy uploaded, and the source deployed. To compare, run shasum -a 384 .wrangler/deploy-bundle/index.js in the engine checkout. An ARTEFACT_SHA384 value that you set yourself takes precedence, with the source operator.

    The status response never returns a secret, a destination endpoint or a downpipe name. It holds presence booleans, the destination kind, the engine version, counts and deployment identifiers. The identifiers include the account id, the version id and the fingerprint of the release signer the engine pins. It is safe to poll during setup.

  11. Probe the live prerequisites with preflight

    Where status reports presence, GET /admin/preflight probes 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/preflight

    Straight after a first deploy, the cron-tick item reads unconfigured and the summary reads 7 of 9 required items verified. The evidence says no cron tick has been observed yet. The engine must see its first scheduled tick, and the cron runs every 15 minutes (*/15). Call preflight again after the first tick: cron-tick turns verified and the summary reads 8 of 9. The ninth required item is the Workers plan, which stays configured because the engine cannot observe the plan.

    One item is not directly observable from inside a Worker: the Workers plan itself. The engine does not read the account’s plan. Preflight says so, and names the deploy-time gate that covers it (the deploy refuses the configured CPU limit on an unpaid account).

  12. Revoke the deploy token

    As soon as the engine is confirmed healthy, revoke the deploy token from the Cloudflare dashboard. After this, no deploy token is stored anywhere, so no deploy credential is left to leak. The only Cloudflare API token the engine can hold is the optional read-only discovery token, which cannot write to or deploy your account. 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.

Which ceremony generated your keys

There are two real ways a downpipes engine can come by its keys. A first-time reader can meet both without being told they are alternatives. This section is the reconciliation, because picking the wrong one is the most expensive mistake available on this page.

The on-machine path, which is the one you just took. On a fresh engine, before the SIGNER_PRIVATE secret is set, npm run deploy generates the full key set on your own machine. The same command installs the halves the engine needs. Nothing is sent to the vendor.

The in-browser path, which is the console’s setup. Setup’s Keys step generates a key set in the tab. Its Apply step installs the set to the engine with one deploy token that you paste. It is the path for an engine that has no keys yet. The Keys screen keeps the same ceremony afterwards for rotation and for an M-of-N custodian split.

The in-browser set also includes the configuration keys: the configuration recovery key pair and the configuration wrap key. They install straight to the engine with the signer key and are never files. On an engine that has a signer and lacks either key, the Keys screen’s Add configuration keys installs the missing keys. A configuration recovery key added this way opens only the configuration exports that the engine seals after you add it. An older sealed export opens offline with the identity.key that was current when the engine sealed it, and the downpipe reader’s unseal-export command.

The byte formats are identical, so a key from either path is read the same way by the engine and by the offline reader. What is not interchangeable is the key material itself.

A second key set does not open archives sealed to the first

Only one of these two runs on any given engine. Because you deployed from a terminal, your keys exist already, and the console will not ask you to make them. GET /setup-state reports keysReady true once the signer and the break-glass public key are both present. Setup then reads its Keys step as done. It opens at the first step you have not completed, which is Connect on a new engine, so it does not show Welcome. After Protect it goes to Apply and does not show Keys. The Keys view “Your engine already holds keys” shows only when you open the Keys step yourself, at /setup/keys.

That matters because the quickstart has a “make your keys” step and reads, on its own, as though every new deployment makes keys there. If you override the console’s own judgement and replace the keys anyway, you get a new break-glass key, and your existing archives are sealed to the old one. A new break-glass private key cannot decrypt them, and the old one is not recoverable from the engine, which never held it. Deliberate re-keying is a rotation, covered on the Keys screen and on rotating your keys, different from a first-run ceremony.

Both paths ask, and neither installs an operational key unasked

This is the choice that decides what your engine can read, and neither path lands you in a posture you did not choose.

Strict break-glass-only custody, where the engine holds no key that can decrypt an archive, is the stricter posture, and both paths put it first. npm run deploy asks before it generates anything: Enter 1 or 2 [1], where 1 is break-glass-only and a bare Enter keeps it. With no terminal attached it takes break-glass-only and says what it skipped. A later npm run deploy keeps that choice: the console ceremony records break-glass-only with the OPERATIONAL_RETIRED marker, and a deploy that finds keys but no operational key asks the same question rather than installing one. Only --enable-operational, --yes or typing 2 installs the operational pair, and that is the two-recipient posture: the engine can decrypt your archives, and so could anyone who took over your Cloudflare account. The step above spells out what the operational key buys, which is the unattended work only.

If you did choose two-recipient, leaving it is a switch rather than a re-key: choose break-glass-only on the console’s Keys screen and the engine’s operational secrets are removed. Runs sealed before you make that switch stay wrapped to the operational key. The switch deletes that key, so the engine cannot read those runs after it, but a copy of the key taken while it was in your account still opens them; removing the key does not retroactively protect them. The postures and what each costs you are set out in full on recovery postures.

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. With dual control on, the retire waits for a second Owner’s approval. The engine cannot delete its own Worker secret, because by design it holds no Cloudflare token that can change its own secrets, 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; see creating a second Owner from the admin token if the original Owner’s passkey and codes are both gone), so disposing of it can never strand you.

The amber shared-token verdict shows only for a caller that presents the admin token. A passkey or identity-provider sign-in shows its own verified verdict. To put Cloudflare Access in front of the engine as well, follow the procedure 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 with no DEST_KIND) 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 MCP-compatible coding assistant can use its search_docs tool to find and quote the right step here rather than guess at it. The index is a crawl of the published site and can lag a recent edit by hours, so if a search result and this page differ, follow this page. There is a copy-paste prompt on the deploy page of the website that sets this up for you.

An assistant is useful here, because most of a deploy is repetitive command-line work. But do not let an assistant run the engine’s first deploy: the script writes your break-glass private key and prints an owner-equivalent token.

An assistant can safely do these. Run npm ci in each checkout, confirm npx wrangler whoami points at the right account, show you the wrangler.toml changes you need (the console’s workers_dev and route settings, 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 deploy generates your keys on this machine and writes the break-glass PRIVATE key to recovery-kit/identity.key. It also prints an owner-equivalent admin token to the terminal, once. It asks you two questions: the key posture, and how to bootstrap the first Owner. Run it in a terminal you control. Any program that runs in that terminal can read the output.
  • 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, run the engine deploy yourself and move the recovery kit offline, deploy the console, then bootstrap the first Owner in it, and attach a destination and sources from the console. The console deploy comes before the Owner bootstrap because the bootstrap is done in the console. There is nothing to open until it is deployed. 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. The closing checks of this page, ready: true, a green preflight and the follow-ups, are consolidated with the first run and the restore drill on the verify your installation checklist.

For the 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 .