Cloudflare Turnstile Rails
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
.
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
andjson
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
-
Include the widget in your views or forms:
<%= cloudflare_turnstile_tag %>
That's it! Though it is recommended to match your
theme
andlanguage
to your app’s design and locale:<%= cloudflare_turnstile_tag data: { theme: 'auto', language: 'en' } %>
-
For all available data-* options (e.g.,
action
,cdata
,theme
, etc.), refer to the official Cloudflare client-side rendering docs: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#configurations -
Supported locales for the widget UI can be found here: https://developers.cloudflare.com/turnstile/reference/supported-languages/
Backend Validation
Simple Validation
-
To validate a Turnstile response in your controller, use either
valid_turnstile?
orturnstile_valid?
. Both methods behave identically and return aboolean
. Themodel
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 aVerificationResponse
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 andvalid_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
-
If you’ve configured explicit mode (
config.render = 'explicit'
orcloudflare_turnstile_tag render: 'explicit'
) but widgets still auto-render, override the default container class:<%= cloudflare_turnstile_tag class: nil %>
or
<%= cloudflare_turnstile_tag class: 'my-widget-class' %>
-
By default Turnstile targets elements with the
cf-turnstile
class. For more details, see Cloudflare’s https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget.
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.