Developer guideSelf-hosting

Self-hosting

Serve the loader from your own origin, or run the whole service yourself: the binary, the store, the ports and the settings.

Two different things

Hosting the loader means serving the script file from your own origin while the checks still run on our service. It suits a strict content security policy, change control that wants to review every byte that runs on the page, or a wish to keep third-party hosts out of the page entirely. Running the service means operating the whole system yourself, including the verify endpoint and the store, so no request leaves your infrastructure. The same software runs our service and a self-hosted installation; self-hosting the service is part of the Enterprise conversation on the pricing page.

Hosting the loader

The loader is one self-contained file. Copy it from us, serve it as application/javascript, and reference it from your page. It carries the address of our API, so no configuration changes; the only differences are that your origin now serves the script and starts the workers, and that updates arrive when you copy a new file rather than automatically.

Shell
# Copy the current loader and keep its hash with the copy
curl -sS -o otcaptcha.js https://api.otcaptcha.com/captcha/v1/otcaptcha.js
shasum -a 256 otcaptcha.js
HTML
<script src="[SCRIPT URL]" defer></script>
<!-- for example /static/otcaptcha.js?v=3; change the query when you copy a new file -->
  • Workers. The background workers are started from the script's own URL, so serve it from an origin your worker-src allows, with the right content type, and do not rewrite or wrap the file: it detects that it is running inside a worker by the absence of document.
  • Caching. Serve it with a long cache life and change the URL, or its query string, when you copy a new file, so no browser runs a stale copy next to a new one.
  • Compression. Serve it compressed. We precompress with brotli and gzip; the file is about 12 KB gzipped and under 11 KB with brotli.
  • Version. The loader sends its version with every call, and your reports show which versions your visitors run, so a copy that falls behind is visible. The changelog says what each version changed.
  • Integrity. Record the SHA-256 of the file you copied. A integrity attribute on the script tag works only when the file is served from your origin, and it must be updated with every copy.

Running the service

The service is one statically linked binary, built from source into a container image with nothing else in it, plus a store. It has no other runtime dependency.

PartDefaultWhat it serves
Public listener:8080/captcha/v1/otcaptcha.js, /load, /verify, /puzzle, /funnel, plus /health-check and /ready. Expose this one to browsers.
Internal listener:8081/validate, the server check, plus the health routes. Expose this one to your application servers only, and set the address allow-list.
Metrics listener:9800/metrics in Prometheus text format. Keep it private.
StoreRedisSpent passes, solved challenges, rate counters and puzzle state. Redis with replicas and Sentinel is supported for failover; an in-memory store exists for a single instance and tests.
RecordsoffPer-attempt records as files in a directory you name, one line per record, kept for 90 days by default.

/health-check answers 200 while the process is up and returns no configuration. /ready fails when the store is unreachable, so use it for readiness, not for a load balancer that would otherwise pull every instance at once: the challenge route needs no store and keeps working through a store failure, while the server check answers 503 and your outage policy applies.

Settings

Everything is set through environment variables. On/off settings take on, off, true or false; anything unparseable stops the start, deliberately, so a typo cannot silently leave a protection off.

Environment
# Minimal single-instance run for evaluation. Memory store: not shared, so one instance only.
OTCAPTCHA_STORE=memory
OTCAPTCHA_CREDENTIALS_FILE=/etc/otcaptcha/credentials.json
OTCAPTCHA_ALLOWED_ORIGINS=https://www.example.com,https://*.example.net
OTCAPTCHA_TRUSTED_PROXIES=10.0.0.0/8
OTCAPTCHA_REAL_IP_HEADER=X-Forwarded-For
OTCAPTCHA_VALIDATE_ALLOWED_CIDRS=10.0.0.0/8
OTCAPTCHA_EVENTS_DIR=/var/lib/otcaptcha/events
VariableDefaultMeaning
OTCAPTCHA_PUBLIC_ADDR, OTCAPTCHA_INTERNAL_ADDR, OTCAPTCHA_METRICS_ADDR:8080, :8081, :9800The three listeners.
OTCAPTCHA_PUBLIC_BASE_PATH/captcha/v1Path prefix of the browser routes.
OTCAPTCHA_CREDENTIALS_FILEemptySite keys and secrets, JSON, format below. Required unless an any-key secret is set.
OTCAPTCHA_ANY_ID_SECRETemptyAt least 32 characters. Lets the service derive a secret for any well-formed site key not in the file, for evaluation; the server check then skips the sign_token for such keys.
OTCAPTCHA_STOREredisredis or memory.
OTCAPTCHA_REDIS_ADDR, OTCAPTCHA_REDIS_PASSWORD, OTCAPTCHA_REDIS_DB127.0.0.1:6379, empty, 0The store, when no Sentinels are set.
OTCAPTCHA_REDIS_SENTINELS, OTCAPTCHA_REDIS_MASTERempty, otcaptchaFollow the primary through Sentinel.
OTCAPTCHA_TOKEN_TTL, OTCAPTCHA_PASS_TTL10m, 5mLife of a challenge; life of a pass. The pass life may exceed the challenge life by at most one minute.
OTCAPTCHA_IDEMPOTENCY_WINDOW20sAn identical server check inside this window gets the same answer, once.
OTCAPTCHA_POW_BITS, OTCAPTCHA_POW_COUNT12, 64The invisible tier's work.
OTCAPTCHA_POW_HARD_BITS, OTCAPTCHA_POW_HARD_COUNT13, 96The hold and puzzle tiers' work.
OTCAPTCHA_HOLD_MS1500Length of the hold.
OTCAPTCHA_INTERACTIVE_THRESHOLD60Score from which a hold is asked.
OTCAPTCHA_RATE_IP_PER_MIN, OTCAPTCHA_RATE_NET24_PER_MIN, OTCAPTCHA_RATE_FP_PER_MIN30, 200, 120Rates that add to the score; 0 turns the fingerprint rate off.
OTCAPTCHA_RISK_ENFORCE, OTCAPTCHA_RISK_ENFORCE_IDSoff, emptyLet the measured checks count, for every key or for the listed keys only.
OTCAPTCHA_ATTACK_MODE, OTCAPTCHA_ATTACK_MIN_PER_MINon, 10Automatic attack mode, and the solves a minute below which volume alone never raises it.
OTCAPTCHA_PUZZLEoffThe slide puzzle tier.
OTCAPTCHA_PUZZLE_SCORE, OTCAPTCHA_PUZZLE_ATTEMPTS, OTCAPTCHA_PUZZLE_TOLERANCE, OTCAPTCHA_PUZZLE_TTL80, 3, 4, 2mScore from which the puzzle is asked, misses allowed, pixels of tolerance, life of a picture.
OTCAPTCHA_THEMESemptyThe brand look per site key, as JSON.
OTCAPTCHA_ALLOWED_ORIGINSemptyPage origins allowed to call the browser routes, comma-separated. https://*.example.com and * are accepted.
OTCAPTCHA_VALIDATE_ALLOWED_CIDRSempty, openAddresses allowed to call the server check. Set it.
OTCAPTCHA_VALIDATE_MAX_CONCURRENT512Above this the server check answers 503.
OTCAPTCHA_TRUSTED_PROXIES, OTCAPTCHA_REAL_IP_HEADERempty, X-Forwarded-ForWhere the visitor's address is read from when a proxy sits in front.
OTCAPTCHA_EVENTS_DIR, OTCAPTCHA_EVENTS_MAX_MB_PER_DAY, OTCAPTCHA_EVENTS_RETAIN_DAYSempty, 512, 90Records: where, how much a day, how long.
OTCAPTCHA_REPEATSonThe within-a-day repeat counters, which are measured only.
SENTRY_DSN, SENTRY_ENVIRONMENTemptyOptional error reporting. Reports carry nothing about a visitor.

The credentials file

A JSON object: an account name, then a list of site keys with their secrets. One site key may appear under several accounts; the lookup is by key. A key must be 32 lowercase hex characters, and a file that fails to parse stops the start.

JSON
{
  "my-account": [
    { "captcha_id": "[SITE KEY]", "captcha_key": "[SECRET]" }
  ]
}

Pointing the loader at your service

The loader is built with our API address. Pass yours in apiServers, or build the loader from source with your address, which is what a fully self-contained installation does.

JavaScript
otcaptcha.init(
  { captchaId: '[SITE KEY]', apiServers: ['https://captcha.example.com/captcha/v1'] },
  (widget) => { /* as usual */ }
);

Running more than one instance

Instances share nothing but the store. Attack mode and its baselines live in memory per instance, so a surge is seen at about half its size on each of two instances; the thresholds are set with that in mind. Put the public listeners behind a load balancer that health-checks /health-check, keep the internal listeners reachable from your application servers only, and give Redis a replica with Sentinel so a lost instance moves the primary without anyone acting. Spent-pass records from the last moments before a failover can be lost, which means a pass could be checked twice in that window; nothing else needs restoring.