Show HN: Wicketkeeper - A self-hosted, privacy-friendly proof-of-work captcha

4 months ago 6

A privacy-friendly, proof-of-work (PoW) captcha system designed to be a user-centric alternative to traditional captchas. Wicketkeeper protects your web forms from simple bots without requiring users to solve frustrating puzzles.

It achieves this by issuing a small, client-side computational challenge that is easy for a modern device to solve but costly for bots to perform at scale. The system is comprised of a Go backend, an embeddable JavaScript client, and a full-stack demo application.

This project was inspired by and adapts concepts and code from the TecharoHQ/anubis project.

til

 MIT Go Node.js Express.js TypeScript Docker


  • Proof-of-Work Engine: Replaces visual puzzles with a computational challenge that is easy for users but hard for bots.
  • Stateless & Secure: Uses signed JSON Web Tokens (JWTs) for challenge/response cycles, eliminating server-side session state.
  • Replay Attack Prevention: Leverages Redis Bloom filters for high-performance, time-windowed prevention of challenge reuse.
  • Embeddable Client Widget: A lightweight, dependency-free JavaScript widget that integrates easily into any web form.
  • Configurable: Easily adjust PoW difficulty, CORS origins, and ports via environment variables.
  • Containerized: Full Docker and Docker Compose support for easy deployment of the backend server and its Redis dependency.
  • Full-Stack Demo: Includes a complete Express.js + TypeScript example to demonstrate a real-world integration.

The Wicketkeeper ecosystem involves four main actors: the User's Browser, the Client Widget, your Application Backend, and the Wicketkeeper Server.

sequenceDiagram %% define IDs and human-readable labels participant UB as "User's Browser" participant CW as "Client Widget" participant AB as "Your App Backend" participant WKS as "Wicketkeeper Server" UB->>+CW: User interacts with form CW->>+WKS: GET /v0/challenge WKS-->>-CW: Returns signed JWT (Challenge + Difficulty) CW->>CW: Solves Proof-of-Work in-browser Note over CW: Finds a valid nonce/hash pair CW-->>UB: Populates hidden form field with solution UB->>+AB: Submits form with solution data AB->>+WKS: POST /v0/siteverify (with solution) WKS->>WKS: Verify JWT, Check PoW hash and Bloom-filter WKS-->>-AB: Returns success/failure alt Verification Successful AB->>AB: Process form data (e.g., save comment) AB-->>UB: Show success message else Verification Failed AB-->>UB: Show error message end
Loading
  1. Challenge Request: The client widget requests a new PoW challenge from the Wicketkeeper Server.
  2. Challenge Issuance: The server generates a unique challenge, packages it into a signed JWT, and sends it to the client.
  3. Proof of Work: The client's browser (using Web Workers) finds a solution (nonce) to the cryptographic puzzle.
  4. Form Integration: The solution is placed into a hidden input field in your web form.
  5. Server-Side Verification: When the user submits the form, your application's backend sends the solution to the Wicketkeeper Server's /v0/siteverify endpoint.
  6. Validation: The Wicketkeeper Server validates the JWT signature, the PoW correctness, and checks a Redis Bloom filter to ensure the challenge hasn't been used before. It returns a final success or failure response.

The repository is organized into three main components:

. ├── client/ # The frontend JS widget that solves the PoW challenge ├── server/ # The Go backend that issues and verifies challenges ├── example/ # A full-stack Express.js demo application └── README.md # This file

Getting Started: Full Demo Setup

This guide will help you run the full Wicketkeeper ecosystem, including the backend server, the client widget, and the example application.

Step 1: Clone the Repository

git clone https://github.com/a-ve/wicketkeeper.git cd wicketkeeper

Step 2: Run the Backend Services

The easiest way to run the Go server and its Redis dependency is with Docker Compose.

cd server/ mkdir data docker-compose up -d

This will build and start the wicketkeeper Go service on port 8080 and a redis-stack container. On the first run, a wicketkeeper.key file will be generated in server/data/.

Step 3: Build the Client Widget

The client widget needs to be compiled into a single JavaScript file.

cd ../client/ npm install npm run build:fast

This creates client/dist/wicketkeeper.js. Now, copy this file to the example application's public directory:

cp dist/wicketkeeper.js ../example/public/

Step 4: Run the Example Application

The example is an Express.js server that serves a simple HTML form and handles submissions.

cd ../example/ npm install # Compile the TypeScript code npx tsc # Start the server node dist/server.js

You should see the output: 🚀 Server listening on http://localhost:8081.

You can now navigate to http://localhost:8081 in your browser to see the Wicketkeeper demo in action!

Usage of Individual Components

The server is configured via environment variables. See server/README.md for more details.

Variable Description Default
LISTEN_PORT The port on which the server will listen. 8080
REDIS_ADDR The address of the Redis instance. 127.0.0.1:6379
DIFFICULTY Number of leading zeros for the PoW hash. Higher is harder. 4
ALLOWED_ORIGINS Comma-separated list of origins for CORS (e.g., https://domain.com). *
PRIVATE_KEY_PATH Path to store the Ed25519 private key. Will be created if it doesn't exist. ./wicketkeeper.key

API Endpoints:

  • GET /v0/challenge: Issues a new PoW challenge.
  • POST /v0/siteverify: Verifies a solved challenge.

Client Widget (JavaScript)

The client is a single JS file (dist/wicketkeeper.js) that can be included in any HTML page.

1. Include the Script

<script defer src="path/to/wicketkeeper.js"></script>

2. Add the Widget to a Form

The script automatically initializes any div with the class .wicketkeeper.

<div class="wicketkeeper" data-input-name="my_captcha_field"></div> <button type="submit">Submit</button> </form>'><form action="/submit" method="POST"> <!-- Other form fields --> <div class="wicketkeeper" data-input-name="my_captcha_field"></div> <button type="submit">Submit</button> </form>

The client can be configured with a custom challenge endpoint during the build step. See client/README.md for details.

This project is licensed under the MIT License.

Read Entire Article