Korzen
Bot Protection
In these docs

First-party mode

The tag, its beacon and the challenge page served from your own domain, by an SDK, a Cloudflare Worker or both, for visitors whose network blocks Korzen.

Optional. A DNS filter, an ad blocker or a network that blocks Korzen's host stops the tag, its beacon and the challenge page. Those visitors still go through, judged on what your server sends alone, but on a route that needs the tag's token they cannot pass. First-party mode serves all three from a path of your own domain, /kz unless you name another, and forwards them to Bot Protection with the visitor's address. Nothing blocks a site's own path without blocking the site.

There are two ways to serve the path, and you can use both:

The wayWhat you turn onWho serves /kz
The SDK alonefirstParty: true in Node, FirstParty: true in Goyour server
The Worker aloneKORZEN_FIRST_PARTY = "1" and a route for /kz/*your Cloudflare Worker
The Worker and the SDKboth, with the same paththe Worker; your server sends challenged visitors to it

In every way, the page loads the tag from your path instead of Korzen's host. The tag takes its address from its own src, so every request it makes goes to /kz too:

<script src="/kz/tag.js" data-site-key="YOUR_SITE_KEY" async></script>

The mode is in every SDK: Node (and its Worker), Go, Python, PHP, Java and .NET, each with its option in its README. A client written from a framework guide has no forwarder: there, use the SDK or, behind Cloudflare, the Worker alone, which gives the mode to an origin in any language.

The SDK alone

Your server serves the path. Give the middleware that already asks Bot Protection the option, and give the path a handler of its own with the same options. The client must be one createClient made, with your API key: the forwarder sends it to Korzen with each request.

// Express, with korzen.js as the Express guide sets it up
const options = { client: korzen, ip: (_get, req) => req.ip, firstParty: true };

app.use("/kz", protect(options)); // before any body parser: it answers /kz/... itself
app.use(express.urlencoded({ extended: false }));
app.post("/signup", protect(options), signup);

// Next.js: app/kz/[...path]/route.js serves the path
const kz = withKorzen(() => new Response(null, { status: 404 }), { client, ip: ipFrom.vercel, firstParty: true });
export { kz as GET, kz as POST };
o := korzen.Options{IP: korzen.RemoteIP, FirstParty: true}

mux.Handle("/kz/", bot.Protect(http.NotFoundHandler(), o)) // the path: Protect answers it itself
mux.Handle("POST /signup", bot.Protect(http.HandlerFunc(signup), o))
  • What you turn on. firstParty: true on protect or withKorzen (on only when it is true); in Go, Options.FirstParty. Then the tag's src on your pages becomes /kz/tag.js. The visitor's address is read as the decision reads it: the ip option, in Go Options.IP.
  • An address the visitor cannot write. Korzen trusts the address the forwarder sends with your key, so the ip option must read one the visitor cannot set: ipFrom.vercel on Vercel alone, ipFrom.cloudflare (Go: korzen.CloudflareIP) only if your server takes traffic from Cloudflare alone, ipFrom.lastForwarded behind your own load balancer or nginx. A header the visitor can write lets a script choose the address Korzen judges.
  • What changes for the visitor. Nothing they see. Their browser loads the tag and sends its beacon to your domain, so a visitor whose network blocks Korzen is judged with the tag's evidence and can take an action that needs its token. The requests under the path are never judged and never reach your handlers; every other path is judged as before.
  • The challenge page. A challenged page load goes to /kz/challenge?c=…&r=… on your domain instead of Korzen's, and your server forwards the page, its files and its post. The same puzzle, and the visitor comes back to the page they asked for.
  • Fail open. When Bot Protection is slow, down or unreachable from your server, each request under the path answers 503 with an empty body, and the tag drops that signal; the page and your other routes wait on nothing. Fail open
  • How to tell it works. curl -I https://shop.example/kz/tag.js answers 200, and the browser's network panel shows the tag's requests going to your own domain. How to tell it works

The Worker alone

Your Cloudflare Worker serves the path, with no change to your server, for an origin in any language. Set up the Worker as Cloudflare Workers shows, then turn the mode on and add a route for the path:

# wrangler.toml
routes = [
  { pattern = "shop.example/kz/*", zone_name = "shop.example" },
  { pattern = "shop.example/signup*", zone_name = "shop.example" },
]

[vars]
KORZEN_URL = "https://korzen.dev"
KORZEN_FIRST_PARTY = "1"
# KORZEN_FIRST_PARTY_PATH = "/kz"   # the path, /kz unless set
  • What you turn on. KORZEN_FIRST_PARTY set to 1 or true (any other value but 0 or false leaves it off, with a warning in the Worker's log), a route for shop.example/kz/*, and the tag's src on your pages as /kz/tag.js. The visitor's address is CF-Connecting-IP, which Cloudflare sets.
  • What changes for the visitor. Nothing they see. The tag and its beacon go to your domain, answered at Cloudflare's edge; your origin never sees those requests. The Worker's routes are judged as before.
  • The challenge page. A page load the Worker challenges goes to /kz/challenge on your domain, which the Worker serves.
  • Fail open. The same as the SDK's: a 503 with an empty body for that request alone, and every other request goes on to your origin. Fail open
  • How to tell it works. As below; and npx wrangler tail lists the requests under /kz, while your origin's access log shows none of them. How to tell it works

The Worker and the SDK

When a Worker is in front of your site and the SDK runs in your application (for the context only your application knows), turn the mode on in both, with the same path.

  • What you turn on. The Worker as above, with its route for /kz/*, and firstParty: true (Go: FirstParty: true) on the SDK behind it. The Worker answers /kz/* itself, so the SDK never sees those requests.
  • What changes for the visitor. The same as with the Worker alone: the tag, its beacon and the challenge page come from your domain, answered at the edge.
  • The challenge page. The SDK's firstParty sends the visitors it challenges to /kz/challenge rather than to Korzen's host, and the Worker serves that page. Without it, a visitor the SDK challenges goes to Korzen's host, which their network may block.
  • Fail open. Whichever serves the path gives up on its own and answers 503; neither holds the other. If the Worker's routes leave out /kz/*, or the Worker is taken away, the SDK serves the path itself: behind Cloudflare, give it ip: ipFrom.cloudflare (Go: korzen.CloudflareIP) then.
  • How to tell it works. As below; npx wrangler tail lists the requests under /kz, and your server's log shows none of them.

What the path serves

The forwarder, the SDK or the Worker, answers these and nothing else:

On your domainWhat it is
GET /kz/tag.jsthe tag, with Korzen's caching headers
GET /kz/n, POST /kz/sthe tag's nonce and its beacon (a body of at most 64 KiB)
GET /kz/pinganswered 204 by the forwarder itself, never forwarded
GET and POST /kz/challenge, GET /kz/challenge/waitthe challenge page and its post (at most 16 KiB)
GET /kz/static/…the challenge page's script, style and fonts

Any other path under /kz is a 404, and a known one with another method a 405; neither reaches Bot Protection, so the path is never an open proxy. To Korzen the forwarder sends the browser's own headers on a short list (User-Agent, Accept, Accept-Language, Content-Type, Origin, Sec-Fetch-*, the client hints, If-None-Match), your API key as the bearer and the visitor's address. Never a cookie, the browser's Authorization or an X-Forwarded-*. What comes back keeps Korzen's caching and security headers, never a Set-Cookie.

Korzen takes the visitor's address only from a request on these routes that carries your API key, and on the beacon only for your own site key; anything else is refused 403. Your key never reaches the browser.

The tag's _kz cookie and its storage stay on your domain, as before; the data Korzen receives is the same. A few pieces of evidence Korzen reads only when the browser connects to it directly are not there through a forwarder; the rest of the tag's evidence arrives, and none of what is missing counts against a visitor.

Another path

firstPartyPath (Go: Options.FirstPartyPath; the Worker: KORZEN_FIRST_PARTY_PATH) names another path, for example once a block list learns /kz. It starts with /, ends without one, and is at most 64 characters of segments of A-Z a-z 0-9 . _ ~ -. One that does not read is logged once, and the mode stays off. Use the same path in the tag's src, in the SDK and in the Worker.

Fail open

The path never holds your page or your other routes:

  • Each forwarded request gives up on its own: after 1 s for a GET, 2.5 s for a POST. A timeout, a network error, an answer over 4 MiB or a fault in the forwarder is a 503 with an empty body and Cache-Control: no-store; redirects are not followed. Bot Protection's own 4xx and 5xx pass through.
  • With no visitor address (the ip option reads none), the nonce, the beacon and the challenge answer 503; the tag and the challenge page's files are still served.
  • The tag then drops that signal, as it does when Korzen's host is down. A tag that did not report is never evidence of a bot: the visitor is judged on what your server sends.

How to tell it works

  • curl -I https://shop.example/kz/tag.js answers 200, and curl -I https://shop.example/kz/ping answers 204.
  • In the browser's developer tools, on a page with the tag, the network panel shows /kz/tag.js, /kz/n, /kz/ping and /kz/s from your own domain, answered 200 or 204, and no request to Korzen's host.
  • Go through a protected action, then press Check your integration on the Set-up page: The tag's token in that call reads Seen. Your decisions show on the dashboard's Decisions page as before.
  • With a Content-Security-Policy, Korzen's host can leave it: script-src 'self'; connect-src 'self'; worker-src blob: is enough for the tag. The tag

Common mistakes

  • The tag still loading from Korzen's host: the mode serves the path, but only the tag's src sends the browser there.
  • A body parser before the forwarder in Express: the beacon's body is read before the forwarder gets it. Mount it before any parser, or on the path alone.
  • The Worker with KORZEN_FIRST_PARTY but no route for /kz/*: the requests reach your origin, which answers 404 unless the SDK serves the path there.
  • Two paths: the Worker on one and the SDK on another. The SDK then sends challenged visitors to a path the Worker does not serve.
  • Calling /v1/decide by hand: its challenge.url is always Korzen's host. Only the SDKs and the Worker send the visitor to your path.