D3 Edge
SDKs

Cloudflare

One line in your existing Worker, or a thin worker in front of your origin.

On Cloudflare, D3 Edge runs in your account, in front of your origin. Per request it extracts metadata, asks the decision service for a decision, then passes the request to your origin or returns a 403. It holds no state.

Exactly two calls leave your zone per request: the decision call, which is blocking and bounded by the timeout budget, and a fire-and-forget report of the outcome afterward, which never affects response time (the Snippet install makes only the first). Pass-traffic flows from your zone to your origin without touching our infrastructure.

Which install is yours?

One question decides it: where does your site run?

Where your site runsHow you installWhat it takes
On a Worker you deployWrap itOne line of code
Anywhere else — nginx, a VPS, another cloudThe deploy buttonA few clicks, no code
On a Worker or Pages project you can't editA separate workerA small worker + service binding
Behind Cloudflare, and you'd rather not deploy anythingPaste a SnippetOne pasted file (paid plans)

In every case, pass means your site serves exactly as before, and block answers 403 before the request reaches it.

Your zone's own security products are unaffected: Cloudflare runs WAF, Bot Management, and DDoS protection before any Worker, so D3 Edge only sees traffic they already passed, and changes nothing about how they behave.

Wrap your Worker

Your site already runs on a Worker, so you don't need another one. Install @d3-inc/d3-edge-cloudflare-adapter and wrap your handler:

npm install @d3-inc/d3-edge-cloudflare-adapter
// src/index.ts — the only code change
import { withD3Edge } from '@d3-inc/d3-edge-cloudflare-adapter';

export default withD3Edge({
  async fetch(request, env, ctx) {
    // your existing worker, unchanged
  },
});

Blocked requests get their 403 before your fetch runs; everything else reaches it exactly as before, and non-fetch handlers (scheduled, queue, …) are untouched. Add the config to your existing wrangler file — no route changes, and no ORIGIN binding, since your own handler is the origin:

[vars]
POLICY_WORKER_URL = "https://edge-api.d3.com/v1/decision"

Then set your API key as a secret and redeploy your worker as you normally would:

npx wrangler secret put POLICY_ADAPTER_KEY
npx wrangler deploy

The kill switch works the same wrapped: unset POLICY_WORKER_URL and withD3Edge is a transparent no-op around your handler.

The deploy button

Your site is hosted outside Cloudflare, so a small worker has to sit in front of it inside Cloudflare. The button builds that worker for you — no terminal, no code:

Deploy to Cloudflare

The whole flow happens in your browser, and everything it creates belongs to you:

  1. Cloudflare copies our worker template into your GitHub account.
  2. It asks for one secret, POLICY_ADAPTER_KEY — have an API key ready.
  3. It deploys the worker into your Cloudflare account.

The new worker isn't connected to your site yet. Connect it in the Cloudflare dashboard: Workers & Pages → your worker → Settings → Domains & Routes, add a route like www.example.com/*. Your DNS record stays exactly what it is; allowed traffic flows on to your site like before. Then verify it worked.

One thing to know: every request through the worker counts against your Cloudflare Workers plan. The $5/month plan covers 10 million requests; the free tier caps at 100k a day, which a busy site can hit.

Want to see every step? Install the same worker by hand.

By hand: a separate worker

The same worker the button deploys, step by step — and the only path when your site runs on a Worker or Pages project you can't edit. Create a minimal Workers project if you don't have a spare one:

npm create cloudflare@latest my-site-d3-edge -- --type=hello-world -y
cd my-site-d3-edge

Install the adapter and re-export it as the worker's entrypoint, replacing the generated src/index.ts:

npm install @d3-inc/d3-edge-cloudflare-adapter
// src/index.ts
import worker from '@d3-inc/d3-edge-cloudflare-adapter';

export default worker;

Replace the generated wrangler config with one that attaches to your hostname. The shape below is a zone route: your DNS record stays exactly what it is, and pass-traffic flows on to your existing origin with no further configuration:

# wrangler.toml
name = "my-site-d3-edge"
main = "src/index.ts"
compatibility_date = "2025-06-01"

routes = [{ pattern = "www.example.com/*", zone_name = "example.com" }]

[vars]
POLICY_WORKER_URL = "https://edge-api.d3.com/v1/decision"

If your site runs on a Worker or Pages project you can't edit (if you can, wrap it instead), attach with a custom domain and a service binding — the worker takes over the hostname and forwards allowed traffic worker-to-worker, with no public hop:

routes = [{ pattern = "www.example.com", custom_domain = true }]

[[services]]
binding = "ORIGIN"
service = "your-pages-or-worker-project"

A custom domain replaces the hostname's DNS record — the worker becomes the origin — so with this shape the ORIGIN binding (or an ORIGIN_URL var, for origins reachable only by URL) is not optional: without one, pass-traffic has nothing behind it to reach.

Set your API key as a secret — it belongs in the secret store, not in [vars]:

npx wrangler secret put POLICY_ADAPTER_KEY

Deploy with npx wrangler deploy, then verify it worked. Every variable the worker reads is in the configuration reference below.

Paste a Snippet

No Worker at all: a Cloudflare Snippet is a single file pasted into your zone, included at no extra cost on every paid plan (the free tier doesn't have Snippets). Same decision, same 403, same fail-open.

The file and its full guide live in the adapters repo: github.com/d3-inc/d3-edge-adapters/cloudflare-snippet. In short: paste snippet.js into Rules → Snippets in your zone, fill in adapterKey with your API key, scope it with a snippet rule like http.host eq "www.example.com", and deploy. Its CONFIG maps one-to-one onto the configuration reference — an empty policyWorkerUrl is the kill switch, and ORIGIN/ORIGIN_URL don't exist: pass-traffic continues to whatever your zone already routes to.

One edge case: signed request bodies over 1 MB — an agent uploading a large file, which is rare — classify as unverified rather than proven here. If large signed uploads matter to your site, use a worker install, which confirms bodies of any size.

Setup (guided)

Prefer a second pair of eyes? During early access we also do the installation with you, because a worker in front of your production hostname deserves one. The setup deploys the worker, attaches the route, and sets the secrets, all inside your account; everything it creates is yours to change or remove. Book a setup; it takes about half an hour. Have ready:

  1. An API key for your org.
  2. The hostname to protect, and what its origin is (Worker, Pages, or external).
  3. Someone with access to the Cloudflare zone.

Configuration reference

The worker reads these. Worth knowing even on a guided setup, since it all lives in your account and you can change any of it:

VariableTypeDefaultWhat it does
POLICY_WORKER_URLstring (URL)unsetDecision endpoint. Unset means pure passthrough: the worker just proxies.
POLICY_ADAPTER_KEYstring (secret)unsetYour org's key, bearer for the decision call.
POLICY_TIMEOUT_MSnumber (ms)150Hard budget for the decision call. Guided installs set 500; lower it if you want a tighter budget.
FAIL_MODE"open" | "closed"openopen: pass traffic when the call fails. closed: block instead.
ORIGINservice bindingn/aStandalone worker only: binding to a Worker/Pages origin. Takes precedence over ORIGIN_URL.
ORIGIN_URLstring (URL)n/aStandalone worker only: explicit origin URL, for origins that aren't Workers.

The kill switch is built in: unset POLICY_WORKER_URL (or detach the route) and the standalone worker is a transparent proxy — the wrapper a transparent no-op. Your site never depends on us to serve.

Response shape

Every response carries an x-d3-edge header: disabled, pass, block, fail-open, fail-closed, or payment-required. A blocked request gets a 403 with a JSON body:

{ "blocked": true, "ruleId": "…", "tier": "…", "identity": "…" }

ruleId is absent when the block came from your org's default action rather than a specific rule.

On this page