🧩 BladeCAPTCHA
MIT 9 KB No tracking

This page details how to integrate and configure BladeCAPTCHA on your website.

🕵️ Privacy

Does not track, does not use cookies, and does not connect to third-party services.

Accessibility

Compatible with screen readers, keyboard navigation, and low visual contrast.

💻 Self-hosted

No dependency on external APIs. Everything runs on your server.

📦 Requirements

📁 Installation

  1. Download the full package from GitHub or clone the repository.
  2. Upload the contents to your server.
  3. Rename config/config.sample.php to config/config.php and edit the CAPTCHA_SECRET_KEY value, which is the secret key used to validate tokens. (More info about this configuration here.)
mv config/config.sample.php config/config.php
nano config/config.php

🚀 Basic usage

Import the captcha.js module and call initCaptcha() with the desired configuration:

<script type="module">
import { initCaptcha } from './captcha.js';
(async () => {
  try {
    await initCaptcha({
      mode: 'autoFormIntegration',
      // other parameters...
    });
  } catch (err) {
    console.error(err || err.message);
  }
})();
</script>

⚙️ Configuration (initCaptcha)

The initCaptcha(options) function accepts the following parameters:

Key Type Req / Opt Description
mode 'manualHandling' or 'autoFormIntegration' Required Defines whether verification is started manually (without direct form integration) or if it intercepts form submission to automatically add a hidden field with the token value.
formSelector string Required if mode = 'autoFormIntegration' CSS selector for the form to intercept.
inputName string Required if mode = 'autoFormIntegration' Name of the hidden field where the token will be injected.
statusSelector string Optional Selector of the element where the CAPTCHA status will be displayed as text. CSS classes applied during execution:
  • .loading: While waiting for server response.
  • .success: When verification succeeds.
  • .error: In case of failure.
  • .info: During numeric progress of the challenge.
verifyButtonSelector string Required if mode = 'manualHandling' Selector of the button that starts manual verification.
submitButtonSelector string Required if mode = 'autoFormIntegration' Selector of the submit button.
apiBaseUrl string Optional (default: '../php') Base URL for backend API calls (PHP endpoints). Useful when using servers on other domains or custom paths; ensure backend CORS allows the frontend domain (see configuration).
onStart function Optional Callback executed when the process starts. Ideal to disable buttons or show a spinner.
onEnd function Optional Callback to revert visual state after challenge ends or is canceled.
onProgress function(p: number) Optional Callback to report verification progress (0–100).
manualHandlingAutoStartOnLoad boolean Optional 🚀 Only for manualHandling. If true, verification starts immediately when the page loads.
onSuccess function(token: string) Required for manualHandling Callback executed upon successful verification. Receives the token for server validation. 🔑 Essential for manual integration, since the token is not automatically injected.
onError function(err: Error) Optional Callback executed when an error occurs during verification. Receives an Error object with message for diagnosis.

🔀 Example: autoFormIntegration

▶️ View demo

<script type="module">
import { initCaptcha } from './captcha.js';

(async () => {
  try {
    await initCaptcha({
      mode: 'autoFormIntegration',
      apiBaseUrl : '../php',
      formSelector: '#contactForm',
      inputName: 'captcha_token',
      submitButtonSelector: '#submitBtn',
      onStart: () => {
        document.querySelector('#submitBtn').textContent = 'Verifying...';
      },
      onEnd: () => {
        document.querySelector('#submitBtn').textContent = 'Submit';
      }
    });
  } catch (err) {
     console.error(err || err.message);
  }
})();
</script>

🖐️ Example: manualHandling

▶️ View demo

<script type="module">
import { initCaptcha } from './captcha.js';

(async () => {
  try {
    await initCaptcha({
      mode: 'manualHandling',
      apiBaseUrl : '../php',
      verifyButtonSelector: '#checkHuman',
      statusSelector: '#captchaStatus',
      manualHandlingAutoStartOnLoad: false,
      onStart: () => {
        console.log('Starting verification...');
      },
      onEnd: () => {
        console.log('Verification canceled or completed.');
      },
      onProgress: (p) => {
        console.log(`Progress: ${p}%`);
      }
    });
  } catch (err) {
    console.error(err || err.message);
  }
})();
</script>

💡 Auxiliary variables and shared context

When using initCaptcha as an imported ES6 module, each function call runs in an isolated environment and internal variables are not accessible externally. Therefore, if you need to preserve information between callbacks—like the original text of a button before changing it—you should define an auxiliary variable in your code and explicitly use it in the functions passed in the configuration.

▶️ View demo

import { initCaptcha } from './captcha.js'; // or correct path

// Define memo in the module or script context
let memo = null; // Auxiliary variable for onStart and onEnd

(async () => {
  try {
    await initCaptcha({
      // other parameters...
      onStart: () => {
        const btn = document.querySelector('#submitBtn');
        memo = btn.textContent;  // save original text in memo
        btn.disabled = true;
        btn.style.cursor = 'wait';
        btn.textContent = 'Please wait...';
      },
      onEnd: () => {
        const btn = document.querySelector('#submitBtn');
        btn.textContent = memo;  // restore original text from memo
        btn.disabled = false;
        btn.style.cursor = 'pointer';
      }
    });
  } catch (err) {
    console.error(err || err.message);
  }
})();

🔄 Example: autoFormIntegration with multiple forms

▶️ View demo

<script type="module">
import { initCaptcha } from './path/to/captcha.js';

// Array of form selectors that will use BladeCAPTCHA
const forms = ['#contactForm', '#loginForm', '#newsletterForm'];

forms.forEach(async (formSelector) => {
  // Auxiliary variable to store the original submit button text
  let memo;

  try {
    await initCaptcha({
      mode: 'autoFormIntegration',
      apiBaseUrl : '../php',
      formSelector: formSelector,
      inputName: 'captcha_token',
      submitButtonSelector: `${formSelector} button[type="submit"]`,
      onStart: () => {
        const btn = document.querySelector(`${formSelector} button[type="submit"]`);
        if (!btn) return;
        memo = btn.textContent;
        btn.textContent = 'Verifying...';
        btn.disabled = true;
        btn.style.cursor = 'wait';
      },
      onEnd: () => {
        const btn = document.querySelector(`${formSelector} button[type="submit"]`);
        if (!btn) return;
        btn.textContent = memo || 'Submit';
        btn.disabled = false;
        btn.style.cursor = 'pointer';
      }
    });
  } catch (err) {
    console.error(err || err.message);
  }
});
</script>

🔧 Key technical features

🎨 Customizable

You can easily adjust its appearance, visibility, and behavior.

📱 Adaptive Execution

The challenge execution is locally adjusted according to the device’s capabilities.

❄️ Lightweight

Only 9 KB of minified JavaScript, including workers.

🤖 Hard for bots

Algorithms designed to resist automated attacks.

💸 Free

100% cost-free, no premium plans.

📜 MIT License

You can use it in personal or commercial projects without restrictions.

🎯 Execution Adjustment

BladeCAPTCHA dynamically optimizes the execution parameters of its proof-of-work challenge—such as the number of parallel workers and the size of computation batches—based on the device's hardware capabilities.

This optimization ensures that devices with lower processing power, such as mobile phones, can complete verification swiftly, while maintaining a high level of security against bots.

The entire calculation and adjustment process occurs locally within the browser. No hardware information or performance metrics are ever sent to external servers, ensuring complete user privacy.

📁 Configuration file (PHP)

BladeCAPTCHA allows defining some general behavior parameters from a simple PHP file.

This file defines constants and configuration variables used by the library during token validation. For security reasons, it is recommended to place it outside the public directory (e.g., in BladeCAPTCHA/config/config.php).

Example:

<?php
// BladeCAPTCHA/config/config.php
define('CAPTCHA_SECRET_KEY', '3b7e151628aed2a6abf7158809cf4f3c');
define('CAPTCHA_DIFFICULTY', 4);
define('CAPTCHA_EXPIRY', 300); // 5 minutes

// CORS configuration for AJAX requests from frontend
$config['cors_enabled'] = true; // Enable/disable CORS handling
$config['cors_allowed_origins'] = [ // Allowed origins for CORS (do not use '*')
    'http://localhost',
    'http://127.0.0.1',
    'http://[::1]'
];
$config['cors_allow_credentials'] = true; // Allow cookies and CORS credentials

Available parameters:

If CAPTCHA_SECRET_KEY is not defined, no token can be validated.

🛡️ Server-side verification (PHP)

After the browser completes the challenge, the generated token is sent along with the form. Your backend must verify this token to ensure it is valid and recent.

Step 1: include the library

require_once __DIR__ . '/captcha-lib.php';
use function Captcha\validateToken;

Step 2: validate the received token

This example checks the CAPTCHA token and, if valid, continues processing the form.

<?php
// BladeCAPTCHA/public/php/process-form.php
require_once __DIR__ . '/captcha-lib.php';
use function Captcha\validateToken;

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit('Method not allowed');
}

// Sanitize and validate the captcha token (always as string)
$token = trim((string)($_POST['captcha_token'] ?? ''));

function render($msg) {
    exit("<!DOCTYPE html><meta charset='UTF-8'><body>$msg</body>");
}

// Validate format and authenticity
if (!preg_match('/^[a-f0-9]{32}$/i', $token) || !validateToken($token)) {
    render(' Invalid CAPTCHA.');
}

if (!isset($_POST['name'])) {
    render(' Missing "name" field.');
}

//  Valid CAPTCHA
echo "<!DOCTYPE html><meta charset='UTF-8'><body><h1>CAPTCHA correct</h1><pre>";
foreach ($_POST as $k => $v) {
    printf("<strong>%s:</strong> %s\n",
        htmlspecialchars($k, ENT_QUOTES, 'UTF-8'),
        htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8')
    );
}
echo "</pre></body>";

Best practices

  • Always validate the token on the server.
  • If using manualHandling with manualHandlingAutoStartOnLoad: true, carefully consider whether it makes sense to also include verifyButtonSelector. While automatic start and the verify button can coexist, it is usually redundant in terms of usability.
  • Keep in mind that the token has a short expiry (~60 seconds) and is single-use.
  • Make sure there is a minimum interval of 10 seconds between each challenge started by the same user before starting another one, to prevent the server from rejecting the request.

Frequently Asked Questions

🧵 What if the browser does not support Web Workers?

BladeCAPTCHA requires Web Workers to execute the proof-of-work challenge without blocking the interface. If the browser does not support them, validation cannot complete and the form will be rejected. Currently, there is no alternative mode built-in.

🚫 What if the user disables JavaScript?

The system relies on JavaScript to generate and solve the challenge. Without JavaScript, the token cannot be generated and the submission will be blocked.

🌐 Does BladeCAPTCHA work offline?

Yes, since it is self-hosted and does not depend on external services, BladeCAPTCHA can work even in offline environments as long as the local server is available.

How long is a token valid?

Each token is valid for approximately 60 seconds and can only be used once.

📝 Can I use it on multiple forms?

Yes, you can protect multiple forms on the same page. However, there must be a minimum interval of 10 seconds between challenges.

🔄 What happens if the user closes or reloads the page during a challenge?

If the page is reloaded or closed before completing the challenge, the generated token is lost and a new one must be generated when submitting the form again.

🎨 Can the CAPTCHA appearance be customized?

Yes, BladeCAPTCHA is fully customizable via CSS and configuration options, allowing you to adapt its appearance and behavior to your site's needs.

⚙️ How to adjust challenge difficulty?

To set the difficulty, change the CAPTCHA_DIFFICULTY value in config/config.php.

🔒 Does the system store personal data?

No. BladeCAPTCHA does not store, track, or send personal information to third parties.

🖼️ Why doesn’t BladeCAPTCHA use images or visual challenges?

To ensure accessibility and privacy, BladeCAPTCHA avoids images, sounds, or videos. Instead, it relies on a computational proof-of-work challenge that is transparent to the user and compatible with assistive technologies.

☁️ Why not use CDN or external services?

BladeCAPTCHA avoids loading scripts from CDNs or external services to prevent dependency on third parties that may collect data, use cookies, or compromise user privacy and security.

🛠️ Can I integrate it without backend validation?

No. BladeCAPTCHA requires your backend to validate the received token to ensure the challenge was solved correctly and within the allowed time.

🌍 Does it work if frontend and backend are on different servers or domains?

Yes, the BladeCAPTCHA JavaScript module can be used even if frontend and backend are on different servers or domains. For AJAX (fetch) requests to work correctly in cross-origin environments, BladeCAPTCHA includes built-in configuration for handling CORS (Cross-Origin Resource Sharing).

This means the PHP backend can automatically send the necessary HTTP headers (Access-Control-Allow-Origin, Access-Control-Allow-Credentials, etc.) according to the settings in config.php ($config['cors_enabled'], $config['cors_allowed_origins'], etc.).

However, it is the developer's responsibility to correctly define allowed origins in cors_allowed_origins to maintain security, and adjust other CORS options as needed.

If using BladeCAPTCHA on the same domain or subdomain as the backend, usually no additional configuration is required.

🔑 How to get the token generated by BladeCAPTCHA in my code?

It depends on the configured integration mode:

  • manualHandling: the token is received as an argument in the onSuccess(token) callback when the challenge is completed.
  • autoFormIntegration: the token is automatically inserted into a hidden field named according to inputName inside the protected form.

In both cases, this token must be sent to the backend to be validated using validateToken().

🐍 Can BladeCAPTCHA be used without PHP?

Currently, BladeCAPTCHA works with PHP, but support for Python and other backends will be available soon.

🤖 Why is it called BladeCAPTCHA and what does the logo represent?

The name pays homage to Blade Runner, where the Voight-Kampff test (inspiring our inverted turtle logo) acts as a biological CAPTCHA to distinguish humans from replicants. This philosophical reference—drawing from Carl Jung archetypes that influenced Philip K. Dick—reflects our approach: a challenge evaluating not only responses but how they are generated.

📊 Comparison with other CAPTCHA systems

Feature BladeCAPTCHA reCAPTCHA hCaptcha
GDPR/CCPA compliance Yes No Partial
Privacy Does not collect personal data Sends data to Google Sends data to third parties
Dependency on external services No, runs entirely on your server without CDN or external APIs Yes, loads scripts and depends on Google APIs Yes, depends on CDN and third-party APIs
Accessibility High, no visual challenges Low Medium
Customization Full via CSS/HTML None Limited
Dual integration mode Flexible: automatic or manual Automatic only Automatic only
Performance Uses Web Workers to avoid blocking Undocumented / unconfirmed No Web Workers
Self-hosted Yes, runs entirely on your server No No
Open source 100% Open Source and no external APIs Proprietary, depends on Google Proprietary, depends on third parties
License MIT Proprietary Proprietary
Cost Free Free with limits Free / Paid
Mandatory credits No Yes Yes (free plan)

Support the project

BladeCAPTCHA is a 100% free and self-funded project. If you found it useful and want to help cover hosting costs, improvements, and support, you can buy me a coffee . Any contribution is welcome!

Buy a coffee