Cloudflare
One line in your existing Worker, or a thin worker in front of your origin.
DNA runs in your Cloudflare account, in front of your site. It checks each
request before your site sees it, and lets it through or returns a 403.
- Your pages go straight from Cloudflare to your site, never through D3.
- Cloudflare's WAF, Bot Management, and DDoS protection run first. DNA sees what they let through and doesn't change how they work.
Choose an install
| Where your site runs | Install | What it takes |
|---|---|---|
| On a Worker you deploy | Wrap it | One line of code |
| Anywhere else: nginx, a VPS, another cloud | The deploy button | A few clicks, no code |
| On a Worker or Pages project you can't edit | A separate worker | A small worker + service binding |
| Behind Cloudflare, and you'd rather not deploy anything | Paste a Snippet | One pasted file (paid plans) |
All installs need an API key.
Wrap your Worker
-
Install
@d3-inc/d3-edge-cloudflare-adapter:npm install @d3-inc/d3-edge-cloudflare-adapter -
Wrap your handler:
// 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 }, }); -
Set your API key as a secret and redeploy. No route changes or
ORIGINbinding are needed; your handler is the origin.npx wrangler secret put DNA_API_KEY npx wrangler deploy
Behavior:
- Blocked requests get a
403before yourfetchruns. Other requests reach it unchanged. - Non-fetch handlers (
scheduled,queue, …) are untouched. - With
DNA_API_KEYunset,withD3Edgeis a transparent no-op. See Configuration reference.
Production only
Cloudflare also serves your Worker on *.workers.dev and on per-version
preview URLs, and each one shows in Sites as its own site. Turn them off so
DNA runs on your production domain alone, which must be a route or custom
domain:
workers_dev = false
preview_urls = falseFor a staging Worker, add an environment. Wrangler doesn't copy [vars] into
one, so staging runs without DNA and answers with x-d3-edge: disabled:
[env.staging]
routes = [{ pattern = "staging.example.com", custom_domain = true }]npx wrangler deploy --env stagingThe deploy button
For sites hosted outside Cloudflare. The button deploys a small worker in front of your site from your browser. No terminal or code needed.
- Cloudflare copies the worker template into your GitHub account.
- It asks for one secret: paste your API key.
- It deploys the worker into your Cloudflare account.
- Connect the worker to your site: in the Cloudflare dashboard, go to
Workers & Pages → your worker → Settings → Domains & Routes and add a
route like
www.example.com/*. Your DNS record doesn't change, and allowed traffic continues to your site. - Verify the install.
- Everything the flow creates belongs to you.
- 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.
- To see each step, install the same worker by hand.
By hand: a separate worker
The same worker the deploy button creates. This is the only install for a site on a Worker or Pages project you can't edit.
-
Create a minimal Workers project if you don't have one:
npm create cloudflare@latest my-site-d3-edge -- --type=hello-world -y cd my-site-d3-edge -
Install the adapter:
npm install @d3-inc/d3-edge-cloudflare-adapter -
Replace the generated
src/index.tsso it re-exports the 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. This is a zone route: your DNS record doesn't change, and passed traffic reaches your existing origin with no further config.
# 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" }] -
Set your API key as a secret, not in
[vars]:npx wrangler secret put DNA_API_KEY -
Deploy with
npx wrangler deploy, then verify the install.
Origin on a Worker or Pages project
If you can edit the project, wrap it instead. Otherwise, in step 4 attach with a custom domain and a service binding:
routes = [{ pattern = "www.example.com", custom_domain = true }]
[[services]]
binding = "ORIGIN"
service = "your-pages-or-worker-project"- The worker takes over the hostname and forwards allowed traffic worker-to-worker, with no public hop.
- A custom domain replaces the hostname's DNS record, so the worker becomes the origin.
ORIGIN(orORIGIN_URL, for origins reachable only by URL) is required. Without it, passed traffic has nowhere to go.
Paste a Snippet
A Cloudflare Snippet is a single file pasted into your zone, with no Worker. Snippets are included at no extra cost on paid plans; the free tier doesn't have them. Blocking and availability work as in the worker installs.
The file and full guide: github.com/d3-inc/d3-edge-adapters/cloudflare-snippet.
- Paste
snippet.jsinto Rules → Snippets in your zone. - Set
apiKeyto your API key. - Scope it with a snippet rule like
http.host eq "www.example.com". - Deploy.
Differences from the worker:
CONFIGholds the settings in the configuration reference, asapiKey,enabled,failMode,timeoutMsandpaymentTimeoutMs.enabled: falseis the kill switch.ORIGINandORIGIN_URLdon't exist. Passed traffic continues to whatever your zone already routes to.- An agent that signs an upload over 1 MB shows as Name only. To verify uploads of any size, use a worker install.
- A Snippet's log doesn't show your site's response times and errors.
Setup (guided)
During early access, we can do the install with you. The session deploys the worker, attaches the route, and sets the secrets in your account. Everything it creates is yours to change or remove. It takes about half an hour. Book a setup.
Have ready:
- An API key for your org.
- The hostname to protect, and its origin type (Worker, Pages, or external).
- Someone with access to the Cloudflare zone.
Configuration reference
The worker reads these variables. They live in your account, including after a
guided setup. They need @d3-inc/d3-edge-cloudflare-adapter 0.9.0 or later.
| Variable | Type | Default | Description |
|---|---|---|---|
DNA_API_KEY | string (secret) | unset | Your organization's API key. Unset: pure passthrough; the worker only proxies. |
DNA_ENABLED | "false" | unset | false turns DNA off without removing the key. |
DNA_FAIL_MODE | "open" | "closed" | open | open: serve the request if no decision arrives. closed: block it. |
DNA_TIMEOUT_MS | number (ms) | 2000 | Cutoff for a decision. |
DNA_PAYMENT_TIMEOUT_MS | number (ms) | 10000 | Cutoff for a request that presents a payment, which settles inside the call. It gets the page only once paid. |
DNA_ENDPOINT | string (URL) | https://edge-api.d3.com/v1/decision | Decision endpoint. Leave unset unless we give you another. |
DNA_DEBUG_LOG_RAW_PAYLOAD | "true" | unset | Debug only: forwards every header but Authorization and Cookie for the request record. |
ORIGIN | service binding | n/a | Standalone worker only: binding to a Worker/Pages origin. Takes precedence over ORIGIN_URL. |
ORIGIN_URL | string (URL) | n/a | Standalone worker only: explicit origin URL, for origins that aren't Workers. |
Kill switch: set DNA_ENABLED = "false" (only false turns it off), or detach
the route. Before 0.9.0, unset POLICY_WORKER_URL instead. The standalone
worker becomes a transparent proxy and the wrapper a transparent no-op, so your
site never depends on DNA to serve.
Earlier variable names
Installs set up with the earlier names keep working unchanged, and a DNA_ name
wins when both are set. With the earlier names, POLICY_WORKER_URL is still
what switches DNA on. DEBUG_LOG_RAW_PAYLOAD is read only while
POLICY_WORKER_URL is set.
| Earlier name | Now |
|---|---|
POLICY_ADAPTER_KEY | DNA_API_KEY |
POLICY_SHARED_SECRET | DNA_API_KEY |
POLICY_WORKER_URL | DNA_ENDPOINT, now optional |
POLICY_TIMEOUT_MS | DNA_TIMEOUT_MS |
POLICY_PAYMENT_TIMEOUT_MS | DNA_PAYMENT_TIMEOUT_MS |
FAIL_MODE | DNA_FAIL_MODE |
DEBUG_LOG_RAW_PAYLOAD | DNA_DEBUG_LOG_RAW_PAYLOAD |
To move over, update the package to 0.9.0 or later, then rename each variable
and drop POLICY_WORKER_URL. If you turned DNA off by leaving
POLICY_WORKER_URL unset, set DNA_ENABLED = "false" first: DNA_API_KEY
switches DNA on by itself.
Response shape
Every response has an x-d3-edge header: disabled, pass, block,
fail-open, fail-closed, payment-required, payment-redirect, or
payment-stub. The payment-* values appear only on
access and pricing.
A blocked request gets a 403 with a JSON body:
{ "blocked": true, "ruleId": "…", "tier": "…", "identity": "…" }ruleId names the rule under Access that stopped the request.