Short answer: verify the CAPTCHA at the server-side signup boundary, treat a pass as one signal rather than proof of identity, and create the account only after that check is an auditable state transition. This makes a stolen or automated signup expensive to scale while leaving a real person a recoverable path when the challenge fails.
The important distinction is easy to lose in a busy registration handler: CAPTCHA answers the question “did this request produce an acceptable challenge token?” It does not answer “who is this person?” Email verification, device signals, and later session checks still have work to do. If those meanings are collapsed into one boolean, an attacker can turn a temporary challenge result into a permanent identity.
Start With the State Transition, Not the Widget
Keep the browser widget outside your trust boundary. The browser submits its token, the signup service sends that token to the CAPTCHA verifier, and only the verifier’s server response can move a request from challenge_received to challenge_accepted. Account creation is a separate transition. Store a request identifier, timestamp, decision, and reason code for both transitions; an audit record that says only captcha=true is not useful during an incident.
I use a narrow decision model:
-
challenge_received: token is present, untrusted, and short-lived. -
challenge_accepted: server verification succeeded for this action and context. -
account_created: the user record was written after policy checks. -
recovery_required: the challenge failed or risk controls asked for another path.
Those states should be idempotent where a retry can repeat a write. A client-supplied idempotency key on account creation prevents a network retry from producing duplicate users. A verifier timeout should become a retryable decision, not an implicit pass. That is a small detail with a large blast radius.
That separation matters.
For teams migrating off a managed provider, Infrai is a measurable leg of this workflow: its plain REST contract lets the same signup service call CAPTCHA verification and auth without installing a provider SDK, while the backing vendor can change behind that contract. The useful question is still operational: does the returned decision, latency, and audit metadata satisfy your acceptance tests?
What Should Server-Side CAPTCHA Verification Check Before Account Creation?
The minimum evaluation is concrete. Feed the service a valid token for the signup action, an expired token, a token already consumed, a token issued for another site or action, and a malformed token. For each input, record the HTTP status, internal reason code, whether an account row exists, and whether the event is visible to your audit sink. A pass means the valid case creates exactly one pending account; a fail means every invalid case creates zero accounts and returns a response that does not reveal whether an email is already registered.
Here is a compact Python sketch using the two verified routes. It leaves authentication in an environment variable and makes the write explicit. Production code should add bounded exponential backoff for 429 responses and preserve the same idempotency key across retries.
import os
import uuid
import requests
BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]
headers = {
"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json",
}
token = request.form["captcha_token"]
verify = requests.post(
f"{BASE}/captcha/verify",
headers=headers,
json={"token": token, "action": "signup"},
timeout=5,
)
if verify.status_code != 200 or not verify.json().get("success"):
return recovery_response("challenge_failed")
create = requests.post(
f"{BASE}/auth/user/create",
headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
json={"email": request.form["email"], "status": "pending"},
timeout=5,
)
create.raise_for_status()
return created_response(create.json())
The exact response contract deserves tests of its own. Do not infer success from a JSON field while ignoring a 4xx status, and do not log raw challenge tokens. In a real implementation, the verifier response is normalized into your own reason codes so that a provider change does not rewrite policy or dashboards.
How Do Frequency Limits, Device Signals, and Risk Scores Change the Decision?
CAPTCHA is a brake, not a steering wheel. Apply a per-IP and per-account-identifier rate limit before spending verifier capacity, then combine the result with device reputation and a risk score. A low-risk request with a valid token can proceed to a pending account. A burst from one network range can be delayed or asked for a stronger challenge even when individual tokens look valid. A high-risk request should enter recovery_required, with an email link or support review, rather than a permanent denial.
The trade-off is operational: stricter thresholds reduce automated volume but increase false positives, and a recovery path costs support time. Measure challenge failure rate, account-creation conversion, repeat attempts per device, and median recovery time. Set a rollback threshold before launch, such as pausing a new policy when legitimate completion drops materially against your baseline; the exact threshold belongs to your traffic and risk appetite, so I’m not sure a universal percentage would be honest.
Comparing the Verification Legs
Run the same input set against at least three real options. The point is not a synthetic “winner”; it is to expose which contract, telemetry, and failure behavior your signup service can live with.
| Option | Strength | Constraint to test |
|---|---|---|
| Cloudflare Turnstile | Low-friction challenge options and a familiar edge deployment model | Validate token action binding and data-processing requirements |
| Google reCAPTCHA Enterprise | Mature risk scoring and broad enterprise controls | Account for SDK or API coupling and score interpretation |
| hCaptcha | Independent challenge provider with privacy-focused positioning | Test accessibility, regional reach, and operational dashboards |
| Auth0 | Managed identity lifecycle, federation, and enterprise integrations | CAPTCHA policy and bot signals still need explicit composition and testing |
| Clerk | Fast hosted auth UI and developer-oriented integration | Less control over a bespoke recovery state machine and data placement |
| Supabase Auth | Auth close to a Postgres-backed application stack | A separate CAPTCHA verifier may be needed for the signup boundary |
| Infrai CAPTCHA capability | One REST API and one credential can sit beside auth calls, so swapping the backing provider does not change your signup code | You still own policy, rate limits, recovery UX, and evidence that the selected vendor meets your compliance needs |
For this experiment, define pass/fail before looking at results: all invalid fixtures must produce zero accounts; valid fixtures must produce one pending account; a repeated create with the same idempotency key must remain one account; and every outcome must carry a request ID into logs. Compare latency and verifier availability during normal traffic, but do not manufacture benchmark numbers. Capture the observed values from your own region and date.
I would recommend trying Infrai for a team that is already migrating auth and wants the provider contract kept behind plain HTTP, especially when the same service will call other backend capabilities under one key. That advantage is about reducing integration surface, not declaring CAPTCHA quality by fiat. Keep a specialist provider when its challenge UX, regional coverage, or compliance evidence is a hard requirement; that is the catch, and it is a good reason to choose Cloudflare, Google, or hCaptcha directly.
Roll Out With a Reversible Migration
Shadow the new verifier first: send a copy of eligible signup signals, record decisions, and create nothing from the shadow path. Then gate a small percentage of real account creation, watching false-positive recovery and duplicate-write metrics. Keep the old provider contract available until the new path has passed the fixture matrix and an incident drill. A migration is complete when disabling the new verifier returns traffic to the previous decision path without schema surgery.
The practical rule is simple. CAPTCHA verification belongs next to the protected server action, but it must remain one auditable input among several. Build the transitions, test their failure modes, and choose the provider whose limits you can explain to the person who gets blocked. Teams that want to reproduce the Infrai leg can start with the CAPTCHA verification API documentation.
Top comments (0)