Skip to content
downpipes docs

Licensing, editions, and the control-plane

This page explains how downpipes is licensed, what its tiers are, and what the vendor-side control-plane is from your point of view as an evaluator. It is the canonical home for the commercial model and for the one internet-facing component the vendor operates.

The shape of it stays small on purpose, however many tiers sit on top of it. The engine runs in your own Cloudflare account; the vendor never holds a backup, a recovery key or a Cloudflare token, at any tier. The control-plane exists to mint a licence your engine verifies and to keep the operator’s own customer-of-record ledger. It takes no payment: there is no checkout in the product. It cannot reach your account, and a licence never gates a backup or a recovery, whatever the tier and whatever the volume behind it.

What it costs

There are two bills, and only one of them is ours.

What you pay us. Nothing, unless you buy a paid plan. Community is free, fully featured and free forever, and it is the default: a Community customer simply carries no licence token. The paid offer is Enterprise and Custom, and buying one is a quote, a purchase order, and an invoice paid by bank transfer, for an annual or a three-year term. There is no online checkout, no card processor and no subscription anywhere in the product, so there is no price list to read here and no self-serve price to look up. The tiers, and what the paid ones actually buy, are the next section down.

What you pay Cloudflare. Everything else, and directly. The engine runs in your own Cloudflare account, so Cloudflare bills you at their own rates for the Workers requests, the Durable Objects and the archive storage the engine uses, including the operations each run performs against that storage. We add no markup and see none of it. Backing up more data, more often, and keeping it for longer all cost more. The floor is a Workers Paid plan; what sits on top of it depends entirely on your volumes, and the prerequisites set out what to work it out against.

We do not restate Cloudflare’s rates, because they are Cloudflare’s to change and only you know your volumes. Once you have sources and a schedule configured, the console’s calculator will project the storage and read side of that bill for you as a planning estimate, described in predicting storage cost. It cannot answer the question before you have configured anything, which is why the answer above is prose rather than a number.

What the control-plane is, and what it structurally cannot do

The control-plane is the only internet-facing, vendor-operated component of downpipes. It is a Cloudflare Worker reached on a custom domain (the workers.dev route is disabled by policy, matching the engine and console). It mints the out-of-band assurance licence that your in-account engine verifies, and it receives a content-free advisory beacon. A prospective customer never reaches it directly: an enquiry goes to the vendor, and a licence is minted by an operator afterwards. The control-plane also keeps a customer-of-record ledger of who was issued a licence, an operator-side record read back only through a handful of internal, non-public routes: listing or fetching a customer, the support-entitlement check, the beacon-derived deploy ledger, and a CoR-health probe (/admin/licences, /admin/entitlement, /support/entitlement, /admin/deploy-ledger, /admin/cor-health in control-plane/src/index.ts). Its only scheduled work is a daily renewal sweep, which emails reminders ahead of an expiring term (runRenewalSweep, control-plane/src/registry/renewal-sweep.ts, wired to the Worker’s scheduled handler in control-plane/src/index.ts). A sale itself happens by direct contact with the vendor, off this Worker entirely.

These limits are structural, not promises. They come from the threat model and are enforced by what the Worker holds and the paths it exposes (control-plane/SECURITY.md).

The control-plane Position in code
Customer data No backup or per-downpipe data. It does keep an operator-side customer-of-record ledger: for each licensed account, the organisation, named contacts, an invoice reference, and the commercial term, alongside the vendor-signed licence token (kept for the operator’s own re-retrieval) (control-plane/src/registry/store.ts). It separately holds, only when an operator turns the beacon on, minimal beacon aggregates in a distinct BEACONS namespace (control-plane/src/beacon/receive.ts).
Cloudflare tokens None held. There is no binding, credential or code path by which it can reach a customer’s Cloudflare account, read a backup, or run a recovery.
Card data None held, at any tier, and none received. There is no checkout and no payment processor in the product: paid plans are invoiced and paid by bank transfer, off the platform entirely.
Secrets it does hold The licence signing key (LICENCE_SIGNER_PRIVATE), the admin mint bearer (ADMIN_MINT_TOKEN) that gates the operator-only mint endpoint and the customer-of-record admin reads, a separate read-only entitlement bearer (ENTITLEMENT_READ_TOKEN), In production all of these live in the account Cloudflare Secrets Store, never in source.
The break-glass key Never reaches the vendor. Even a full compromise of the control-plane cannot decrypt a customer archive.
Reaching into an account No inbound path exists that can touch a backup or a recovery.

The licence signing key is not your encryption key and not your break-glass key. It is the vendor key that signs licences, and the only thing your engine ever does with the matching public half is check a signature. Your archive encryption keys and your offline break-glass key are described in the no-custody trust model; they live with you, never here.

Six licence tiers, one commercial model

The difference between tiers is people, paperwork and the volume a plan covers, never product capability.

Community is free, fully featured, and free forever. Every security control ships in it for everyone: external identity-provider sign-in, multi-factor authentication, Cloudflare Access, server-side role-based access control, the tamper-evident audit log, and the no-custody post-quantum hybrid cryptography. A Community customer simply carries no licence token, and Community is never minted: it is the engine’s fail-open default, not a grant anyone issues.

The paid offer is Enterprise and Custom, and both are invoice-billed and sales-assisted: a quote, a purchase order, and an invoice paid by bank transfer, for an annual or a three-year term. There is no online checkout anywhere in the product. What a paid plan pays for is human work; the signed feature list a licence carries is a record of what was bought, and the engine echoes it but enforces nothing (tierToFeatures in control-plane/src/licence/issue.ts).

The volume bands below (starter, growth, business and the MSP pack) remain in the licence format and can still be minted for a Custom arrangement, so the band definitions and the support-time check described here still apply. What changed on 26 July 2026 is that they are no longer sold self-serve: the four self-serve tiers and the Stripe checkout that sold them were withdrawn, because supporting thousands of small-revenue customers does not pay for the revenue those tiers bring. Nothing needs unwinding by anyone reading this: that checkout only ever ran in test mode and its buy path was never open, so no card was charged and no subscription exists.

Tier Motion Volume band Term
Community Never minted None; the full product, unbanded No expiry
Enterprise Invoice-billed, sales-assisted Unlimited Annual or three-year, manually re-minted
Starter Not sold; band retained for a Custom arrangement 1 Cloudflare account, 50 GB protected By agreement
Growth Not sold; band retained for a Custom arrangement 3 Cloudflare accounts, 500 GB protected By agreement
Business Not sold; band retained for a Custom arrangement 10 Cloudflare accounts, 5 TB protected By agreement
MSP pack Not sold; band retained, sized by pack quantity 10 Cloudflare accounts and 500 GB protected per pack, multiplied by however many packs By agreement

A “Custom” arrangement is not a separate value on the licence token: it mints under Enterprise, or under one of the volume bands above, with bespoke invoice terms rather than a standard annual or three-year term. The token’s tier field itself only ever carries one of the six values above.

Enterprise buys services, not a feature unlock

The comment block in control-plane/src/licence/issue.ts states it directly: security is not an entitlement and never appears in the licence. The signed feature list tierToFeatures names seven services for Enterprise: audit assistance, ISO 27001 documentation support, SOC 2 evidence support, security-questionnaire support (such as UpGuard), compliance advisory, priority support, and a dedicated account manager. The Enterprise offer adds signed disaster-recovery drill evidence and a periodic backup-assurance attestation, which are described in the console’s ENTERPRISE_SERVICES list (console/src/lib/billing.ts) rather than carried in the signed token. No product capability sits behind any licence, at any tier.

A licence enforces nothing on the data or recovery path, whatever it was bought for. This is the load-bearing property of the whole model, and it is described fully under fail-open below.

Volume bands, and what “protected data” means

Each band travels on the licence token as ordinary feature strings, the same generic features list every tier already carries (LicenceClaims.features: string[] in engine/src/admin/licence.ts and control-plane/src/licence/issue.ts), so adding a band needed no change to the token’s wire format. A Growth licence, for example, carries band:growth, accounts:3 and protected-gb:500 alongside whatever service entitlements the tier names. The MSP pack scales the same two numbers by the quantity bought: three packs mint band:msp, accounts:30 and protected-gb:1500.

“Protected data” is the sum, across your downpipes, of the most recent successful run’s plaintext size, never the size of the retained archive. A downpipe that has run for years and kept every generation still counts once, at its latest snapshot size, so retention depth never affects which band you need; only the estate you are actively protecting does. “Accounts” is the count of distinct Cloudflare account ids among your bound sources, which is what actually drives support burden for an agency or a multi-account estate, not a seat count or a source count.

Neither number is metered or enforced anywhere in the product. They exist to describe what a subscription covers, and the only place they are ever checked against your real estate is support intake, described below.

Buying and activating a licence, with no command line

Activation never needs a terminal, whatever you end up on. A token reaches you one way now, and it ends with you pasting it into the console.

  1. Agree the plan and settle the invoice

    Enterprise and Custom are quoted and invoiced: a quote, a purchase order, and an invoice paid by bank transfer. The agreed term sets the licence validity. There is no online checkout in the product (control-plane/src/licence/admin.ts).

  2. The vendor mints the licence

    A vendor operator mints the token at the single authenticated mint endpoint (POST /admin/licence, handleAdminMint in control-plane/src/licence/admin.ts), gated by an admin bearer verified fail-closed and in constant time. This is the only path in the product that issues a licence at all. Until 26 July 2026 a Stripe webhook was a second minting path for the self-serve tiers; it was removed with the checkout, which means the unauthenticated mint path is gone and every licence now comes from a deliberate, authenticated act.

  3. A short claim code is emailed to you

    The purchase email address receives a short claim code of the form DWNP-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX (the DWNP prefix then thirty Crockford base32 characters), not the raw signed token. The hybrid Ed25519 and ML-DSA-87 signature makes the token itself too long to paste comfortably from a mail client, so the email carries the code and the control plane holds the token.

  4. Enter the claim code in the console

    Open your console, go to Licence and updates, then Activate, and enter the claim code. The control plane exchanges the code for your account’s current licence token at POST /licence/claim (the claim row never stores the token; it reads the account’s stored token, so a re-mint automatically serves the newest one), and the console submits that token to the engine’s verify-before-store activation. This step is identical for every tier: there is no Worker secret to set, no CLI and no redeploy (console/src/screens/licence/activation.ts). A code that is malformed, unknown or expired is refused with one uniform result, and the claim code stays usable for the licence term (older codes age out on their own) so you can re-activate after a console reset.

The console’s Activate form takes either input. The short claim code is the normal delivery, and the full signed licence token, pasted directly, is for an activation or renewal email that carried the token itself rather than a code, which an operator mint can send that way. Whichever path a token came from, the console posts it to the engine, which verifies it live against the pinned vendor key and, only if it verifies, hands it to the scheduler to store (POST /licence, handleUpdates in engine/src/admin/router-updates.ts). A token that does not verify is refused with a plain reason, and nothing is stored. The stored token wins over any deployment-time token, the same way a console-set destination wins over its deployment-time fallback (readLicence in engine/src/admin/licence.ts).

The Licence card on the console's Licence and updates screen, badged Community. Its text states that a licence never gates the data or recovery path, that backups and restores work at every tier, and that Community is the full product with security included while Enterprise adds services and assurance rather than features. It reports the estate measuring about 0.0 GB across 1 account, with a note reading no licence configured. Below, an Activate licence section explains that the engine verifies a pasted token live against the pinned vendor key and stores it in the account with no Worker secret, no command line and no redeploy. It offers an empty Claim code field showing a masked DWNP placeholder, an empty Licence token box, and an Activate button.

Renewals: nothing auto-renews

Nothing renews itself, and no payment details are held anywhere: a renewal is a fresh mint with the new term once the renewing invoice is paid, and you re-activate the renewed token exactly as you did the first (handleAdminMint in control-plane/src/licence/admin.ts). The daily renewal sweep emails a reminder about six weeks ahead of a term’s expiry and again about two weeks ahead, so this never arrives as a surprise (runRenewalSweep, control-plane/src/registry/renewal-sweep.ts).

You paste the fresh token into the console yourself, because there is no path by which the control-plane can reach into your engine. The console’s renewal cue, which surfaces an amber “Renews soon” state ahead of any paid term’s expiry (renewalNotice in console/src/lib/billing.ts), is what prompts you. Until 26 July 2026 the four self-serve tiers instead auto-renewed as a Stripe subscription, with the control-plane re-minting on each successful charge; that path went with the checkout.

A cancellation or non-renewal, at any tier, simply means no fresh token is minted. Because the licence is fail-open, a churned customer keeps their existing token until its notAfter lapses, at which point the engine answers Community and nothing on the data or recovery path changes.

The fail-open invariant

A downpipes licence is fail-open by design, at every tier. A missing licence, an expired one, a forged one, one carrying a tier name this engine predates and does not yet recognise, and even a control-plane that has vanished entirely, all degrade to the free Community tier. None of those is an error, and none blocks anything. Backups keep running and offline recovery always works, because both depend solely on the archive format and your own keys, not on this Worker (control-plane/SECURITY.md; engine/src/admin/licence.ts).

The engine’s verification path makes this concrete. Every abnormal outcome (no token, an unpinned vendor key, a malformed token, a signature that does not verify, an expired notAfter) returns the Community tier rather than throwing (verifyLicenceToken and the community(...) helper in engine/src/admin/licence.ts). A tier name the running engine does not recognise is not an error either: it is classified distinctly, as a future tier, and still falls open to Community, so an engine that predates Starter, Growth, Business or the MSP pack tolerates a licence minted for one of them safely rather than crashing on an unfamiliar string (the future-tier reason code in engine/src/admin/licence.ts). The route that reads the licence wraps the whole thing in a backstop so a licence problem can never throw out of the handler, and it answers HTTP 200 on every path, including expiry, tampering, an unrecognised tier and absence.

The licence itself is a compact, dot-joined token. The body is the canonical-JSON encoding of { account, tier, notAfter, features }, and the signature is a hybrid Ed25519 and ML-DSA-87 detached signature over exactly those body bytes. The engine verifies the token against the pinned vendor public key, and both halves of the hybrid signature must verify; neither can be stripped (hybridVerify, called from verifyLicenceToken).

The verifier is always the pinned vendor key, never a key the token asserts. The token carries no key id and no algorithm field by design, so a forged token cannot nominate its own signer. The engine resolves the pin from a deployment-time value or a baked-in vendor pin, and a token that does not verify under it falls open to Community (effectiveSignerPin in engine/src/admin/licence.ts).

Volume bands are checked once, at support intake, never by the product

The product itself never checks a band. The engine does not measure your estate against your tier to enforce anything, and nothing about band-checking phones home; it is the same fail-open, nothing-leaves-by-default posture the rest of this page describes.

What the engine does instead is compute a local estate rollup entirely within your own account: total protected bytes, records and bytes per source type, the count of distinct accounts and zones behind your bound sources, your downpipe count, and the timestamp it was computed as of. That rollup appears in three places and travels no further than those three: as a local display on the console’s Licence screen, as an estate field in the GET /admin/licence response, and as a new volumes section in the support pack (engine/src/admin/licence.ts, engine/src/admin/support.ts, console/src/screens/licence/view.ts). The first two never leave your account at all. The third does, but only when you choose to send a support pack, exactly like every other section the pack already carries.

This is not the beacon, and it is not metering

The estate rollup is unrelated to the opt-in beacon described below: the beacon is a separate, off-by-default telemetry emitter that a customer must explicitly configure before anything leaves the account, while the estate rollup is computed either way and simply sits inertly in your own console and API response until you choose to attach a support pack to a ticket. Neither one enforces usage against a limit; nothing the engine does ever blocks a backup, a restore or a console action because of volume.

A band is checked in exactly one place: support intake, and only on the tickets where a customer sends a pack. The check is deterministic, never an inference: it reads the band your licence carries and the volumes rollup your pack reports, and compares them. What happens next depends on why you are writing in, not on the numbers alone. A ticket about data loss, a restore, or a security concern is helped first regardless of whether the reported estate is over the licensed band, with any true-up handled afterwards; recovery is never held hostage to a subscription tier. A routine ticket from an over-band account gets a friendly note with the measured numbers and an invitation to talk about the right plan before deeper work proceeds. Either way, restores themselves are never gated by a band, the same fail-open invariant that governs the licence as a whole.

For exactly what the volumes section contains, field by field, see the support bundle. For the support model this check sits inside, see getting support without giving us access.

The beacon: opt-in, off by default

The beacon is designed to be the least the vendor could receive, and it is never sent unless an operator turns it on. It is content-free, fail-open and advisory, and it is a completely separate system from the estate rollup above: it carries only an opaque account tag, the engine version and the Cloudflare deploy id, plus aggregate counts (a downpipe count, a coarse healthy/stalled split, and one account-wide maximum RUNLOG index), never anything per-downpipe and never a volume or band figure, and the receiver rejects any payload that carries a per-downpipe field (it returns 422). A store failure still returns a success status, because losing a beacon degrades only an assurance view and can never touch a backup or a recovery (control-plane/src/beacon/receive.ts). When a shared ingest key is set, a beacon must present it; unset, the endpoint is closed.

The emitter lives in the engine’s cron pass (runBeaconEmitPass in engine/src/cron/beacon-emit.ts, wired at engine/src/cron/drive.ts), and it runs after the backup passes so it can never delay a backup: the seal loop that writes the backup runs near the start of the tick (runSealLoop, engine/src/cron/drive.ts), and the beacon pass runs well after it in the same file, itself followed by the digest flush, the update-available alert and the tick-outcome record. It stays off unless the operator sets both BEACON_URL and BEACON_INGEST_KEY; if either is missing, the pass returns immediately and nothing leaves the account. The engine’s posture report reflects this live: beaconEnabled (engine/src/admin/router-posture.ts) is computed from whether both are configured, not a hard-coded value.

What an enabled beacon is used for today

Once enabled, the beacon feeds the control-plane’s per-account deploy ledger (control-plane/src/beacon/deploy-ledger.ts), derived from the version-identity change between two beacons. That ledger has one internal, read-only route, GET /admin/deploy-ledger (control-plane/src/beacon/admin.ts), which the support-diagnosis flow calls to corroborate whether an independent vendor-side deploy actually happened in a fault window, before it will attribute a missing-binding fault to a deploy. There is still no customer-facing assurance dashboard; the ledger exists to support this one internal diagnosis check, not to display beacon data back to you.

This is the same honesty the rest of the docs hold to: the beacon is genuinely off until you configure it, its blast radius stays bounded to the counts and identities described above even once enabled, and it carries no volume or band figures at all; those belong to the estate rollup described above, which only ever leaves your account inside a support pack you chose to send.

Where this fits

The custody story this page leans on (no data, no keys, no token, and the offline break-glass key) is set out in full in the no-custody trust model, and the adversary-by-adversary reasoning is in the threat model. The way these claims are phrased, and the claims the docs deliberately never make, including the absence of a live assurance view, are collected in precise claims and honesty. For exactly what the support pack’s volumes section contains and how the pack protects itself in transit, see the support bundle and getting support without giving us access.

The other half of the cost answer above lives on two pages. What Cloudflare bills you for, and the account settings that are a precondition of any of it, are on the prerequisites. The calculator that projects the storage and read side of that bill once you have sources and a schedule is on predicting storage cost.

Deeper detail: the licence token, the mint endpoint and the validity window

The token format, byte for byte. A licence is <body_b64url>.<sig_b64url>. The body is base64url-no-pad over the canonical-JSON claims bytes; the signature is base64url-no-pad over the detached hybrid signature (Ed25519 then ML-DSA-87) computed over exactly those body bytes. The engine reconstructs the message as the decoded body, re-canonicalises the parsed claims, requires the re-canonicalisation to equal the signed bytes (so a non-canonical re-serialisation is rejected as malformed even if the raw signature checked), and verifies both hybrid halves against the pinned key (control-plane/src/licence/issue.ts; engine/src/admin/licence.ts).

The pinned public key. The vendor public key is ed25519(32) then ML-DSA-87 public(2592), totalling 2624 bytes, base64url-no-pad. A normal customer never pins it by hand; it is baked in when the engine is deployed and does not change between renewals or between tiers. Only a self-host administrator setting up a brand-new engine ever sets it (pinnedPublic in control-plane/src/licence/issue.ts).

How a mint happens. A licence is minted through one path: the operator-authenticated endpoint POST /admin/licence (handleAdminMint in control-plane/src/licence/admin.ts). It fails closed: an unset admin bearer or an unset signer returns 503, a missing or wrong bearer returns 401 (compared in constant time over SHA-384 digests), and an invalid body returns 400. On success it signs the claims with the pinned vendor signer, stores the token best-effort under account:<account> so the operator can re-retrieve it, optionally emails it, and returns the token. On this endpoint the tier defaults to enterprise and an unknown value is refused, so a typo can never mint a grant that does not exist; Community is never minted anywhere. Until 26 July 2026 a Stripe webhook was a second minting path, structurally limited to the four self-serve tiers; it was removed with the checkout, so this endpoint is now the only issuer.

The validity window. A licence’s notAfter is the agreed contract end date an operator sets from the invoice term. The endpoint accepts any Date-parseable string, requires it to be valid and in the future, and renders it in the engine’s RFC-3339 UTC-millis form so the signed bytes are stable (normaliseNotAfter in control-plane/src/licence/issue.ts). There is no auto-renewing subscription and no grace window: a renewal is a fresh mint with the new term.

The byte-compatibility proof. A build test mints a token with the control-plane signer and verifies it with the real engine verifier, so any drift between issuer and verifier fails the build (control-plane/SECURITY.md).

Last updated .