Skip to content

Cloudflare Turnstile

Cloudflare Turnstile is our CAPTCHA solution that replaced Google reCAPTCHA for better global performance and passive user experience.

Why Turnstile?

We migrated from Google reCAPTCHA to Cloudflare Turnstile for:

  • Global accessibility: Better performance in regions where Google has known issues (e.g., China)
  • Passive experience: Users are not actively challenged for proof of humanity under normal circumstances

Management

Dashboard Access

Turnstile configuration is managed at: Cloudflare Dashboard

There, we have two site keys:

  • IxDF-web--sandbox (for development and staging)
  • IxDF-web--production (for production)

Approved Domains

The following domains are configured:

  • interaction-design.org (production)
  • ixdf.dev (development)
  • staging.ixdf.dev (staging)
  • localhost (local development)

Implementation

Package

We use ryangjchandler/laravel-cloudflare-turnstile, maintained by Ryan Chandler (Software Engineer at Laravel).

Configuration

Keys are configured in config/services.php:

php
 'turnstile' => [
     'key' => env('TURNSTILE_SITE_KEY', '0x4AAAAAABY_Hn_2YpySXd_5'),
     'secret' => env('TURNSTILE_SECRET_KEY', '0x4AAAAAABY_HvuQ59JPdiZRHOkBoEk_Elc'),
 ],

The key is used in the frontend (is public), while the secret is used in the backend to verify the response.

NOTE

You'll notice that our keys have default values — these are sandbox keys. We don't use them in production.

Usage Locations

Right now, turnstile is implemented on the following forms:

  1. Contact forms:

    • About contact: /about/contact
    • Corporate contact: /corporate
    • Affiliate contact: /affiliate
  2. Registration flows:

    • Toolkits download: /toolkits
    • Company signup: /join/company/{slug}
  3. Authentication:

Using in HTMX forms

When using HTMX, parts of the page are dynamically updated without a full page reload. This can cause issues with Turnstile widgets, as they need to be re-rendered after DOM changes.

The following code snippet handles this situation (see contactForm.js):

javascript
handleAfterSwap() {
    // ...
    // Re-render Turnstile after DOM changes
    if (window.turnstile) {
        const turnstileElement = this.querySelector('.cf-turnstile');
        if (turnstileElement) {
            window.turnstile.render(turnstileElement);
        }
    }
}