# Cloudflare Turnstile With Clojure

> Published  Jun 06 2025, last updated Feb 02 2026  
> By Ryan Fleck <hello@this-site> and written without LLMs!  
> Original post at <https://ryanfleck.ca/2025/cloudflare-turnstile-with-clojure/>  
> An article of astonishing quality and insight. Happy Hacking!


A common way to counteract the infernal swarm of bots, crawlers, and
other inhuman entities on the internet is to implement a **captcha**
system to protect features on your website. Google's *reCAPTCHA*,
*ALTCHA*, *hCaptcha*, and other systems all function similarly.
Regardless of the choices you make, all follow a similar
implementation pattern for securing forms:

1. Using JavaScript, add a challenge field to the form
2. On submit, this data is included in the form as a 'token' field
3. On the backend, POST the token and other data to verify it
4. If the token is valid, allow the user to continue

Note that some of the systems above return a score instead of a
pass/fail value.

## Register with Cloudflare

First, navigate to the **Turnstile** page in your Cloudflare account
and create a new widget. After entering your hostname[^2] and a few
other options, you be provided with a **site key** and **secret key**
that both look like long, random strings of numbers and letters. The
following links will also be provided, which have good documentation
on the two halves of this process:

1. [Client side integration code](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/)
2. [Server side integration code](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)

**Crucially,** know that you can use `localhost` as a valid domain.[^4]
Create a separate widget for your development environment to implement
and learn the technology, and don't share keys with your production
environment.

## Add Turnstile Code to Frontend

Because I typically use HTMX[^7] with `hx-boost`, which does not
evaluate page scripts each time the user navigates to a different
page, I need to make additional calls to activate turnstile widgets
after a user decides to login or signup.

```html
<script>
  document.addEventListener('htmx:after-swap', function(e) {
    if(e.target === document.body){
      const captchaElement = document.querySelector("div.cf-turnstile");
      if (typeof turnstile !== "undefined" && captchaElement){
        turnstile.render(captchaElement);
      }}});
</script>

<!-- Key: Load the Cloudflare Turnstile script -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js"></script>
```

A similar approach will need to be taken for a PWA.[^3]

## Add Turnstile Element to Form

The following element (selected above) can be included within your
`<form>` tag, and assuming you are using a templating system like
selmer,[^1] this is where to insert the **site key** from Cloudflare.
I like it right before the submission button.

```html
<div
  class="cf-turnstile"
  data-sitekey="{{ captcha_site_key }}"
  data-size="flexible"
  theme="light"
  ></div>
```

This is all that is required to present a challenge to your users and
ensure the token field (`cf-turnstile-response`) is included in form
submissions!

## Handle Captcha Token on Backend

Now that our frontend is including the token field in our login and
signup forms, let's handle that tocken on our site's backend.
Fundamentally, we are simply sending the token to Cloudflare to
confirm that the user successfully completed the challenge - and
didn't maliciously insert an additional field called `cf-turnstile-response` with
random data. A simple `POST` request with `clj-http.client` will do
the trick.

Here is a full additional namespace you can drop into your project to
validate Cloudflare tokens:

```clojure
(ns <your-site>.cloudflare
  (:require
   [clj-http.client :as client]
   [site.config :refer [env]]    ; <- We pull the secret key from the site configuration
   [clojure.data.json :as json]
   [clojure.string :as s]
   [clojure.tools.logging :as log]))

(def cf-url "https://challenges.cloudflare.com/turnstile/v0/siteverify")

;; Read the response and check for the "success" key from Cloudflare
(defn handle-response [res]
  (let [body (json/read-str (:body res))]
    (boolean (get body "success" false))))
    ;; -> Struct expects 'true' or 'false' as a validation result.

;; POST the token and your secret to Cloudflare for validation
(defn validate-token [token]
  (handle-response
   (client/post cf-url
                {:headers {"Content-Type" "application/json"}
                 :accept "application/json"
                 :body (json/write-str
                        {:response (or token "")
                         :secret (:cf-turnstile-secret-key env)
                         :idempotency_key (java.util.UUID/randomUUID)})})))
                         ;; Providing an idempotency_key ensures tokens can't be re-used
```

If you are using the `struct`[^5] library to validate your forms[^6],
the `validate-token` function written above can be called as an
additional validation function for the `cf-turnstile-response` field.
Demonstrated below is a method for merging an additional field
validation with your current rules, checking the Cloudflare token
without disturbing your existing validation structures.

```clojure
(defn- with-cf-field [validation-schema]
    (vec (concat validation-schema
        [[:cf-turnstile-response
           st/required
           st/string
           {:message "Capcha check failed."
            :validate #(cloudflare/validate-token %)}]])))

;; For example, if you wanted to validate a form with album info:

(def album-schema
  [[:band st/required st/string]
   [:album st/required st/string]
   [:year st/required st/number]])

;; You could call:

(with-cf-field album-schema)

;; Which returns the equivalent of:

[[:band st/required st/string]
 [:album st/required st/string]
 [:year st/required st/number]
 [:cf-turnstile-response
  st/required
  st/string
  {:message "Capcha check failed."
   :validate #(cloudflare/validate-token %)}]]
```

Apart wrapping your calls to `(st/validate)` with the `with-cf-field`
function created above, **no additional changes to your login or
signup backend logic are required!** Enjoy your security-enhanced
forms, now slightly more protected against bots!



[^1]: "Selmer, A fast, Django inspired template system in pure Clojure." [github.com/yogthos/Selmer](https://github.com/yogthos/Selmer)

[^2]: The hostname of the production website.

[^3]: PWA *"Progressive Web Application"* - referring to a single-page web application made with React or another JavaScript framework.

[^4]: "Allow localhost or 127.0.0.1 as acceptable domains for Turnstile" [community.cloudflare.com](https://community.cloudflare.com/t/allow-localhost-or-127-0-0-1-as-acceptable-domains-for-turnstile/423897/3)

[^5]: "Struct, A structural validation library for Clojure(Script)." [github.com/funcool/struct](https://github.com/funcool/struct)

[^6]: "Luminus: Input Validation" [luminusweb.com/docs](https://luminusweb.com/docs/input_validation.html)

[^7]: "HTMX: High power tools for HTML" [htmx.org](https://htmx.org/)



> Thank you for reading!  
> Find more content at <https://ryanfleck.ca/>  
> Source page: <https://ryanfleck.ca/2025/cloudflare-turnstile-with-clojure/>  
> Site index: [llms.txt](https://ryanfleck.ca/llms.txt)