Get Started
This guide walks through adding CAPTCHA verification to a public route. You will create a Captcha guard from a provider page, attach it to a route, submit the provider token from the client, and use manual verification when a route needs custom control flow.
1. Choose A Provider
Section titled “1. Choose A Provider”Use one provider page to create the signupCaptcha guard:
- Cloudflare Turnstile creates and maintains Turnstile widgets during deploy, and can adopt an existing Turnstile widget.
- Google reCAPTCHA uses an existing Google reCAPTCHA widget with a public sitekey and a Layeron secret reference.
import { backend } from "@layeron/core"import { captcha } from "@layeron/modules"
const app = backend({ project: "shop" })
const signupCaptcha = captcha.turnstile({ name: "signup", domains: ["app.example.com"], action: "signup",})2. Protect A Route Automatically
Section titled “2. Protect A Route Automatically”Add the guard to the route use list.
app.post("/signup", { use: [signupCaptcha] }, async () => { return { ok: true }})At deploy time, Layeron creates or adopts the provider resource and adds Gateway Worker bindings:
LAYERON_CAPTCHA_DEFAULT_SIGNUP_SITEKEYLAYERON_CAPTCHA_DEFAULT_SIGNUP_SECRETUse the CLI after deployment to print the public sitekey for frontend rendering:
layer captcha sitekeys --name signupAt request time, Gateway extracts the CAPTCHA token, verifies it with the provider Siteverify API, validates configured provider claims, and continues to the handler when verification succeeds.
Use automatic protection for endpoints where the entire route should be gated, such as signup, contact forms, password reset requests, and checkout steps. The handler does not need to call Captcha directly in this path.
3. Submit A Token From The Client
Section titled “3. Submit A Token From The Client”Render the provider widget on the page and submit the token to your Layeron route. Each provider page lists its default token names.
For JSON requests, send the token in the body or header.
await fetch("/api/signup", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email, cfTurnstileResponse: captchaToken, }),})Customize them when your frontend uses different names.
captcha.turnstile({ name: "signup", tokenFrom: { header: "x-captcha-token", json: "captchaToken", },})4. Use Manual Verification
Section titled “4. Use Manual Verification”Use manual verification when the route needs custom control flow, staged form handling, multiple challenge points, or a response format controlled by your handler.
const checkoutCaptcha = captcha.turnstile({ name: "checkout", domains: ["app.example.com"], action: "checkout", failure: { status: 400, body: { error: "human_verification_required" }, },})
app.use(checkoutCaptcha)
app.post("/checkout", async (request) => { const result = await checkoutCaptcha.verify(request)
if (!result.ok) { return result.response() }
return { ok: true }})For JSON APIs and SPAs, pass the token explicitly:
app.post("/api/signup", async (request) => { const body = await request.json() const result = await signupCaptcha.verifyToken(body.captchaToken, { request, action: "signup", hostname: "app.example.com", })
if (!result.ok) { return result.response() }
return { ok: true }})Manual verification returns a structured result:
{ ok: boolean success: boolean errorCodes: string[] hostname?: string action?: string cData?: string score?: number challengeTs?: string response(): Response}verify(request) extracts the token from the configured tokenFrom sources.
verifyToken(token, input) uses the explicit token and still uses request for
remote IP, request identity, and route-host validation. Pass hostname,
action, cData, or minScore when the handler needs stricter per-call
validation than the module defaults.
5. Set Production Defaults
Section titled “5. Set Production Defaults”Configure domains for production routes so Layeron validates the returned
provider hostname against an explicit allowlist.
captcha.turnstile({ name: "signup", domains: ["app.example.com"], action: "signup",})Use distinct Captcha instances for flows with different risk profiles.
const signupCaptcha = captcha.turnstile({ name: "signup", action: "signup" })const checkoutCaptcha = captcha.turnstile({ name: "checkout", action: "checkout" })const passwordResetCaptcha = captcha.turnstile({ name: "password-reset", action: "password_reset" })Recommended production settings:
- Use
domainsfor public hosts and preview hosts that should accept tokens. - Use stable
namevalues so deployment state can track widget ownership. - Keep
failure.bodystable for API clients. - Send the client token over HTTPS and verify it once per sensitive action.
- Treat
invalid-input-responseandtimeout-or-duplicateas retryable user flows on the client.
Troubleshooting
Section titled “Troubleshooting”missing_token means the route did not receive a token in any configured
source. Check the client widget render and the tokenFrom names.
hostname_mismatch means the provider returned a hostname outside the configured
domain policy. Check domains, the request host, and any preview domain setup.
action_mismatch means the widget action and server action differ. Keep
data-action and captcha.turnstile({ action }) or
captcha.recaptcha({ action }) aligned.