A long-lived project that still receives updates
Integrates Cloudflare Turnstile into Rails applications, handling script injection, CSP-nonce support, and automatic Turbo/Turbolinks reinitialization.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

>= 5.0
 Project Readme

Cloudflare Turnstile Rails

Gem Version License Lint Status Test Status

Cloudflare Turnstile gem for Ruby on Rails with built-in Turbo and Turbolinks support and CSP compliance.

Supports Rails >= 5.0 with Ruby >= 2.6.0.

"Buy Me A Coffee"

Features

  • One‑line integration: <%= cloudflare_turnstile_tag %> in views, valid_turnstile?(model:) in controllers — no extra wiring.
  • Turbo & Turbo Streams aware: Automatically re‑initializes widgets on turbo:load, turbo:before-stream-render, and DOM mutations.
  • Legacy Turbolinks support: Includes a helper for Turbolinks to handle remote form submissions with validation errors.
  • CSP nonce support: Honors Rails’s content_security_policy_nonce for secure inline scripts.
  • Rails Engine & Asset pipeline: Ships a precompiled JS helper via Railtie — no manual asset setup.
  • Lightweight: Pure Ruby/Rails with only net/http and json dependencies.

Table of Contents

  • Getting Started
    • Installation
    • Frontend Integration
    • Backend Validation
    • CSP Nonce Support
    • Turbo & Turbo Streams Support
    • Turbolinks Support
  • Automated Testing of Your Integration
  • Upgrade Guide
  • Troubleshooting
  • Development
  • License

Getting Started

Installation

  • Add the gem to your Gemfile and bundle:

    gem 'cloudflare-turnstile-rails'
  • Run the following command to install the gem:

    bundle install
  • Generate the default initializer:

    bin/rails generate cloudflare_turnstile:install
  • Configure your Site Key and Secret Key in config/initializers/cloudflare_turnstile.rb:

    Cloudflare::Turnstile::Rails.configure do |config|
      # Set your Cloudflare Turnstile Site Key and Secret Key.
      config.site_key   = ENV.fetch('CLOUDFLARE_TURNSTILE_SITE_KEY', nil)
      config.secret_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SECRET_KEY', nil)
    end

    If you don't have Cloudflare Turnstile keys yet, you can use dummy keys for development and testing. See the Automated Testing of Your Integration section for more details.

    For production use, you can obtain your keys from the Cloudflare dashboard. Follow the instructions in the Cloudflare Turnstile documentation to create a new site key and secret key.

Frontend Integration

Backend Validation

Simple Validation

  • To validate a Turnstile response in your controller, use either valid_turnstile? or turnstile_valid?. Both methods behave identically and return a boolean. The model parameter is optional but recommended for automatic error handling:

    if valid_turnstile?(model: @user)
      # Passed: returns true
    else
      # Failed: returns false, adds errors to @user
      render :new, status: :unprocessable_entity
    end
  • You may also pass additional siteverify parameters (e.g., secret, response, remoteip, idempotency_key) supported by Cloudflare’s API: Cloudflare Server-Side Validation Parameters

Advanced Validation

  • To inspect the entire verification payload, use verify_turnstile. It returns a VerificationResponse object with detailed information:

    result = verify_turnstile(model: @user)

    This method still adds errors to the model if verification fails. You can query the response:

    if result.success?
      # Passed
    else
      # Failed — inspect result.errors or result.raw
    end
  • The VerificationResponse object contains the raw response from Cloudflare:

    # Success:
    Cloudflare::Turnstile::Rails::VerificationResponse @raw = {
      'success' => true,
      'error-codes' => [],
      'challenge_ts' => '2025-05-19T02:52:31.179Z',
      'hostname' => 'example.com',
      'metadata' => { 'result_with_testing_key' => true }
    }
    
    # Failure:
    Cloudflare::Turnstile::Rails::VerificationResponse @raw = {
      'success' => false,
      'error-codes' => ['invalid-input-response'],
      'messages' => [],
      'metadata' => { 'result_with_testing_key' => true }
    }
  • The following instance methods are available in VerificationResponse:

    action, cdata, challenge_ts, errors, hostname, metadata, raw, success?, to_h
    

CSP Nonce Support

The cloudflare_turnstile_tag helper injects the Turnstile widget and accompanying JavaScript inline by default (honoring Rails' content_security_policy_nonce), so there's no need to allow unsafe-inline in your CSP.

Turbo & Turbo Streams Support

All widgets will re‑initialize automatically on full and soft navigations (turbo:load), on <turbo-stream> renders (turbo:before-stream-render), and on DOM mutations — no extra wiring needed.

Turbolinks Support

If your Rails app still uses Turbolinks (rather than Turbo), you can add a small helper to your JavaScript pack so that remote form submissions returning HTML correctly display validation errors without a full page reload. Simply copy the file:

templates / shared / cloudflare_turbolinks_ajax_cache.js

into your application’s JavaScript entrypoint (for example app/javascript/packs/application.js). This script listens for Rails UJS ajax:complete events that return HTML, caches the response as a Turbolinks snapshot, and then restores it via Turbolinks.visit, ensuring forms with validation errors are re‑rendered seamlessly.

Automated Testing of Your Integration

Cloudflare provides dummy sitekeys and secret keys for development and testing. You can use these to simulate every possible outcome of a Turnstile challenge without touching your production configuration. For future updates, see https://developers.cloudflare.com/turnstile/troubleshooting/testing/.

Dummy Sitekeys

Sitekey Description Visibility
1x00000000000000000000AA Always passes visible
2x00000000000000000000AB Always blocks visible
1x00000000000000000000BB Always passes invisible
2x00000000000000000000BB Always blocks invisible
3x00000000000000000000FF Forces an interactive challenge visible

Dummy Secret Keys

Secret key Description
1x0000000000000000000000000000000AA Always passes
2x0000000000000000000000000000000AA Always fails
3x0000000000000000000000000000000AA Yields a "token already spent" error

Use these dummy values in your development environment to verify all flows. Ensure you match a dummy secret key with its corresponding sitekey when calling verify_turnstile. Development tokens will look like XXXX.DUMMY.TOKEN.XXXX.

Overriding Configuration in Tests

You may also directly override site or secret keys at runtime within individual tests or in setup blocks:

Cloudflare::Turnstile::Rails.configuration.site_key   = '1x00000000000000000000AA'
Cloudflare::Turnstile::Rails.configuration.secret_key = '2x0000000000000000000000000000000AA'

Controller Tests

As long as config.auto_populate_response_in_test_env is set to true (default) in cloudflare_turnstile.rb and you're using a dummy secret key that always passes, your existing controller tests will pass without changes.

If config.auto_populate_response_in_test_env is set to false, then you will need to manually include the cf-turnstile-response parameter in your test cases with any value. For example:

post :create, params: { 'cf-turnstile-response': 'XXXX.DUMMY.TOKEN.XXXX' }

This will ensure that the Turnstile response is included in the request, allowing your controller to validate it as expected.

Feature/System Tests

Assuming you're using a dummy key, you can confirm that the Turnstile widget is rendered correctly in Minitest with:

assert_selector "input[name='cf-turnstile-response'][value*='DUMMY']", visible: :all, wait: 5

Or, if using RSpec:

expect(page).to have_selector("input[name='cf-turnstile-response'][value*='DUMMY']", visible: :all, wait: 5)

This will cause the browser to wait up to 5 seconds for the widget to appear.

Upgrade Guide

This gem is fully compatible with Rails 5.0 and above, and no special upgrade steps are required:

  • Simply update Rails in your application as usual.
  • Continue using the same cloudflare_turnstile_tag helper in your views and valid_turnstile? in your controllers.
  • All Turbo, Turbo Streams, and Turbolinks integrations continue to work without changes.

If you run into any issues after upgrading Rails, please open an issue so we can address it promptly.

Troubleshooting

Duplicate Widgets

  • If more than one Turnstile widget appears in the same container, this indicates a bug in the gem—please open an issue so it can be addressed.

Explicit Rendering

CSP Nonce Issues

  • When using Rails’ CSP nonces, make sure content_security_policy_nonce is available in your view context — otherwise the Turnstile script may be blocked.

Server Validation Errors

  • Validation failures (invalid, expired, or already‑used tokens) surface as model errors. Consult Cloudflare’s server-side troubleshooting for common error codes and test keys.

Still stuck? Check the Cloudflare Turnstile docs: https://developers.cloudflare.com/turnstile/get-started/

Development

Setup

Install dependencies, linters, and prepare everything in one step:

bin/setup

Running the Test Suite

Appraisal is used to run the full test suite against multiple Rails versions by generating separate Gemfiles and isolating each environment. To install dependencies and exercise all unit, integration and system tests:

Execute all tests (unit, integration, system) across every Ruby & Rails combination:

bundle exec appraisal install
bundle exec appraisal rake test

CI Note: Our GitHub Actions .github/workflows/test.yml runs this command on each Ruby/Rails combo and captures screenshots from system specs.

Code Linting

Enforce code style with RuboCop (latest Ruby only)::

bundle exec rubocop

CI Note: We run this via .github/workflows/lint.yml on the latest Ruby only.

Generating Rails Apps Locally

To replicate the integration examples on your machine, you can generate a Rails app directly from the template:

rails new test_app \
  --skip-git --skip-action-mailer --skip-active-record \
  --skip-action-cable --skip-sprockets --skip-javascript \
  -m templates/template.rb

Get the exact command from the test/integration/ folder, where each integration test has a corresponding Rails app template. For example, to replicate the test/integration/rails7.rb test for Rails v7.0.4, run:

gem install rails -v 7.0.4

rails _7.0.4_ new test_app \
  --skip-git --skip-keeps \
  --skip-action-mailer --skip-action-mailbox --skip-action-text \
  --skip-active-record --skip-active-job --skip-active-storage \
  --skip-action-cable --skip-jbuilder --skip-bootsnap --skip-api \
  -m templates/template.rb

Then:

cd test_app
bin/rails server

This bootstraps an app preconfigured for Cloudflare Turnstile matching the versions under test/integration/.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/vkononov/cloudflare-turnstile-rails.

License

The gem is available as open source under the terms of the MIT License.