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 way | What you turn on | Who serves /kz |
|---|---|---|
| The SDK alone | firstParty: true in Node, FirstParty: true in Go | your server |
| The Worker alone | KORZEN_FIRST_PARTY = "1" and a route for /kz/* | your Cloudflare Worker |
| The Worker and the SDK | both, with the same path | the 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: trueonprotectorwithKorzen(on only when it istrue); in Go,Options.FirstParty. Then the tag'ssrcon your pages becomes/kz/tag.js. The visitor's address is read as the decision reads it: theipoption, in GoOptions.IP. - An address the visitor cannot write. Korzen trusts the address the forwarder sends with your key, so the
ipoption must read one the visitor cannot set:ipFrom.vercelon Vercel alone,ipFrom.cloudflare(Go:korzen.CloudflareIP) only if your server takes traffic from Cloudflare alone,ipFrom.lastForwardedbehind 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
503with 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.jsanswers200, 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_PARTYset to1ortrue(any other value but0orfalseleaves it off, with a warning in the Worker's log), a route forshop.example/kz/*, and the tag'ssrcon your pages as/kz/tag.js. The visitor's address isCF-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/challengeon your domain, which the Worker serves. - Fail open. The same as the SDK's: a
503with 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 taillists 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/*, andfirstParty: 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
firstPartysends the visitors it challenges to/kz/challengerather 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 itip: ipFrom.cloudflare(Go:korzen.CloudflareIP) then. - How to tell it works. As below;
npx wrangler taillists 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 domain | What it is |
|---|---|
GET /kz/tag.js | the tag, with Korzen's caching headers |
GET /kz/n, POST /kz/s | the tag's nonce and its beacon (a body of at most 64 KiB) |
GET /kz/ping | answered 204 by the forwarder itself, never forwarded |
GET and POST /kz/challenge, GET /kz/challenge/wait | the 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 aPOST. A timeout, a network error, an answer over 4 MiB or a fault in the forwarder is a503with an empty body andCache-Control: no-store; redirects are not followed. Bot Protection's own4xxand5xxpass through. - With no visitor address (the
ipoption reads none), the nonce, the beacon and the challenge answer503; 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.jsanswers200, andcurl -I https://shop.example/kz/pinganswers204.- In the browser's developer tools, on a page with the tag, the network panel shows
/kz/tag.js,/kz/n,/kz/pingand/kz/sfrom your own domain, answered200or204, 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
srcsends 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_PARTYbut no route for/kz/*: the requests reach your origin, which answers404unless 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/decideby hand: itschallenge.urlis always Korzen's host. Only the SDKs and the Worker send the visitor to your path.