--- url: /guide.md description: >- Set up Cap, the open-source self-hosted CAPTCHA, in about five minutes. Run the server with Docker, drop in the widget, verify tokens. No Google, no telemetry, no visual puzzles. --- # Quickstart cap is a self-hosted and privacy-first captcha alternative that doesn't force your users to click on traffic lights. there are no puzzles, no tracking, and no third parties watching your visitors. cap has two parts: a **widget** that runs the challenge and shows the checkbox, and a **server** that issues challenges and verifies solutions. you'll have both running in about five minutes. ::: tip Already using reCAPTCHA? Cap's `/siteverify` is compatible with reCAPTCHA's API. You can point your existing verification code at Cap by changing one URL, run both side by side, and cut over whenever you're ready. There's no rewrite and no risky big-bang switch. See the [feature comparison](./alternatives.md). ::: ## What you'll need * [Docker](https://docs.docker.com/get-docker/) (the fastest way to run the server) * A place to host it that's reachable from your users' browsers * A few minutes ## 1. Run the server I recommend [Cap Standalone](./standalone/index.md), a single container that exposes a small REST API and a dashboard for managing keys. It supports multiple site keys and is compatible with reCAPTCHA's siteverify API. [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/cap-1?referralCode=93HYBZ\&utm_medium=integration\&utm_source=template\&utm_campaign=generic) Create a `docker-compose.yml`: ```yaml services: cap: image: tiago2/cap:latest container_name: cap ports: - "3000:3000" environment: ADMIN_KEY: your_secret_password REDIS_URL: redis://valkey:6379 depends_on: valkey: condition: service_healthy restart: unless-stopped valkey: image: valkey/valkey:9-alpine container_name: cap-valkey volumes: - valkey-data:/data command: valkey-server --save 60 1 --loglevel warning --maxmemory-policy noeviction healthcheck: test: ["CMD", "valkey-cli", "ping"] interval: 5s timeout: 3s retries: 5 restart: unless-stopped volumes: valkey-data: ``` Start it: ```bash docker compose up -d ``` Open `http://localhost:3000` (or your server's IP or domain on port 3000), log in with your `ADMIN_KEY`, and create a site key. You'll get a **site key** and a **secret key**. Keep both, you'll need them in the next steps. ::: tip Tips * `ADMIN_KEY` is your dashboard password. Make it at least 32 characters. * Change `3000:3000` if that port is already in use. * If the dashboard is unreachable, add `network_mode: "host"` under the `cap` service. ::: ## 2. Add the widget The widget is a single web component. If you wouldn't like to pin versions, replace `` with `latest`. ```html ``` ::: tip Check the [latest release](https://github.com/tiagozip/cap/releases) for the version to pin. You can also [self-host the widget](./widget.md#self-hosting-the-widget), serving both the script and the solver's WASM files from your own domain instead of the CDN. ::: ### The simple way: drop it in a form If your widget lives inside a `
`, Cap injects a hidden `cap-token` field automatically and submits it with the rest of your form data. No JavaScript required. ```html
``` * `` is the public URL of your Cap server, e.g. `cap.example.com`. It has to be reachable by your visitors, so not `localhost`. * `` is the site key from your dashboard. On submit, your server receives `cap-token` alongside the other fields. Skip to [step 3](#_3-verify-the-token) to verify it. ### With JavaScript: when you need control For SPAs, custom flows, or anything that isn't a plain form, listen for the `solve` event: ```js const widget = document.querySelector("cap-widget"); widget.addEventListener("solve", (e) => { const token = e.detail.token; // send token to your server, enable the submit button, etc. }); ``` You can also render the widget invisibly and solve [programmatically](./programmatic.md), or use [floating mode](./floating.md). Framework snippets (React, Vue, Svelte and more) are on the [widget page](./widget.md#usage). ## 3. Verify the token Before you trust a submission, your server has to verify the token. Send a `POST` to your instance's `/siteverify` endpoint: ::: code-group ```sh [curl] curl "https:////siteverify" \ -X POST \ -H "Content-Type: application/json" \ -d '{ "secret": "", "response": "" }' ``` ```js [fetch] const { success } = await ( await fetch("https:////siteverify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ secret: "", response: "" }), }) ).json(); if (!success) throw new Error("invalid cap token"); ``` ```py [python] import requests success = requests.post( "https:////siteverify", json={"secret": "", "response": ""}, ).json().get("success") ``` ```php [php] //siteverify", false, stream_context_create([ "http" => [ "method" => "POST", "header" => "Content-Type: application/json", "content" => json_encode(["secret"=>"","response"=>""]) ] ]) ), true); var_dump($data['success'] ?? false); ``` ::: * `` is the **secret key** from your dashboard, not the dashboard `ADMIN_KEY`. Mixing these up is the most common setup mistake. * `` is the token from the widget (the `cap-token` form field or `e.detail.token`). A valid token returns: ```json { "success": true } ``` Tokens are single-use, so verify each one once and then run your own logic (create the account, send the message, and so on). ## 4. Confirm it works A quick end-to-end check: 1. Load your page. The checkbox should tick and your `solve` handler (or form field) should produce a token. 2. Send that token to `/siteverify`. You should get `{ "success": true }`. 3. Send the same token again. It should now fail, which confirms single-use is working. If verification always fails, check that you're using the secret key (not the admin key) and that `` is the same public URL the widget points at. That's the whole integration. Users solve challenges in their browser, your server verifies tokens, and you keep every byte of the data. ## Built for compliance Because Cap is self-hosted with no cookies, no tracking, and no third-party calls, your users' data never leaves your infrastructure. Cap is designed to meet GDPR, CCPA, HIPAA, LGPD and other privacy regimes, and the proof-of-work checkbox avoids the WCAG 2.2 barriers that image and audio puzzles run into. Full details, and the regulations Cap is built around, are on the [Compliance](./compliance.md) page. ## Next steps You're protecting your forms. From here you can: * Drop Cap into your stack with a [framework snippet](./widget.md#usage) * [Customize the widget](./widget.md#options)'s look and behavior * Tune [instrumentation](./instrumentation.md) and [configure](./standalone/options.md) CORS and rate limiting * See how Cap compares to [reCAPTCHA, Turnstile, hCaptcha and others](./alternatives.md) * Read the [best CAPTCHA alternatives for 2026](./best-captcha-alternatives.md) guide if you're still evaluating --- --- url: /guide/alternatives.md description: >- Compare Cap against reCAPTCHA, hCaptcha, Turnstile, Altcha and more. See how the open-source, self-hosted, proof-of-work CAPTCHA stacks up on privacy and cost. --- # CAPTCHA Feature Comparison: Cap vs the Alternatives Cap is a free, open-source, self-hosted CAPTCHA alternative that uses proof-of-work and [instrumentation challenges](./instrumentation.md) instead of image puzzles. Here is how it compares to reCAPTCHA, hCaptcha, Cloudflare Turnstile, Altcha, FriendlyCaptcha, SilentShield, and others across 12 criteria. | CAPTCHA | Open-source | Free | Private | Fast to solve | Easy for humans | Small error rate | GDPR compliant | Customizable | Hard for bots | Instrumentation | RSW support | Easy to integrate | | :------------------- | :---------- | :--- | :------ | :------------ | :-------------- | :--------------- | :------------- | :----------- | :------------ | :-------------- | :---------- | :---------------- | | **Cap** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Cloudflare Turnstile | ❌ | ✅ | 🟨 | 🟨 | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | 🟨 | ✅ | | reCAPTCHA | ❌ | 🟨 | ❌ | ❌ | ❌ | 🟨 | 🟨 | ❌ | 🟨 | ✅ | 🟨 | ✅ | | hCAPTCHA | ❌ | 🟨 | 🟨 | ❌ | ❌ | 🟨 | 🟨 | ❌ | 🟨 | ✅ | 🟨 | ✅ | | Altcha | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🟨 | ❌ | ❌ | 🟨 | | FriendlyCaptcha | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | 🟨 | | SilentShield | ❌ | 🟨 | 🟨 | ✅ | ✅ | 🟨 | ✅ | ❌ | 🟨 | ✅ | ❌ | ✅ | | MTCaptcha | ❌ | 🟨 | 🟨 | ❌ | ❌ | 🟨 | ✅ | ❌ | ❌ | ❌ | ❌ | 🟨 | | GeeTest | ❌ | ❌ | ❌ | 🟨 | 🟨 | 🟨 | ✅ | ❌ | 🟨 | ❌ | ❌ | 🟨 | | Arkose Labs | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | 🟨 | 🟨 | 🟨 | 🟨 | 🟨 | One criterion Cap deliberately fails: there is no managed hosting. If you refuse to run any infrastructure at all, Cloudflare Turnstile or FriendlyCaptcha is a better fit. ::: tip Note Based on internal testing, Cap achieved lower challenge abandonment rates and higher privacy-browser compatibility than competing solutions. "Hard for bots" refers to resistance against commodity automation, including headless browsers, scripted attacks, and self-operated bot networks. For Cap, this is primarily provided by PoW and instrumentation. This category does not consider commercial CAPTCHA-solving services or human-assisted solver platforms, as their effectiveness is difficult to independently verify. ::: ## All alternatives ### Cloudflare Turnstile Cloudflare Turnstile is a great alternative to Cap, but it is widely reported to fail or loop for users on privacy browsers such as Brave or Librewolf, because its verdicts rely on fingerprinting signals those browsers deliberately break. Additionally, unlike Turnstile, Cap is open-source and self-hosted. With Turnstile, if Cloudflare's algorithm marks a user as "suspicious," you cannot override it. Cap puts the levers of control in your hands, so you decide the difficulty and strictness, not a third party. [Full comparison: Cap vs Cloudflare Turnstile →](./alternatives/turnstile.md) ### reCAPTCHA Not only is Cap significantly smaller and faster than reCAPTCHA, it's open-source, fully free, and much more private. Cap doesn't require you to check traffic signs or solve puzzles, and it doesn't track users or collect data. reCAPTCHA v2 ("I'm not a robot") is getting harder for humans while remaining trivial for AI solvers (especially audio challenges). v3 (Invisible) is great, but if Google thinks you are "suspicious" (e.g., using a VPN or privacy tools), it often blocks you entirely or forces a hard puzzle loop with no way out. [Full comparison: Cap vs reCAPTCHA →](./alternatives/recaptcha.md) ### hCAPTCHA Pretty much the same as reCAPTCHA, however, while it's significantly more resistant to bots, it imposes a heavy "Puzzle Tax" on your users. Users hate puzzles. They leave. Drop-off rates on hCaptcha challenges can be **5-15%** depending on difficulty. Additionally, hCaptcha's free tier is aggressive with serving puzzles to save their own costs, which hurts your conversion rates. [Full comparison: Cap vs hCaptcha →](./alternatives/hcaptcha.md) ### Altcha Cap is slightly smaller than Altcha and includes extra features like progress tracking, instrumentation challenges, and a simpler dashboard. If you don't need these, Altcha is still a solid choice. [Full comparison: Cap vs Altcha →](./alternatives/altcha.md) ### mCAPTCHA While mCAPTCHA is similar to both Cap and Altcha, it is still pre-1.0 with infrequent releases and has a larger widget bundle. ### FriendlyCaptcha Unlike FriendlyCaptcha, Cap is completely free and self-hosted at any volume (FriendlyCaptcha's Starter plan is €9/month for 1,000 requests/month, with higher tiers as you scale). [Full comparison: Cap vs FriendlyCaptcha →](./alternatives/friendlycaptcha.md) ### SilentShield SilentShield is a hosted, invisible bot-protection service that scores mouse, keyboard, and scroll behavior instead of running a challenge. It's convenient on WordPress, but it's closed-source, can't be self-hosted, and its free tier caps at 500 requests/month (paid tiers from €9/month for 5,000 requests). Cap imposes a real computational cost on bots rather than guessing from behavior, is fully open-source, and is free at any volume on your own infrastructure. [Full comparison: Cap vs SilentShield →](./alternatives/silentshield.md) ### MTCaptcha MTCaptcha relies heavily on image challenges, which are usually easily solvable by LLMs and OCRs and have high drop-off rates. Cap is also lightweight, self-hostable and doesn't rely on obfuscation. ### GeeTest Cap is free, self-hosted, and open-source, while GeeTest is a paid service. Cap is also more private and doesn't rely on tracking users or collecting data. GeeTest is also China-based, which may be a concern for some users regarding data sovereignty. ### Arkose Labs Arkose's CAPTCHA is known for being hard, slow, and annoying for humans to solve. It is also a paid, closed-source service mostly available to big enterprises. They also only operate in the US, Canada, Argentina, India, Israel and a small set of other countries, excluding many EU countries. ### Anubis While Anubis is a great scraper deterrent and uses the same proof-of-work concept as Cap, it uses a low difficulty by default (which is easier for bots to solve) and does not provide a standalone CAPTCHA server. Cap also implements dynamic instrumentation challenges, which make it harder for bots to finish the process after solving PoW. [Full comparison: Cap vs Anubis →](./alternatives/anubis.md) ## Related guides * [Best CAPTCHA alternatives in 2026](./best-captcha-alternatives.md): the full field, ranked with criteria * [Open-source CAPTCHA options](./open-source-captcha.md): Cap, ALTCHA, mCAPTCHA, and Anubis compared * [CAPTCHA and conversion rate](./captcha-conversion-rate.md): how challenges cost you signups * [Bot protection for mobile forms](./mobile-form-bot-protection.md): puzzle-free protection on touch devices * [Migrate from reCAPTCHA](./alternatives/migrate-from-recaptcha.md): the URL-swap migration path --- --- url: /guide/alternatives/migrate-from-recaptcha.md description: >- Google is moving reCAPTCHA into Google Cloud and auto-migrating Classic keys. Migrate to Cap instead: a faster, private, self-hosted, open-source CAPTCHA with a reCAPTCHA-compatible siteverify API and Turnstile-tier bot detection. --- # Migrating from reCAPTCHA to Cap Google is moving reCAPTCHA into Google Cloud and auto-migrating Classic keys onto billed projects. So you have to touch your integration regardless. The good news: this is the perfect excuse to leave Google's tracking-based CAPTCHA behind for something faster, private, and free. Cap is a drop-in replacement for the part that matters, and an upgrade for almost everything else. ## Why teams switch to Cap * **Turnstile-tier detection, without the third party.** Cap pairs proof-of-work with instrumentation challenges, the same browser-verification technique YouTube and Twitter/X run at massive scale. It sits in the same detection tier as Cloudflare Turnstile while staying fully self-hosted. * **Proven at scale.** Around **1 billion challenges solved in Q1 2026 alone** (per JSDelivr), and trusted in production by teams like **AdGuard** and **Bunny.net**. This is not an experiment. * **A fraction of the weight, and invisible.** Cap's widget is roughly 21 KB gzipped versus reCAPTCHA's 200 to 600 KB client, often a 10x or more reduction. Default challenges solve in 2 to 3 seconds in the background, with no traffic-light puzzles and nothing for the user to click through. * **Genuinely free, no metering.** No Google Cloud project, no billing account, no per-assessment fee. One Docker container and a Valkey instance run most workloads on a $5 VPS. * **Private by default.** reCAPTCHA loads scripts from `google.com` and ships user signals to Google. Cap sends data nowhere. Nothing third-party touches your page. * **You hold the controls.** reCAPTCHA v3 silently penalizes users on VPNs, Tor, and privacy browsers with no recourse. With Cap you set the difficulty, and every real user always has a path through. * **Open source, forever.** Apache 2.0. Audit it, fork it, deploy it. No vendor can change the terms on you. For the full breakdown, see [Cap vs reCAPTCHA](./recaptcha.md). ## What's changing with reCAPTCHA If you need the context for why the migration emails are landing now: * New keys can no longer be created in the legacy reCAPTCHA admin console. * Existing reCAPTCHA Classic keys are being migrated automatically into Google Cloud projects, a process Google ran through late 2025 and into 2026. * Once migrated, a key's API access is tied to a Google Cloud project. Past the free tier of 10,000 assessments per month, you have to enable billing on that project. Keeping reCAPTCHA now means a Google Cloud project, a billing account on file, and metered assessments. Moving to Cap means none of that, on a timeline you control. ## How the migration works Cap's `/siteverify` endpoint mirrors reCAPTCHA's request shape on purpose, so the server side is close to a drop-in. The widget swap is tag-for-tag. Three steps, and you can run both in parallel during the cut-over. ### 1. Stand up a Cap instance Follow the [quickstart](../index.md) to run Cap Standalone with Docker. Create a site key in the dashboard and note both the **site key** and its **secret key**. Keep [instrumentation challenges](../instrumentation.md) on (the default) for the strongest bot protection. ### 2. Swap the client widget Replace the reCAPTCHA script and element with Cap's widget. Before: ```html
``` After: ```html ``` If your reCAPTCHA widget lived inside a `
`, the token handling carries over: reCAPTCHA auto-injected a `g-recaptcha-response` field, and Cap auto-injects a `cap-token` field on submit. Outside a form, listen for the `solve` event: ```js document.querySelector("cap-widget").addEventListener("solve", (e) => { const token = e.detail.token; }); ``` ### 3. Swap the server verification reCAPTCHA verification posts `secret` and `response` to a fixed Google URL. Cap takes the same two parameters, posted to your own instance: Before: ```js const { success } = await ( await fetch("https://www.google.com/recaptcha/api/siteverify", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ secret: RECAPTCHA_SECRET, response: token }), }) ).json(); ``` After: ```js const { success } = await ( await fetch("https:////siteverify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ secret: CAP_SECRET, response: token }), }) ).json(); ``` The token your server reads also changes name: pull `cap-token` from the submitted form (or the value you captured from the `solve` event) instead of `g-recaptcha-response`. ## What's compatible, and what isn't We'd rather you migrate with eyes open than hit a surprise in production. The compatibility is real, but not byte-for-byte: | | reCAPTCHA | Cap | | --- | --- | --- | | Request params | `secret`, `response`, optional `remoteip` | `secret`, `response` (`remoteip` is ignored) | | Endpoint | fixed `google.com` URL | your own `//siteverify` | | Success field | `success` (boolean) | `success` (boolean) | | Error reporting | `error-codes` (array) | `error` (string) | | Extra fields | `challenge_ts`, `hostname`, `score` (v3) | none | In practice: * Code that only checks `response.success` works after a URL and secret swap. That's the common case, and it's a one-line change. * Code that inspects `error-codes`, `challenge_ts`, `hostname`, or the v3 `score` needs updating. Cap is a verification system, not a behavioral risk score, so those fields don't exist. * If you use a backend SDK that hardcodes Google's verify URL, swap it for one that lets you set the endpoint, or call `/siteverify` directly. It's two parameters. ## Migrate with zero downtime You never have to flip a switch and pray. Mount Cap on a separate element and have your backend accept either a valid `cap-token` or a valid `g-recaptcha-response` during the transition. Watch Cap's verification rate in your logs, and once it looks healthy, delete the reCAPTCHA script, element, and server call. Most teams finish the cut-over in an afternoon. ## See also * [Live demo](../demo.md) — solve a Cap challenge yourself, then time it against reCAPTCHA * [Cap vs reCAPTCHA](./recaptcha.md) — the full comparison * [How Cap detects bots](../effectiveness.md) — the proof-of-work + instrumentation model * [Quickstart](../index.md) — stand up Cap from scratch in five minutes --- --- url: /guide/alternatives/recaptcha.md description: >- Cap vs Google reCAPTCHA v2 and v3: a privacy-first, self-hosted, open-source alternative. No Google tracking or visual puzzles, just proof-of-work challenges. --- # Cap vs reCAPTCHA reCAPTCHA is Google's CAPTCHA service, available in two main versions: v2 ("I'm not a robot") and v3 (invisible, score-based). Both require sending traffic data to Google. ## Quick verdict If your site doesn't already depend on Google for analytics or login, there is little reason to keep reCAPTCHA. Cap matches the protection level for the vast majority of use cases without sending users to Google, without per-request quotas, and without forcing visitors through traffic-light puzzles when Google's risk score doesn't like them. ## Where reCAPTCHA still makes sense * You're already deeply integrated with Google's identity stack and want one less vendor decision. * You specifically need Google's behavioral risk score (v3) and have the infrastructure to act on it. * Your team is unwilling to run any backend service, even a single Docker container. ## Where Cap is the better choice * **Privacy.** reCAPTCHA loads scripts from `google.com` and sends user signals to Google. Cap runs entirely on your infrastructure and adds nothing third-party to your page. * **No visual puzzles.** reCAPTCHA v2 routinely drops users into traffic-light, fire-hydrant, and crosswalk puzzles. These are increasingly easy for AI solvers and increasingly hard for humans, especially on mobile or for users on VPNs and privacy browsers. * **Bundle size.** reCAPTCHA's client weighs 500 KB+. Cap's widget is ~20 KB. * **No quotas.** reCAPTCHA Enterprise is metered per assessment. Cap has no per-request fee: it runs on a $5 VPS for most workloads. * **No "suspicious user" lockouts.** v3 will silently penalize users who use Tor, VPNs, or privacy-respecting browsers. With Cap you set the difficulty; the user always has a path through. * **Open source.** Apache 2.0. Audit, fork, deploy. ## Migration Cap's `/siteverify` endpoint is intentionally compatible with reCAPTCHA's API shape, so most server-side migrations are a single URL swap plus a new secret. Client-side, replace the ` ``` For the floating mode, use: ```html ``` And by setting `window.CAP_CUSTOM_WASM_URL` and `window.CAP_CUSTOM_HASHWX_URL` to the paths of the `cap_wasm_bg.wasm` and `hashwx.wasm` files, like this: ```js window.CAP_CUSTOM_WASM_URL = "https:///assets/cap_wasm_bg.wasm"; window.CAP_CUSTOM_HASHWX_URL = "https:///assets/hashwx.wasm"; ``` `hashwx.wasm` ships in `@cap.js/wasm` 0.0.8 and newer. With an older `WASM_VERSION`, `/assets/hashwx.wasm` responds with a 503, so leave `CAP_CUSTOM_HASHWX_URL` unset and the widget will load it from jsdelivr. By default, these fetch from `process.env.CACHE_HOST` (which defaults to `https://cdn.jsdelivr.net`). You can change this by setting the `CACHE_HOST` env variable when running the server. ### Troubleshooting The assets are downloaded from `CACHE_HOST` into Redis at startup, then refreshed hourly. If an asset endpoint responds with `Asset not cached yet`, the download hasn't happened. Check that: * `ENABLE_ASSETS_SERVER=true` is actually set on the Cap container. If you changed it in a compose file, recreate the container. When it isn't set, the `/assets/*` endpoints respond with a 404 explaining that the asset server is disabled. * The container has outbound network access to `CACHE_HOST`. If a download fails, the server logs a line containing `[asset server] failed to update assets cache` at startup and retries every hour. * `WIDGET_VERSION` and `WASM_VERSION` point to versions that actually exist on npm. ## Rate-limiting Challenge endpoints are rate-limited per client IP using a fixed window, defaulting to 30 requests every 5 seconds. You can change the global limit under **Settings** in the dashboard (or via `PUT /settings/ratelimit`), and override it per site key in the key's **Configuration** tab. When the limit is exceeded, requests get a `429` response with an `X-RateLimit-Remaining: 0` header. The `/siteverify` endpoint is intended for server-to-server use, so it's not ratelimited by default. ### Client IPs behind a proxy Standalone identifies clients by checking the `X-Forwarded-For`, `X-Real-IP` and `CF-Connecting-IP` headers (in that order), falling back to the socket address. If you're behind a reverse proxy that uses a different header, set `RATELIMIT_IP_HEADER` in your env (or the IP header under **Settings > Headers** in the dashboard). For example, behind Cloudflare you might set it to `cf-connecting-ip`. Make sure your proxy actually forwards the client IP. For nginx: ```nginx location / { proxy_pass http://localhost:3000; proxy_set_header X-Forwarded-For $remote_addr; } ``` Without this, every request appears to come from the proxy's own IP and all clients share a single rate-limit bucket. Also note that `X-Forwarded-For` is trusted as-is, so the server must not be directly reachable from the internet, otherwise clients can spoof the header and bypass rate limiting. ## Redis / Valkey Cap Standalone uses Redis (or Valkey) for all data storage. Set the `REDIS_URL` environment variable to your Redis connection string. This defaults to `redis://localhost:6379`. The recommended setup uses Valkey (a Redis-compatible store) via the docker-compose file provided in the [quickstart guide](/guide/standalone/). If you share a single Redis instance across multiple Cap deployments (or with other apps), set `REDIS_PREFIX` to namespace all keys. For example, `REDIS_PREFIX=cap:` stores sessions as `cap:session:...`, metrics as `cap:metrics:...`, and so on. It's empty by default, so existing deployments are unaffected. ## Health checks and shutdown Cap Standalone has two unauthenticated endpoints for orchestrators and uptime monitors: * `GET /health` returns `200 {"status":"ok"}` when Redis answers a `PING` within 2 seconds, and `503 {"status":"unavailable"}` otherwise. Use it for readiness checks and alerts. * `GET /health/live` returns `200` as long as the process is running, even while Redis is down. Use it for liveness checks, so an orchestrator doesn't restart Cap in a loop during a Redis outage. Checks that arrive within the same second share one `PING`, so polling `/health` adds no load on Redis. In Kubernetes: ```yaml readinessProbe: httpGet: path: /health port: 3000 livenessProbe: httpGet: path: /health/live port: 3000 ``` With Docker Compose, add this to the `cap` service. Plain Docker only marks the container as unhealthy, it doesn't restart it. ```yaml healthcheck: test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:3000/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"] interval: 30s timeout: 5s retries: 3 ``` On `SIGTERM` or `SIGINT`, Cap stops accepting connections, lets requests already in progress finish, closes its Redis connection and exits with code 0. If a request is still running after 8 seconds, Cap exits with code 1 anyway, which keeps it inside Docker's default 10-second stop timeout. A second signal exits immediately. ## Error messages Error messages are redacted by default and instead logged to the console. To disable error logging, set `DISABLE_ERROR_LOGGING=true`. To disable error message redaction, set `SHOW_ERRORS=true`. ## HashWX proof of work Standalone uses [HashWX](../hashwx.md), a GPU-resistant proof of work, as the default challenge protocol for new keys. It's configured per site key, so individual keys can use SHA-256 while others stay on HashWX. Keys created before HashWX became the default keep whatever they were on until you change them. To change the protocol, open a key's **Configuration** tab and pick one under **Challenge protocol**. HashWX needs no setup: there's no keypair to generate or persist. Difficulty is controlled by the **HashWX difficulty** slider, the expected number of hashes a client must compute. It defaults to `1_000_000`, split into four sub-challenges, which measured a 578 ms median on an 8-core desktop in Chrome and 1.1 to 5.9 s on phones. See [the phone measurements](../hashwx.md#phones) before you raise it. The valid range is `50_000`-`5_000_000`. Clients without WebAssembly cannot solve HashWX. If you need to support them, put that key on SHA-256 proof of work instead, which has a pure-JS fallback. RSW time-lock puzzles are still selectable for existing deployments, but they are deprecated: a GPU clears about 170 times as many of them per second as a CPU, so they do not deliver the GPU resistance they were added for. The **RSW difficulty** slider sets `t`, the number of sequential squarings, in the range `10_000`-`300_000`, and `RSW_BITS=2048` overrides the modulus size at boot. ::: tip The widget auto-detects the protocol from the wire format, so switching a key is the only change you need to make. Outside Standalone, cap-core still defaults to SHA-256 PoW unless you opt in. ::: ## Instrumentation challenges Cap Standalone supports JavaScript instrumentation challenges to defeat proof-of-work solvers, along with options to block headless browsers from solving them. Instrumentation challenges are enabled by default when creating new site keys. You can toggle instrumentation challenges on or off in your site key config. To block headless browsers, turn on "Attempt to block headless browsers" in the key settings. Note that high instrumentation levels can significantly reduce generation throughput. We recommend keeping it at level 3 unless you need stronger obfuscation. If you find level 3 is too slow, level 1 is significantly faster on a single core. ## IP database Country and ASN lookups can use one of three providers, configured under `Settings > IP Data > Country & ASN data` in the dashboard: DB-IP Lite, MaxMind GeoLite2 and IPInfo's API. For DB-IP and MaxMind, the `.mmdb` files are downloaded into `/usr/src/app/data/` inside the container. ### Docker volume permissions The container runs as the unprivileged `bun` user (UID 1000). If you bind-mount a host directory onto `/usr/src/app/data`, the host directory must be writable by UID 1000, otherwise the download will fail with `EACCES: permission denied`. ```bash mkdir -p ./cap-data sudo chown 1000:1000 ./cap-data ``` ```yaml services: cap: image: tiago2/cap:latest volumes: - ./cap-data:/usr/src/app/data # ... ``` If you can't change ownership on your host (some platforms like Coolify make this awkward), the simplest alternatives are: * Skip the bind mount entirely and let Docker manage the data directory — the image already creates it with the correct ownership. * Use a named volume instead of a bind mount. * Switch to an IP Data provider that doesn't require any local files. --- --- url: /guide/widget.md description: >- Cap's client widget requests, solves and displays proof-of-work challenges with a native web component and WASM. Client-side docs for the open-source CAPTCHA. --- # Widget Cap's client-side widget handles requesting, solving and displaying challenges using a native web component and rust-flavoured WASM. It also includes the [programmatic mode](./programmatic). ## Installation ::: code-group ```sh [pnpm] pnpm add cap-widget ``` ```sh [npm] npm i cap-widget ``` ```sh [bun] bun add cap-widget ``` ```html [cdn] ``` ::: ## Self-hosting the widget The jsdelivr script tag above is the easiest way to start, and stays the default. If you'd rather not depend on a CDN, you can serve everything from your own domain instead. There are two parts to it: the widget JavaScript, and the WASM files the solver loads at runtime. ### Serving the widget JavaScript Load the `cap-widget` package from a path you control instead of jsdelivr. You have two options: * Install it from npm (`pnpm add cap-widget`) and `import "cap-widget"` in your app, letting your bundler serve it alongside your other code. The framework examples below already do this. * Serve the prebuilt file directly. The [Standalone asset server](./standalone/options#asset-server) exposes it at `/assets/widget.js` once you set `ENABLE_ASSETS_SERVER=true`, so you can point a plain script tag at your own instance: ```html ``` ### Pointing the WASM solver at your own copies By default the widget fetches its two WASM files (`cap_wasm_bg.wasm` and `hashwx.wasm`) from jsdelivr at solve time. Override the URLs with `window.CAP_CUSTOM_WASM_URL` and `window.CAP_CUSTOM_HASHWX_URL`. Set them before the widget solves a challenge, so the simplest place is an inline script before you load the widget: ```html ``` If you use a bundler, set the two globals before the import runs: ```js window.CAP_CUSTOM_WASM_URL = "https:///assets/cap_wasm_bg.wasm"; window.CAP_CUSTOM_HASHWX_URL = "https:///assets/hashwx.wasm"; import "cap-widget"; ``` The URLs can point anywhere you host the files, not just a Standalone instance. The [Standalone asset server](./standalone/options#asset-server) is one way to host them: with `ENABLE_ASSETS_SERVER=true` it serves `/assets/cap_wasm_bg.wasm` and `/assets/hashwx.wasm` (alongside `/assets/widget.js` and `/assets/floating.js`), downloaded once from `CACHE_HOST` and refreshed hourly. Set `WIDGET_VERSION` and `WASM_VERSION` to pin the versions it serves. ::: warning `hashwx.wasm` only ships in `@cap.js/wasm` 0.0.8 and newer. If your `WASM_VERSION` is older, `/assets/hashwx.wasm` responds with a 503, so leave `CAP_CUSTOM_HASHWX_URL` unset and the widget will load HashWX from jsdelivr. ::: ## Usage The widget requires a `data-cap-api-endpoint` pointing at your Cap deployment. For Standalone instances, this is: ``` https://// ``` ### Vanilla ```html ``` ::: tip When the widget lives inside a `
`, it automatically injects a hidden `cap-token` input, and the token is submitted alongside your other fields with no extra JavaScript needed. ::: ### React ```jsx import "cap-widget"; export default function ContactForm() { return ( console.log("token:", e.detail.token)} onprogress={(e) => console.log(e.detail.progress)} onerror={(e) => console.error(e.detail.message)} /> ); } ``` ::: tip We recommend using React 19 or later as it improves custom element event handling ::: ### Vue ```vue ``` If you're getting an unknown-component warning, add this to your `vite.config.js`: ```js export default defineConfig({ plugins: [ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("cap-") }, }, }), ], }); ``` ### Svelte 5 ```svelte
console.log("token:", e.detail.token)} on:progress={(e) => console.log(e.detail.progress)} on:error={(e) => console.error(e.detail.message)} /> ``` ### SolidJS ```jsx import "cap-widget"; export default function ContactForm() { return (
console.log("token:", e.detail.token)} on:progress={(e) => console.log(e.detail.progress)} on:error={(e) => console.error(e.detail.message)} /> ); } ``` ### Astro ```astro --- // ContactForm.astro ---
``` If you're rendering a React/Vue/Svelte component inside Astro, follow that framework's guide above and add `client:load` to the component. ### Preact ```jsx import "cap-widget"; export default function ContactForm() { return (
console.log("token:", e.detail.token)} onprogress={(e) => console.log(e.detail.progress)} onerror={(e) => console.error(e.detail.message)} /> ); } ``` ### Qwik ```tsx import { component$ } from "@builder.io/qwik"; import "cap-widget"; export default component$(() => { return (
console.log("token:", e.detail.token)} on:progress$={(e: CustomEvent) => console.log(e.detail.progress)} on:error$={(e: CustomEvent) => console.error(e.detail.message)} /> ); }); ``` ## Programmatic mode If you don't want a visible widget, for example when protecting a background action like a post submission, use the [programmatic mode](./programmatic): ```js import Cap from "cap-widget"; const cap = new Cap({ apiEndpoint: "https:////", }); const { token } = await cap.solve(); ``` ## Events All events are dispatched as CustomEvent. | Event | When it fires | Detail | | ---------- | ------------------------------ | ---------------------- | | `solve` | Challenge solved successfully | `{ token: string }` | | `progress` | Progress update during solving | `{ progress: number }` | | `error` | An error occurred | `{ message: string }` | | `reset` | Widget reset to initial state | `{}` | ## Options You can also specify a custom fetch function with `window.CAP_CUSTOM_FETCH`: ```js window.CAP_CUSTOM_FETCH = (url, params) => fetch(url, params); ``` If you serve the widget under a strict Content-Security-Policy, you can supply nonces so the widget's injected `