CopyDisable

Wednesday, 26 August 2026

GateCHA Hands-On Guide

GateCHA is a self-hosted CAPTCHA management platform designed to block bot spam and automated form submissions on websites without annoying human visitors. It is self-hosted alternative to ALTCHA Sentinel, a paid cloud SaaS.

Unlike traditional CAPTCHA solutions that ask users to solve image puzzles or decipher distorted text, GateCHA leverages Proof-of-Work (PoW) computational challenges. When a user submits a form, their web browser silently solves a light mathematical puzzle in the background. For a real user, this process takes a split second and is completely invisible. For automated spam bots trying to submit thousands of forms at scale, these computational challenges quickly become too expensive to process, forcing them to stop the spam attacks.

Third-party CAPTCHA providers often track user IP addresses and browsing behavior for advertising profiles. GateCHA is self-hosted, meaning no data is sent to external third parties.

GateCHA integrates into a web application using a simple three-step architecture: 

1) GateCHA instance Setup, 

2) Web App's Frontend Integration, and 

3) Web App's Backend Verification.


In this hands-on demo, we are going to deploy GateCHA on a Ubuntu 24.04 server. I will provide simple code for a demo web-application, where we are going to have a simple HTML form as a client application, which going to capture user information and submit the data to a backend API. I will provide backend code for both Python and GoLang, but I will only show the backend deployment process for the Python API. 

GateCHA relies on an embedded SQLite database (MySQL also supported) and ships as a lightweight container, deploying it via Docker on Ubuntu 24.04 is the cleanest and most reliable method.

For this example we are going to use a VM with IP 10.2.11.116. In the code you will see this IP, you can replace this IP with your IP or domain name. 

Step 1: Installation 

I am running the below commands as root user:

Update package lists: apt update && apt install -y curl git jq build-essential

Install Docker and Docker Compose: 

curl -fsSL https://get.docker.com -o get-docker.sh

sh get-docker.sh


Install GoLang:

# wget https://go.dev/dl/go1.22.2.linux-amd64.tar.gz

# rm -rf /usr/local/go && tar -C /usr/local -xzf go1.22.2.linux-amd64.tar.gz

# echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile

# source ~/.profile


Install Docker and Docker Compose: 

# curl -fsSL https://get.docker.com -o get-docker.sh

# sh get-docker.sh


Install Python:

We are going to write the backend API using Python, so we are installing it.

# apt install python3-dev

# apt install software-properties-common

# apt install python3.12-venv


Install Nginx webserver:

Our client application (HTML form) will be served using Nginx webserver.  

# apt install nginx


Step 2: Setup GateCHA

The GateCHA docker image is fully self-contained, that means we don't need to spin up separate database.

Create a new directory for the project: 

# mkdir ~/gatecha 
# cd ~/gatecha

Create a docker-compose.yml file: 
# nano docker-compose.yml

Paste the following configuration into the file:

services:
  gatecha:
    image: ghcr.io/upellift99/gatecha:latest
    container_name: gatecha
    ports:
      - "9090:8080"
    volumes:
      - gatecha_data:/app/data
    environment:
      - GATECHA_ADMIN_PASSWORD=devpassword
volumes:
  gatecha_data:


We are going access the GateCHA dashboard using the port 9090. So the GateCHA dashboard URL will be http://10.2.11.116:9090/.
The default admin username is admin and we specified the password as devpassword (in docker-compose.yaml)

Save the file and start the container in the background: 

# docker compose up -d




Step 3: Create API key















Once the container is up, we can open the dashboard using a webbrowser.

After login we can see the dashboard.














Right now we do not have any API key for our web application. A GateCHA API key is a unique, site-specific security token. GateCHA authenticates form widgets and server-side verification requests using the API key.










We can create new API key by clicking the Create Key button. 





















Explanation of important fields for creating key:

  • Domains: To prevent someone from taking our API key and using it on their own website, GateCHA allows us to configure Domain Restrictions in the dashboard. Once we restrict the key to our production domain (e.g., myapp.com), the backend will reject any verification requests originating from unauthorized domains.
  • Difficulty: When we configure the Difficulty (or maxNumber) for a new API key in GateCHA, we are setting the maximum possible computational effort required by the user's browser to solve the proof-of-work challenge.
    Some use cases for setting this value:
    • Medium / Standard (Recommended)
      • Value: 100,000
      • Use Case: Standard user registrations, login pages, and general forms.
      • Experience: It takes less than 1 second on modern laptops and smartphones. It is enough to deter most cheap spam bots while keeping the experience completely frictionless for legitimate users.
    • Low / Fast
      • Value: 10,000 to 50,000
      •  Use Case: Low-risk applications or internal tools where user experience must be instant.
      •  Experience: Solves almost instantaneously on any device. It stops simple bots that don't execute JavaScript, but determined botnets might still brute-force it easily.
    • High / Strict
      •  Value: 500,000 to 1,000,000
      •  Use Case: High-abuse targets like password resets, payment gateways, or if our site is actively experiencing a bot attack.
      •  Experience: Highly secure. While a modern desktop might solve this in 1–3 seconds, an older budget smartphone might take 5–10 seconds. If a user clicks submit and the button spins for 10 seconds, user experience may suffer.
    • Best Practice: Start with 100,000. If we notice spam getting through, increase it to 250,000 or 500,000. We can change this value on the server at any time without having to touch our frontend HTML code.
  • HIS Sampling: HIS stands for Human Interaction Signature. It is an advanced feature in the ALTCHA (and GateCHA) protocol that acts as a behavioral biometrics collector to detect automated environments and bots.  Rather than just relying on the mathematical proof-of-work challenge, HIS analyzes how a user physically interacts with our webpage before they submit the form.


Once we click the Create Key button, the new API key is created:










We will use the Key ID in our client application and HMAC Secret will remain in the server.



Step 4: Integrating GateCHA

Now that we have installed GateCHA and created our API key, it’s time to integrate it into our web application.

GateCHA works silently in the background using Proof-of-Work (PoW). Let’s look at how the verification process works under the hood.


The 4-Step Verification Flow:

To understand how GateCHA protects our application, think of the verification process as a 4-step conversation between our webpage (Client application), our server (Backend application), and the GateCHA server:

  1. Requesting a Challenge: When a user visits our form with GateCHA widget, the GateCHA widget automatically calls our GateCHA server to request a math challenge.
  2. Solving the Puzzle: The user's web browser solves the puzzle in the background using JavaScript. This usually takes less than a second on modern devices.
  3. Submitting the Payload: Once solved, the widget appends a hidden input field containing the completed solution (called the altcha payload) into our form. When the user clicks Submit, this payload is sent to our application backend alongside their form data (e.g., name, email).
  4. Backend Verification: Before processing the submitted form data, our application backend sends this payload back to GateCHA to ask, "Is this math solution real, valid, and unexpired?" If GateCHA confirms with ok: true, our backend allows the user to proceed.


Client-Side Integration (Frontend)


Adding GateCHA to a HTML forms requires only two things: 
1) importing the official lightweight web component script and 
2) adding the <altcha-widget> HTML tag.

Step 1: Add the JavaScript Script

Add the following script tag inside the <head> of our HTML document.

    Note: Because ALTCHA uses ES Modules, ensure we include type="module".


<script type="module" async defer src="https://cdn.jsdelivr.net/npm/altcha/dist/altcha.min.js"></script>


Step 2: Add the widget to our form

Place the <altcha-widget> tag inside our HTML <form> element wherever we want the CAPTCHA checkbox to appear:

<form action="/submitdata" method="POST">
    <!-- User Input Fields -->
    <label for="email">Email Address:</label>
    <input type="email" id="email" name="email" required>
    <!-- GateCHA Widget -->
    <altcha-widget 
        challengeurl="http://10.2.11.116:9090/api/v1/challenge?apiKey=gk_96fccfb465545285bc3a3bc0"
        hidelogo
        hidefooter>
    </altcha-widget>

    <button type="submit">Submit</button>
</form>


Key Widget Options:
  • challengeurl: Points to the GateCHA server’s challenge endpoint, with the generated apiKey for the site.
  • hidelogo: (Optional) Removes the ALTCHA logo icon for a cleaner look.
  • hidefooter: (Optional) Hides the bottom footer text inside the widget.

My example implementation of client page:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Registration Form</title>
    <!-- Import the ALTCHA web component -->
    <script type="module" async defer src="altcha.min.js"></script>
    <style>
        body { font-family: sans-serif; padding: 20px; max-width: 400px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; }
        input[type="text"], input[type="email"], input[type="tel"] { width: 100%; padding: 8px; }
        button { padding: 10px 15px; background: #007bff; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>

    <h2>User Registration</h2>
   
    <!-- Form submits to our Python API -->
    <form action="http://10.2.11.116:9999/submitdata" method="POST">
        <div class="form-group">
            <label for="name">Name:</label>
            <input type="text" id="name" name="name" required>
        </div>
       
        <div class="form-group">
            <label for="email">Email:</label>
            <input type="email" id="email" name="email" required>
        </div>
       
        <div class="form-group">
            <label for="phone">Phone Number:</label>
            <input type="tel" id="phone" name="phone" required>
        </div>

        <!-- GateCHA/ALTCHA Widget -->
        <div class="form-group">
            <altcha-widget
                challengeurl="http://10.2.11.116:9090/api/v1/challenge?apiKey=gk_96fccfb465545285bc3a3bc0"
        hidelogo
                hidefooter>
            </altcha-widget>
        </div>

        <button type="submit">Register</button>
    </form>

</body>
</html>


Note: Web browsers require a Secure Context (HTTPS or localhost) to run the browser cryptography functions required by GateCHA. 
Otherwise we will get error like:

Uncaught (in promise) Error: Web Crypto is not available. Secure context is required (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).

Suppose we are testing from a different local machine on the network and cannot use localhost or HTTPS, we can temporarily tell our browser to treat the local IP as secure.

For Chrome we can do this as follows:
  • Open a new tab in Chrome or Edge and paste this into the URL bar:
    chrome://flags/#unsafely-treat-insecure-origin-as-secure
  • Enable the setting.
  • Add our local server's URL into the text box.
  • Click Relaunch at the bottom of the screen to restart the browser.
  • Reload the form page. The Web Crypto API will now work.











Server-Side Integration (Backend)

When the form is submitted, our backend receives all standard form fields plus the altcha hidden payload field.

To prevent malicious users from tampering with form submissions, our backend must verify this payload with GateCHA before trusting the user.

Backend Verification Logic

Regardless of what language our backend is written in (Python, Go, Node.js, PHP), the verification steps remain identical:
  1. Read the altcha field from the incoming request form data.
  2. If missing, return an error (400 Bad Request).
  3. Send a POST request to our GateCHA server at /api/v1/verify?apiKey=YOUR_API_KEY containing {"payload": "..."}.
  4. Inspect GateCHA's JSON response. If ok is true, accept the submission. Otherwise, reject it (403 Forbidden).

Example Implementation (Python / Flask)

from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

# GateCHA Configuration
GATECHA_VERIFY_URL = "http://127.0.0.1:9090/api/v1/verify"
GATECHA_API_KEY = "gk_96fccfb465545285bc3a3bc0"

@app.route('/submitdata', methods=['POST'])
def submit_data():
    # 1. Extract standard form fields
    name = request.form.get('name')
    email = request.form.get('email')
    phone = request.form.get('phone')
   
    # 2. Extract the CAPTCHA payload
    altcha_payload = request.form.get('altcha')

    print("Payload: ", altcha_payload)

    if not altcha_payload:
        return "Error: CAPTCHA is missing or incomplete.", 400

    # 3. Verify the payload with our GateCHA server
    verify_url = f"{GATECHA_VERIFY_URL}?apiKey={GATECHA_API_KEY}"
   
    try:
        verify_response = requests.post(
            verify_url,
            json={"payload": altcha_payload},
            timeout=5
        )
       
        verify_result = verify_response.json()
        print(verify_result)
        # 4. Check if verification was successful
        if verify_response.status_code == 200 and verify_result.get("ok"):
            # CAPTCHA passed! Process the user registration here
            print(f"New Registration: {name}, {email}, {phone}")
            return f"Registration successful for {name}!"
        else:
            # CAPTCHA failed (expired, tampered, or reused)
            # Print the actual response from GateCHA to our server console for debugging
            print(f"GateCHA rejected payload. Status: {verify_response.status_code}, Response: {verify_result}")
            return "Error: CAPTCHA verification failed.", 403
           
    except Exception as e:
        print(f"GateCHA connection error: {e}")
        return "Internal Server Error during CAPTCHA validation.", 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9999)



This is the example webpage:
























When we click on the "I'm not a robot" button, the altcha widget sends request to provide a math challange or puzzle.

















If we check the response we can the JSON object returned by the GateCHA server








{

    "algorithm": "SHA-256",

    "challenge": "ce66813a276e9c91f8cbd2e798e2f7f1b5d8092af43f21d770f8797be4ae0735",

    "maxNumber": 100000,

    "salt": "1caa882d1b86cf565bead696?expires=1787662905\u0026",

    "signature": "a7853808cb78de93f5218d78c040d8b59598a4e284cc2352fb43c4aab446b985"

}


The JSON block is a hidden math puzzle send to the browser by the GateCHA server.

Here is what each piece of the puzzle means:

  1. algorithm: "SHA-256": This tells our browser to use SHA-256 hashing function to solve the puzzle.
  2. maxNumber (The Guessing Limit): 100000: To solve the puzzle, our browser has to guess a secret number. This field tells the browser: "The secret number is somewhere between 0 and 100,000." The browser will rapidly count up and check each number until it finds the right one.
  3. salt (The Unique Puzzle Piece & Timer): "1caa88...expires=1787662905&": This is a random string of characters combined with an expiration timestamp. It guarantees that every single puzzle generated is unique and can't be solved ahead of time. Once the timestamp passes, the puzzle is useless.
  4. challenge (The Target Answer): "ce66813a...": This long string of characters is the final "target.". The browser's job is to take the salt, attach its guessed number to it, and run it through the algorithm (SHA-256). If the result matches this exact challenge string, the browser has found the correct number.
  5. signature (The Anti-Cheat Seal): "a7853808...": This is a digital seal created by the server. It prevents hackers from cheating.



How the system remains cryptographically secure

The API Key: It simply tells the GateCHA server which configuration (difficulty level, expiration time, etc.) to use to generate the math puzzle. To prevent someone from taking our site's API key and using it on their own website, GateCHA allows us to configure Domain Restrictions for an API key. Once we restrict the key to our production domain (e.g., myapp.com), the backend will reject any verification requests originating from unauthorized domains.


Puzzle Metadata: GateCHA relies on mathematical Proof-of-Work.To solve a math puzzle, our browser needs the instructions. 

The server generates a random secret number (let's call it n). It sends the browser the salt and the resulting final hash, which is the challenge.

The client-side JavaScript must run a loop to guess n using the following logic:

Hash(salt + guess) = challenge

The client guesses 0, 1, 2... up to the maxNumber until the hashes match. 

Because there is no shortcut in cryptography to reverse a hash, the bot is forced to spend time and CPU cycles doing the math.

Once found, it sends the solved number, along with the original challenge, salt, and signature, back to the backend server.


The Signature: If the attacker can see the instructions, what stops them from changing the maxNumber to 1 so the puzzle solves instantly?

The security relies entirely on the signature field. When the GateCHA server generates the puzzle, it signs the salt and challenge using a Private Secret Key that never leaves the server. This creates the signature

If a bot tampers with the maxNumber, modifies the salt, or alters the challenge, the signature will instantly become invalid. When our backend sends this tampered payload to the /verify endpoint of GateCHA, the GateCHA server will check the signature, realize it was forged, and return a 403 Forbidden.


How the Signature is Generated:

When the GateCHA server receives a request for a new challenge, it performs the following cryptographic operations:

  • Generate the Puzzle: The server creates a random salt (which includes the expiration timestamp) and selects a random secret number n between 0 and the maxNumber.
  • Compute the Challenge: The server hashes the salt and the secret number together to create the target challenge:
    challenge = SHA-256(salt + n)
  • Sign the Challenge: To prevent tampering, the server uses our private HMAC Secret (let's call it K) to sign the newly created challenge using a Hash-based Message Authentication Code (HMAC):
    signature = HMAC-SHA-256(K, challenge)

    The HMAC Secret generated during the API key creation is the private cryptographic key used to generate that exact signature field.
The server then packages the salt, challenge, maxNumber, and signature and sends them to the client. It discards the secret number n entirely.


GateCHA server verification: When our backend sends this data to GateCHA, GateCHA immediately takes the submitted challenge and re-runs it through the HMAC equation using its private HMAC Secret (K).

If an attacker tried to create their own easy challenge, or alter the salt to remove the expiration date, the signature GateCHA calculates will not match the signature the attacker sent. 

GateCHA immediately drops the request with a 403 Forbidden error without even looking at the math.

Only if the signature is perfectly valid GateCHA perfrom the accuracy check and verify the math:

SHA-256(submitted_salt + submitted_number) == submitted_challenge


Expiration and Replay Protection: In the JSON object returned by GateCHA server, we can see that there is a  timestamp inside the salt (expires=1787560709).

  • Time-to-Live: The server mathematically verifies this timestamp upon submission to ensure the puzzle hasn't expired.
  • Single Use: Once a bot solves the puzzle and submits it, the server logs the transaction. If the bot tries to reuse that exact same solved payload for a second form submission (a replay attack), the server recognizes the duplicate and rejects it.


I hope, I am able to explain how GateCHA works and how to easily implement it in your websites. 

The sample code used in this blog is shared in this Github repository so that you can easily refer:

https://github.com/pranabsharma/gatecha-demo