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: Honours Rails'
content_security_policy_noncefor 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/httpandjsondependencies.
Table of Contents
- Getting Started
- Installation
- Frontend Integration
- Backend Validation
- CSP Nonce Support
- Turbo & Turbo Streams Support
- Turbolinks Support
- Internationalization (I18n)
- Overriding Translations
- Locale Fallbacks
- Adding New Languages
- Available Translation Keys
- 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
themeandlanguageto your app's design and locale:<%= cloudflare_turnstile_tag data: { theme: 'light', 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/#configuration-options -
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. Themodelparameter is optional:if valid_turnstile?(model: @user) # Passed: returns true else # Failed: returns false, adds errors to @user.errors render :new, status: :unprocessable_entity end
-
When no model is provided and verification fails,
valid_turnstile?automatically setsflash[:alert]with the error message. This is useful for redirect-based flows:def create if valid_turnstile? # Passed: no model needed redirect_to dashboard_path, notice: 'Success!' else # Failed: flash[:alert] is automatically set redirect_to contact_path end end
-
You may also pass additional siteverify parameters (e.g.,
secret,response,remoteip,idempotency_key) supported by Cloudflare's API: Cloudflare Server-Side Validation ParametersFor example, to pass a custom remote IP address:
if valid_turnstile?(model: @user, remoteip: request.remote_ip) # Passed with custom IP verification else # Failed render :new, status: :unprocessable_entity end
Advanced Validation
-
To inspect the entire verification payload, use
verify_turnstile. It returns aVerificationResponseobject with detailed information:result = verify_turnstile(model: @user)
This method adds errors to the model if verification fails, but unlike
valid_turnstile?, it does not automatically set flash messages. This gives you full control over error handling:if result.success? # Passed else # Failed — handle errors yourself flash[:error] = "Custom message: #{result.errors.join(', ')}" end
-
The
VerificationResponseobject 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 (honouring 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 Turbo navigations (turbo:render) and on <turbo-stream> renders (turbo:before-stream-render) — 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.
Internationalization (I18n)
Error messages are fully internationalized using Rails I18n. See all languages bundled with the gem.
Overriding Translations
To customize error messages, add your own translations in your application's locale files:
# config/locales/cloudflare_turnstile/en.yml
en:
cloudflare_turnstile:
errors:
default: "Verification failed. Please try again."
timeout_or_duplicate: "Session expired. Please try again."Your application's translations take precedence over the gem's defaults.
Locale Fallbacks
This gem respects Rails' built-in I18n fallback configuration. When config.i18n.fallbacks = true, Rails will try fallback locales (including default_locale) before using the gem's default message.
For example, with fallbacks enabled and a chain of :pt → :es, if a Portuguese translation is missing, Rails will automatically try Spanish before falling back to the gem's default message.
Note: If you use regional locales (e.g.,
:pt-BR,:zh-CN), you should enable fallbacks. Withoutconfig.i18n.fallbacks = true, a locale like:pt-BRwill not automatically fall back to:pt, and users will see the generic fallback message instead of the Portuguese translation.
Adding New Languages
To add a language not bundled with the gem (e.g. Yoruba), create a new locale file:
# config/locales/cloudflare_turnstile/yo.yml
yo:
cloudflare_turnstile:
errors:
default: "A ko le jẹrisi pe o jẹ ènìyàn. Jọwọ gbìyànjú lẹ́ẹ̀kansi."
missing_input_secret: "Kọkọrọ̀ ìkọkọ Turnstile kò sí."
invalid_input_secret: "Kọkọrọ̀ ìkọkọ Turnstile kò bófin mu, kò sí, tàbí pé ó jẹ́ akọsọ ìdánwò pẹ̀lú ìdáhùn tí kì í ṣe ìdánwò."
missing_input_response: "A kò fi ìdáhùn Turnstile ránṣẹ́."
invalid_input_response: "Ìbáṣepọ̀ ìdáhùn Turnstile kò bófin mu."
bad_request: "Ìbéèrè Turnstile kọjá àṣìṣe nítorí pé a kọ ọ̀ ní ọna tí kò tó."
timeout_or_duplicate: "Àmì-ẹ̀rí Turnstile ti lo tẹlẹ̀ tàbí pé ó ti parí."
internal_error: "Àṣìṣe inú Turnstile ṣẹlẹ̀ nígbà ìmúdájú ìdáhùn. Jọwọ gbìyànjú lẹ́ẹ̀kansi."Available Translation Keys
| Key | Cloudflare Error Code | Description |
|---|---|---|
default |
— | Fallback message for unknown errors |
missing_input_secret |
missing-input-secret |
Secret key not configured |
invalid_input_secret |
invalid-input-secret |
Invalid or missing secret key |
missing_input_response |
missing-input-response |
Response parameter not provided |
invalid_input_response |
invalid-input-response |
Invalid response parameter |
bad_request |
bad-request |
Malformed request |
timeout_or_duplicate |
timeout-or-duplicate |
Token expired or already used |
internal_error |
internal-error |
Cloudflare internal error |
For more details on these error codes, see Cloudflare's error codes reference.
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: 5Or, 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_taghelper 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
Explicit Rendering
-
If you've configured explicit mode (
config.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-turnstileclass. For more details, see Cloudflare's https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicit-rendering.
CSP Nonce Issues
- When using Rails' CSP nonces, make sure
content_security_policy_nonceis 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/setupRunning the Test Suite
Appraisal is used to run the full test suite against multiple Rails versions by generating separate Gemfiles and isolating each environment.
Execute all tests (unit, integration, system) across every Ruby & Rails combination:
bundle exec appraisal install
bundle exec appraisal rake testCI Note: The GitHub Action .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 rubocopCI 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.rbGet 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.rbThen:
cd test_app
bin/rails serverThis 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.