On this page

← For agents

For agents

Cloudflare-first projects

The shipped Cloudflare-line blueprints cover a whole product hosted on the edge. This page explains how they compose, where their scope ends, and when to reach for the Hetzner-line blueprints.

A blueprint is a specification package: a set of contributed RCF documents (facades, ADRs, TACs, elicits, and other chain records), a guide for the operator, and optional supporting assets or probes. This page explains how the Cloudflare blueprints compose into a product and where their scope ends. The named slugs on this page are on the shelf today unless the page explicitly calls one out as reserved or held; the full list is on the blueprints reference. Each recommendation reflects what the shipped blueprint contributes (a contract, an ADR, a probe or a capability declaration) plus an operator judgement the agent proposes before applying a blueprint. Three Hetzner blueprints ship together: deploy-hetzner-server, platform-docker-compose-host and edge-cloudflare-tunnel. The final four sections explain when to combine a Cloudflare front end with a Hetzner backend and how those blueprints fit together.

1. When to reach for the Cloudflare line

The Cloudflare-line shelf covers a product that wants the edge, not one that grew up in a VPC. Reach for it when: the surface is served worldwide with low latency and the reads dominate the writes, the workload is bursty and event-shaped rather than long-running, the storage fits a KV cache plus a durable relational store, and the operator does not want to run a Kubernetes cluster to serve a login page. The deploy-cloudflare-workers blueprint (Workers-with-static-assets) is a signal you are in this territory: a Worker plus a directory of static assets is the SPA-on-Workers shape Cloudflare itself recommends over Pages for new projects.

Do not reach for it when: the workload is a long-running process (a persistent daemon, a real background worker doing minutes of CPU per unit, a machine-learning training loop), the data fits better in a big relational store with heavy joins than in KV plus a few Postgres tables, the customer contract requires a private network with no egress, or the operator already has a mature Hetzner or on-prem estate they want the product to live on. The edge-cloudflare-tunnel blueprint is the shape that lets a Cloudflare front escape to a Hetzner backend when the shelf's Cloudflare-line pieces stop fitting.

2. The deploy foot

Two shapes ship, and the elicit picks between them.

deploy-cloudflare-workers is the entry blueprint. Bare-Worker is the shape when the project serves no static assets: the assets-directory elicit is left empty and the project's wrangler.toml carries no [assets] block. SPA-on-Workers is the shape when the project ships a build-output directory of static assets alongside the Worker: assets-directory names that directory (typically ./dist, ./build, or ./public), and run-worker-first chooses whether the Worker's fetch() handler runs before the static-assets router tries to match the URL. The blueprint contributes the wrangler contract (facade, ADRs, and a probe pack); the host project owns and writes its own wrangler.toml to match. The shipped assets-manifest-scan runner scans the shared cf-platform fixture (its exported scanner function is the one host projects wire to their own wrangler.toml). Applied to a project's manifest, it refuses when the elicits expect a truthy run_worker_first and the file does not agree, when the [assets] block is missing on an SPA project, or when the Pages-only pages_build_output_dir field is present.

Migrating from Cloudflare Pages? Follow Cloudflare's own walkthrough at developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/, apply this blueprint with assets-directory set to what the Pages project pointed at, pick run-worker-first per the project's routing shape, and drop the Pages workflow. The Workers-with-static-assets shape covers the SPA and API surfaces the Pages workflow covered.

3. Persistence choices on Cloudflare

Three durable-store blueprints and one KV facade compose here; they conflict where they should, and they do not conflict where they should not.

persistence-data-d1 is Cloudflare's own SQL store, best for a small-to-medium relational workload that fits inside a single D1 database. It is the durable store for Workers-deployed applications behind a request-scoped Worker-side facade, with numbered forward-only migrations owned by the wrangler CLI. persistence-data-postgres is the Postgres shape when D1 is too small: a facade that is the sole reader of the pg client, forward-only migrations under per-file transactions, a tokeniser-based prepared-statement conformance inventory (with two documented wrapper pass-through sites), and a two-path recovery model. When the Postgres host is off the edge, a Hyperdrive adapter slot is reserved at the driver boundary so the Worker fetch of a Postgres row does not pay the full-round-trip latency. persistence-data-sqlite is the third option, useful for a locally-persisted tool that runs on a single machine.

Any two of sqlite, d1, postgres deliberately conflict on the durable-store and the migration-discipline decisions: the add refuses and shows both sides. On a Cloudflare-line project the answer is almost always D1 or Postgres, not both.

platform-cloudflare-kv sits alongside the durable store. KV is not a substitute for the durable store; it is the cache and the low-write metadata surface (feature flags, session mappings, a small edge-visible configuration). The facade is the sole reader of the KV namespace binding, ships a cache-aside helper on top of put and get, and emits metadata-only lifecycle events so an audit trail exists without leaking values. Declares capabilities: ["keyValueStore"].

4. Scheduling and background work

Two shapes; they compose.

platform-cloudflare-cron-triggers is the shape when the project just needs a scheduled entry point on the Worker: a scheduled() handler contract with an expression-router and a skew-tolerance discipline. The probes drive wrangler dev --test-scheduled against the shared cf-platform fixture, and the shipped events (cronFired, cronSkewed, cronStalled, cronUnmatched) let the operator watch the schedule from an audit stream without pulling values through it.

jobs-background is the shape when the project needs the whole background-work vocabulary: named job definitions with a handler, an input schema, a retry policy and a timeout, plus a scheduler that takes cron expressions and one-shot delayed runs. The scheduler mode elicit picks how the timer fires: workerCron composes with platform-cloudflare-cron-triggers and shares its skew-tolerance discipline. A Workflows scheduler-mode slot is reserved at the ADR boundary for the projects that would rather lean on Cloudflare Workflows once that reaches a production shape.

Applying jobs-background first on a bare project refuses at add time with the queue-required refusal; --allow-no-queue-yet is the deliberate escape hatch that records the acknowledgement.

5. Edge security

Three blueprints, each guarding a slice of the ingress path. They compose along the request path; none of the three conflict with each other.

edge-cloudflare-turnstile belongs on any public form that mints a credential or a transactional event (magic-link mint, sign-up, contact form). The client widget is pinned to the Cloudflare Turnstile host only, the server-side siteverify runs against https://challenges.cloudflare.com/turnstile/v0/siteverify, and a refuse-if-token-missing guard fires when the client did not present a token. The pack ships pinned public test sitekeys (1x00000000000000000000AA always-pass, 2x00000000000000000000AB always-fail, 3x00000000000000000000FF forced-interactive) with their paired secrets so a CI run needs no real Turnstile credentials to exercise the deterministic branches.

edge-cloudflare-access belongs in front of admin surfaces (/admin, an ops dashboard, a runbook viewer). It is a JWT validator that is the sole reader of the Cf-Access-Jwt-Assertion header, an Access application shape, and a real-account gated-URL probe that runs accountBound-skipped without a Cloudflare account. Declares capabilities: ["zeroTrustGate"]. application-admin-console consumes the capability: when Access is applied the admin console switches its sign-in surface to the Access-gated shape (no local login form, principal read from request.auth). Absent Access, the admin console falls back to its local login unchanged.

edge-cloudflare-rate-limiting is the zone-level guard on top of both. The rate-limit manifest lives under cloudflare/rate-limits/*.json, the drift audit checks the applied Cloudflare account against the manifest, and one accountBound-skipped burst-and-429 probe covers the throttle behaviour end to end. Declares capabilities: ["edgeRateLimit"].

Together: Turnstile in front of a public form to keep the bots out, Access in front of the admin surface to keep the wrong humans out, rate-limiting as the zone-level guard on both to cap the noise from the ones that get through.

6. Follow-up adapters

Some adapters are held for later releases, so a project on the current shelf keeps working when they arrive. A Postgres-backed queue sibling (messaging-queue-postgres) is reserved for projects that would rather run everything on one database. The Hyperdrive adapter slot is reserved at the persistence-data-postgres driver boundary for projects with a Postgres host off the edge. The Cloudflare Workflows scheduler-mode slot is reserved on jobs-background for projects that want to lean on Workflows once it reaches a production shape. The deploy-cloudflare-workers shape has a follow-up planned for when the Cloudflare-side deploy verifier needs to prove a Turnstile widget was mounted on a public form. And edge-cloudflare-access will pick up an audit-log sink adapter as the audit-event vocabulary settles.

None of these are on the shelf today; naming them here is a promise about direction, not a commitment to a date. To see what your applied blueprints have contributed on your own chain, browse the tree with rcf audit view and scope its deep link to the applied blueprint of interest.

7. What NOT to reach for on Cloudflare

The Cloudflare-line shelf deliberately stops short of a few surfaces the platform offers.

Containers on Cloudflare: not on the shelf. The container product is fine, but the shelf's posture is that a project that wants a container has already left the Workers execution model, so a container blueprint is a different design conversation than the Cloudflare-line one this shelf is about.

Workers AI, Vectorize, AI Gateway: not on the shelf. Held pending a rcf-lite design decision on how a blueprint declares a model-serving dependency and where an LLM-inference gate lives on the chain. When that lands, an ai-* family is the likely home.

Cloudflare Pages: superseded by Workers-with-static-assets for new projects, per Cloudflare's own recommendation. The migrate-from-Pages walkthrough is the escape path.

Long-running processes and heavy compute: the shelf routes to Hetzner through edge-cloudflare-tunnel. The three Hetzner-line blueprints ship together and the four sections below cover the shape: a Cloudflare front-of-house with an edge-cloudflare-tunnel bridge back to a platform-docker-compose-host stack on a deploy-hetzner-server VM. If your workload is a persistent daemon, a full-fat database with heavy joins, or a training loop, this is the composition to reach for.

8. When to reach for the Hetzner line

The Hetzner line covers what the Cloudflare-line shelf deliberately does not: a persistent runtime, a compose stack of long-running services, a database or a job worker that needs a full VM, or a workload with heavy compute the edge is the wrong shape for. Reach for it when the project has outgrown Workers (the daemon does not fit inside a request lifecycle), the storage wants a real Postgres host rather than KV plus D1, the operator wants a compose file they can docker compose up on any Linux VM, or the customer contract wants a private network the tunnel gates rather than a public Workers URL.

The three Hetzner blueprints ship together because they cover one shape end to end: deploy-hetzner-server is the VM contract, platform-docker-compose-host is the compose stack on top, and edge-cloudflare-tunnel is the ingress bridge back to the Cloudflare edge. Applied together, they give the project a Hetzner origin (persistent services on a VM behind the tunnel) as a separate deploy foot from the Cloudflare-line Workers path. If the same project also applies deploy-cloudflare-workers (and accepts the trade-off of two deploy targets to reason about), a Workers front-of-house can serve the public surface while the Hetzner origin handles the long-running work; the same Zero-Trust posture applies to both sides when edge-cloudflare-access is on the chain. The Cloudflare-line shelf keeps its shape; the Hetzner line is what you compose alongside it when the edge is not enough on its own.

9. The Hetzner deploy foot

deploy-hetzner-server contributes the provisioning contract for a Hetzner Cloud server through an hcloud-shaped adapter that is the sole reader of the Hetzner API token via the applied security-secrets-management facade. It contributes a canonical manifest shape under hetzner/servers/<name>.json naming the server type, image, location and firewall attachment; a cloud-init template contract that lands the estate hardening baseline on first boot (SSH key-only, root disabled, UFW default-deny incoming, a DOCKER-USER iptables chain, a fail2ban SSH jail, unattended-upgrades); and a Hetzner Cloud Firewall contract that sits alongside the manifest. A /live HTTP probe on a private interface is contributed by the platform-docker-compose-host fixture as a compose service, not by the VM hardening baseline itself. The host project owns realising and running the provisioner; blueprint apply writes the RCF chain records and sidecars, it does not call the Hetzner API. Server-type sizing is an elicit whose current enum covers the x86 and ARM Hetzner Cloud shapes the shelf supports; cx23 and cx33 are the two guide examples the elicit description walks through, and the blueprint guide at blueprints/deploy-hetzner-server/guide/deploy-hetzner-server.md walks the operator through the sizing choice with worked examples for cx23 and cx33; treat that guide as a selection guide with examples rather than a complete vCPU/RAM/disk table for every enum value.

Snapshot cadence is a three-way elicit: weekly is the default (the guide recommends Sunday at 02:00, and the host project owns implementing the scheduled snapshot runner that fires on that cadence), daily is the reach when the data changes fast enough that a week is too long, and off is the deliberate choice reserved for the guide's stricter condition: Hetzner-managed backups are enabled and the application layer also runs its own logical backups on cadence. Off without both of those is not the right answer. Hetzner-managed backups are the elicited paid option on top of any of the three cadences.

A project usually picks one deploy foot per shape, but the CLI does not enforce a one-target-per-project rule at add time: the deploy-cloudflare-workers and deploy-hetzner-server blueprints use different global ADR topics (deploymentTarget versus linuxCloudHostContract and snapshotAndBackupCadence), so composing them together does not raise a globalAdrTopic conflict. Applying both together is possible and lets a single project run a Cloudflare front-of-house alongside a Hetzner origin, at the cost of reasoning about two deploy shapes; the operator judgement is whether that trade-off is worth carrying.

10. Running a compose stack on Hetzner

platform-docker-compose-host contributes the compose-runtime contract for the provisioned VM. Docker Engine and the Compose plugin are not installed by the deploy-hetzner-server cloud-init baseline; the host project owns the install step after the VM baseline settles (the shipped real-account fixture compose-stack-driver.mjs performs that step against a live VM, so a project can pattern its own runner on it). Once that install has run, the canonical layout is a compose.yaml at the repo root, service-per-container, named networks and volumes. Every HTTP-terminating service declares a healthcheck; the shipped compose-config-lint probe walks the fixed fixture services (web and caddy) and reports missing healthchecks on those, and host projects wire an equivalent project-wide classification into their own pipeline. Non-HTTP services are not required to declare a healthcheck. Restart discipline is restart: unless-stopped for long-running services and on-failure for one-shot jobs.

Caddy is the shipped default reverse proxy with Traefik as the elicited alternative. Coolify is not a supported option: the platform blueprint is the compose contract itself, not a wrapper around a hosted panel that keeps its own state. The reverse-proxy config is a first-class versioned artefact under caddy/Caddyfile or traefik/traefik.yaml. Verification of the served surface is a separate real-account probe (real-account-minimal-stack-up, which brings a live stack up and performs the HTTP observations); operators run it against a live stack rather than blueprint apply performing it. Reload is zero-downtime: on Caddy, run docker compose exec caddy caddy reload to pick up a config change without dropping the connection; on Traefik, docker compose kill -s USR1 traefik triggers the equivalent hot reload.

Secrets are compose file mounts, not environment variables. The applied security-secrets-management facade fetches each secret from the configured secrets manager; the compose mount contract (contributed by platform-docker-compose-host as tac-3902-platform-docker-compose-host-secrets-mount) is what pins the on-disk mode to 0o400 and lands the file under a per-project secrets directory. Each service that needs a secret mounts that file (typically at /run/secrets/<name>) and reads its bytes at startup. The shipped secrets-as-files-scan probe reports Compose services that name a secret the compose file has not declared, and plaintext-secret shapes it can spot in the compose surface. It does not inspect the secrets-facade fetch state, so a missing-fetch defect surfaces through the facade's own runtime error rather than through this probe.

Log shipping is elicited. The default is journald, which lands service logs on the VM's journal with no extra moving parts; the opt-in is loki, which ships to a Loki endpoint the project itself configures (typically in the compose file's logging driver options or via a separate log-shipper service) and pairs with the applied observability-logging sink for the audit-event and reader side. The elicit is left open on purpose: a small project that owns one VM does not need a log aggregator, but a project on the second server (or the tenth) does. Switching the log-driver elicit later requires the host project to update or regenerate each service's logging block; re-applying the blueprint at a newer version records the new elicit answer through the supported upgrade path (same-version re-application is not the intended sidecar-update workflow).

11. Bridging Hetzner to the Cloudflare edge

edge-cloudflare-tunnel contributes the ingress-bridge contract. cloudflared runs in one of two shapes: as a compose service (the default when containerHost is applied, so the tunnel lives in the same compose stack the applied services do) or as a systemd unit (the alternative for a bare cloudHost without a compose runtime, per Cloudflare's cloudflared-as-a-service documentation). The tunnel manifest lives under cloudflare/tunnels/<name>.yaml with the tunnel id, a credentials reference into the applied secrets facade, and ingress rules mapping public hostname to internal service URL with a mandatory catch-all http_status:404. The shipped manifest-schema-validate and cloudflared-config-lint runners validate the shared fixture variants; the applying project wires equivalent validation to its own manifest to exercise the same refusals (missing catch-all, wrong ingress-rule shape, internal address that is not a syntactically valid origin). DNS resolution of the internal service name is not part of the probe's job.

The public-hostname shape is the default when Access is not on the chain. Requests to the public hostname reach the tunnel connector and route to the internal service without an authentication step. Use this when the surface is meant to be public (a marketing page served from an origin the edge does not cache, an API surface the caller signs itself) and the tunnel is only there to keep the origin off the public internet.

The Access-gated shape is the composition when edge-cloudflare-access is applied. zeroTrustGate is a capability the tunnel's contract reads: when it is present, the tunnel's ingress-rule contract expects each rule to carry an originRequest.access block with the Access AUD, and the connector-side JWT check the host project stands up refuses requests without a valid Access assertion. The materialisation and the cross-field enforcement are the host project's responsibility to implement (blueprint apply writes the RCF chain records and the ADRs; it does not itself patch a cloudflared config). The Zero-Trust posture is the same on either side of the tunnel, so an admin surface that lives behind Access on Workers keeps its posture when it moves behind the tunnel to a Hetzner-hosted origin.

Hostname management is contracted rather than performed by apply. The blueprint contributes a DNS-route contract (tac-4004-edge-cloudflare-tunnel-zone-and-hostname-apply) that names the public DNS record the tunnel needs; the host project implements and executes the Cloudflare API call to create or update that record (a small wrangler-side script or a cloudflared tunnel route dns invocation typically covers it), so a working hostname lands after both the RCF apply and the operator-side DNS step. Two real-account probes (connector-healthy, tunnel-hostname-routes) ship accountBoundSkipped for now and record skipped when the account-gated variables the probe needs are unavailable: the connector-health probe needs both the Cloudflare token vault and a reachable Hetzner-side connector to exercise, and the Access-gated hostname subcase additionally needs the Access gate to be applied and its variables present. A CI run without any of those records skipped; the local probes carry the ship-time verdicts (manifest schema, cloudflared tunnel ingress validate, AUD-presence sidecar-read check).