Guide
Running the Cloudflare edge connector
Deploy the redirect-only Worker on your own Cloudflare account, set the allowlist before the queue goes on, verify it is live, read its log, and detach it in one step.
Last updated
The edge connector is a small Cloudflare Worker that runs in your Cloudflare account, attached to a route in front of your store. Per request it either forwards the original, unmodified request to your own origin, or answers for the queue: a redirect to the waiting room for a navigation, or a 503 with an x-dropify-queue header for a data fetch. Pass verification is a local signature check against keys it pulls from us — no call to us on the hot path — and any error, unreachable brain or missing policy fails open to your origin.
Three values configure it and nothing else: BRAIN_URL, TENANT, and the ADAPTER_KEY secret. There is deliberately no origin URL and no proxying mode. If a topology seems to need one, the edge connector does not support that topology; use the plugin instead.
The sold topology: the edge sheds the crowd and admits; the WooCommerce plugin spends the pass at the money. Edge-only is fine for browse protection. It is not a complete commerce gate — see Running both adapters for any store that takes payment.
Prerequisites
- Your store’s DNS zone is on Cloudflare, and the store’s hostname is a proxied (orange-cloud) record. Routes require this, and there is no DNS-only fallback by design.
- A Cloudflare account with Workers enabled — yours, not ours. Check before deploying:
npx wrangler whoami. - The Wrangler CLI. There is no build step: the connector is plain JavaScript modules that Wrangler bundles itself.
- The connector package, which you receive from us.
1. Get the credentials
In the console open Install the adapter and use the Cloudflare tab. It shows the Store ID (your TENANT), the adapter key (the ADAPTER_KEY secret) and the brain URL (BRAIN_URL), and a copyable wrangler.toml snippet with the [[routes]] block already filled in with your zone.
Paste the snippet, then npx wrangler secret put ADAPTER_KEY. Apply the WooCommerce allowlist preset in the console and add your office IP for /wp-admin — the preset covers login, cron, assets and webhooks; admin POSTs need the IP allowlist.
2. Configure wrangler.toml
cp wrangler.toml.example wrangler.toml
Fill in BRAIN_URL and TENANT under [vars], then the route:
[[routes]]
pattern = "shop.example.com/*"
zone_name = "example.com"
Two rules, both load-bearing:
- A route, never a Worker Custom Domain. The passthrough works because Cloudflare guarantees a route cannot be the target of a same-zone
fetch(). A Custom Domain has no such protection and the Worker would fetch itself in a loop. - Whole-zone (
host/*) is recommended. Rule changes in the console then take effect with no route edit. Narrow per-path routes (shop.example.com/checkout*) keep the Worker out of other traffic, but every newly gated path needs a route edit — and anything not on a route is invisible to the connector entirely.
Two more lines in the file that are easy to skim past:
-
compatibility_date. It selects the Workers runtime semantics your Worker runs under. Keep it as shipped unless you have a reason, and change it deliberately if you ever do — it is a real runtime change, not cosmetics. -
[[kv_namespaces]] binding = "DROPIFY_KV"— optional, and recommended. It makes the policy rollback floor durable. The Worker refuses a validly signed policy older than the newest it has accepted, which is what stops anyone who could answerBRAIN_URLfrom replaying an older policy to silently downgrade gating. Without KV that floor lives only in each warm isolate, so every freshly spawned isolate starts at floor zero and will accept a replayed older policy until it sees a newer one — and a drop spawns isolates in every Cloudflare location at once. Create the namespace once and paste its id:npx wrangler kv namespace create DROPIFY_KVThen uncomment the three lines in
wrangler.toml. Nothing else changes.
3. Set the allowlist before the queue goes on
This step is not optional. Under whole-shop gating — the default — the connector default-denies every un-passed request on the route, and it has no concept of WordPress execution context. That includes /wp-login.php, /wp-admin/*, the WP-Cron loopback, and every stylesheet, script, font and image the store serves.
The plugin needs none of this: WordPress never runs the plugin’s gate for static files, /wp-json, admin-ajax.php, wp-cron.php or xmlrpc.php, and /wp-admin is excluded outright. This is an edge-only concern.
Non-English store? Set the checkout path first. The Worker recognises a classic checkout submission by its path, and it only knows /checkout. If your checkout page is /afrekenen, /kassa, /kasse, /caisse — or anything else — enter it under Store settings → Checkout path(s) in the console before the queue goes on. It travels in the signed policy and makes a POST to that page protected commerce at the edge, exactly like /checkout; it also stops the allowlist from accepting an entry that covers it. Unset means the English default only. A store running the WooCommerce plugin is covered at the origin regardless.
What it costs to skip
A logged-in staff member is queued out of their own admin — and worse. Every gated subresource on an admin page is answered by the edge, and a navigation-shaped one is answered with a redirect to the waiting room. Browsers follow redirects for subresources, so one admin page load can become one waiting-room render per asset, topped up by the WordPress heartbeat every fifteen seconds. In the browser console it shows as a wall of Refused to apply style … MIME type (‘text/html’), Refused to execute script …, and CORS failures on every heartbeat request and webfont.
The connector’s navigation classifier stops the amplification — a subresource is not a navigation, so it gets a same-origin 503 that never touches us. That protects the queue. It does not make wp-admin work. Only the allowlist does that.
The list
Every entry below is accepted by the console’s validator, which executes candidate patterns against a corpus of protected requests at save time and refuses anything that can move product:
| Pattern | Method | Why |
|---|---|---|
^/wp-login\.php$ | any | Login. Cannot reach a cart. |
^/wp-admin | GET | Admin reads. Method-unrestricted is refused — it would also cover POST /wp-admin/admin-ajax.php?action=woocommerce_add_to_cart. |
^/wp-json | GET | REST reads. Method-unrestricted is refused — the Store API lives here. |
^/wc-api/ | any | Payment-gateway webhooks. Inbound notifications; they cannot mutate a cart. |
^/wp-cron\.php$ | any | Loopback cron. |
^/xmlrpc\.php$ | any | Better still, block it at Cloudflare — it is an attack surface, not a feature. |
^/\.well-known/ | any | ACME, Apple and Google association files. |
\.(?:css|js|mjs|jpg|jpeg|png|gif|webp|avif|svg|ico|woff|woff2|ttf|eot|map|txt)$ | any | Static assets. Anchored with $, and matched against the path without the query, so it cannot reach /?wc-ajax=add_to_cart. |
An allowlist entry does not save you a Worker invocation — the match runs inside the Worker. It buys correctness, not cost.
Minimising Worker request volume
If Worker request volume or cost is a concern, you can add Worker: None routes in the Cloudflare dashboard for static asset directories.
Do not use extension wildcards like
*.jsor*.cssat the route level. Cloudflare route patterns match against the entire URL including the query string. A route likeshop.example.com/*.js→ Worker: None can be exploited by appending?v=123.jsor?bypass=.jsto any protected checkout or product URL — Cloudflare matches the wildcard first and the request never meets the queue.Likewise
shop.example.com/wp-admin/*→ Worker: None ungatesPOST /wp-admin/admin-ajax.php?action=woocommerce_add_to_cart. In an edge-only deployment without the WooCommerce plugin, that exposes the legacy add-to-cart endpoint.
Only path-anchored directory routes for static assets are safe:
| Route pattern | Target | Why it is safe |
|---|---|---|
shop.example.com/app/uploads/* | Worker: None | Static images and media only; cannot reach a commerce endpoint. |
shop.example.com/wp-includes/css/* | Worker: None | Core CSS files only. |
shop.example.com/wp-includes/js/* | Worker: None | Core JS files only. |
shop.example.com/wp-includes/fonts/* | Worker: None | Core font files only. |
shop.example.com/* | Worker: dropify-edge | Everything else: page views, REST, AJAX, cart and checkout. |
The in-Worker allowlist matches paths without query strings, which is why the extension pattern in the table above is safe there and not at the route level.
What must never go in it
These are the entries a WordPress-shaped allowlist usually grows, and each one opens the purchase flow:
- “Exempt every non-GET request.” That is every checkout POST, add-to-cart form post and Store API checkout. The allowlist cannot express it — entries restrict to methods, never exclude them — and the validator would refuse it anyway.
- Anything with
?wc-ajax=.?wc-ajax=add_to_cartand?wc-ajax=checkoutare the commerce endpoints. - Anything with
?rest_route=.?rest_route=/wc/store/v1/cart/add-itemis the plain-permalink route to Store API commerce, and it is in the validator’s corpus for that reason. - The checkout page itself, under any name. The connector knows
/checkout; a store whose checkout page is/afrekenen,/kassaor/caissemust tell the console so (Store settings → Checkout path(s)), which puts the path into the signed policy and into the validator’s corpus. Until the field is set, a non-English store’s classic checkout POST is ordinary traffic to the Worker and an allowlist entry covering it is accepted. The WooCommerce plugin’s settings page shows the value to enter, under its Cloudflare connector card. /graphql. Headless GraphQL storefronts are not supported, and the console refuses any allowlist entry that reaches the endpoint. Under whole-shop gating the endpoint simply requires a pass like everything else. If you saved a^/graphqlentry before this rule existed, remove it — the validator only runs on save.
The safety net behind all of this: the connector classifies a request as protected commerce before the allowlist is consulted, so no entry can exempt a request it recognises as commerce. That bounds the damage of a too-broad entry to whatever the classifier does not recognise — which is why the two exceptions above matter.
4. Deploy
npx wrangler whoami # YOUR account
npx wrangler secret put ADAPTER_KEY # paste the adapter key; never in [vars], never committed
npx wrangler deploy
5. Verify it is live
Three independent checks, in increasing strength:
-
The Worker is in the request path.
npx wrangler tail dropify-edgeand load any page on the routed host — each request appears as a tail event even when nothing is logged (the connector logs onlyqueueandfail-opendecisions, never allows). -
We have seen it. The Worker pulls its verification keys on its first request, and every authorised pull counts as a heartbeat for the connector. In the console, Install the adapter → Verify the connection → Run check should show
edge, not stale. -
It actually gates. With the queue enabled in the console, from a cookie-less client:
# Navigation-shaped: expect 302 with a Location on your waiting-room host curl -sI -H 'accept: text/html' https://shop.example.com/ | sed -n '1p;/^location/Ip' # Data-fetch-shaped: expect 503 + x-dropify-queue + retry-after: 5 curl -sI -H 'accept: application/json' https://shop.example.com/ | sed -n '1p;/^x-dropify-queue/Ip;/^retry-after/Ip'Both shapes prove something is gating, but neither says which layer answered: the WooCommerce plugin emits the same
503and header for a data fetch, on purpose, so a storefront never has to know what is in front of it. On a store running both, tell the layers apart by their own evidence — Running both adapters shows how. While the queue is off there is no externally visible marker at all: the passthrough is your original request and your untouched origin response, byte for byte.
What “redirect-only, same-zone passthrough” means for your DNS and origin
- You change nothing. No DNS records move, the origin IP is never exposed or configured anywhere, and TLS terminates at Cloudflare exactly as before. The allow path forwards the request to the zone’s origin as your existing DNS defines it, without re-invoking the Worker.
- Queued traffic never reaches your origin. The redirect or
503is manufactured at Cloudflare, so during a drop the origin sees only admitted shoppers plus allowlisted requests. This is the performance shape that makes the connector the right answer for high-traffic drops. - Origin responses pass through untouched, including your origin’s own
Set-Cookieheaders. The Worker only appends its own pass cookies when a rebind or sliding re-issue minted a fresh pass. - The origin must be the zone’s origin. Anything reached by changing the destination — a different host, a load balancer outside the zone, an unproxied record — is out of scope by design; that store is plugin-only.
Reading wrangler tail, and what a fail-open spike means
The connector emits one JSON line per decision that acted. Secrets and key-named fields are scrubbed and IPs truncated:
{"component":"edge","reqId":"…","route":"/product/x","action":"queue","drop":"default","tenant":"…"}
{"component":"edge","level":"error","reqId":"…","route":"/checkout","action":"fail-open","tenant":"…","error":"…"}
npx wrangler tail dropify-edge --search fail-open # only fail-opens
npx wrangler tail dropify-edge --search '"action":"queue"'
fail-open means the request threw and was passed to your origin ungated. Isolated lines are noise; a spike during a drop is gating silently off for the affected isolates. What to check, in order:
- Are we reachable?
curl -si "$BRAIN_URL/v1/policy?tenant=$TENANT" | head -1from anywhere — the artifact pulls are plain HTTPS. A cold isolate that cannot complete its first pull serves with no policy at all until we answer; warm isolates ride out an outage on their last-known-good copy. - The
errorfield. It carries the thrown message; a repeated identical string is a bug to report to us, not an outage. - Rate limits. The policy endpoint is rate-limited per store, far above what a normal fleet of isolates needs, but a misbehaving retry loop can hit it.
Two documented caveats to keep in mind while reading an incident: a cold isolate starts with no policy (it fails open) and with a rollback floor of zero — the floor is in-memory per isolate unless you bound the KV namespace, so cold starts briefly re-open the policy-replay window. Both are deliberate trades in favour of your store staying up.
Rolling back or disabling
The connector is fail-open and holds no state your store depends on, so every exit is safe:
- Detach instantly: delete the route in the Cloudflare dashboard (Workers Routes on the zone). Traffic flows exactly as before the connector existed. Re-attach by re-adding the route or re-running
npx wrangler deploywith the[[routes]]block present. - Roll back a bad version:
npx wrangler rollback --name dropify-edge, or redeploy a previous copy. - Remove entirely: delete the route, then
npx wrangler delete --name dropify-edge. - After an adapter-key rotation: the old key stops authenticating; run
npx wrangler secret put ADAPTER_KEYwith the new value. Until then the Worker cannot refresh its verification keys. With a still-valid signed policy and an empty key set it fails open — it admits everyone, never blocks — and logs a warning inwrangler tail. Update the secret promptly so gating resumes.
What this deployment does not give you
The connector has no notion of pass spending, the pending-order window, or the order-creation backstop — that whole lifecycle exists only in the WooCommerce plugin — and its commerce classifier is a compiled description of WooCommerce’s wire protocol, not a hook into it. The combination that covers both directions is Running both adapters.