Google reCAPTCHA
captcha.recaptcha() creates a Captcha guard backed by Google reCAPTCHA.
Layeron uses an existing Google widget, stores the provider secret as a Layeron
secret reference, and verifies tokens through Google’s Siteverify API.
Create A Guard
Section titled “Create A Guard”Create the widget in Google reCAPTCHA first. Then provide the 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.recaptcha({ name: "signup", sitekey: "6Lc...", secret: { kind: "secret_ref", name: "RECAPTCHA_SIGNUP_SECRET" }, domains: ["app.example.com"], action: "signup", minScore: 0.7,})At deploy time, Layeron injects the reCAPTCHA secret and sitekey into the Gateway Worker:
LAYERON_CAPTCHA_DEFAULT_SIGNUP_SECRETLAYERON_CAPTCHA_DEFAULT_SIGNUP_SITEKEYThe public sitekey can be rendered in the browser. The secret is injected only into the runtime surface that performs Siteverify.
Protect A Route
Section titled “Protect A Route”Add the guard to the route use list for automatic verification.
app.post("/signup", { use: [signupCaptcha] }, async () => { return { ok: true }})Gateway extracts the token, calls Google Siteverify, validates hostname,
action, and minScore when configured, and runs the route handler after
verification succeeds.
Submit The Token
Section titled “Submit The Token”Google writes g-recaptcha-response for normal reCAPTCHA form submissions.
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<form method="post" action="/signup"> <input name="email" type="email" autocomplete="email" /> <div class="g-recaptcha" data-sitekey="6Lc..."></div> <button>Create account</button></form>Default token sources:
{ header: "g-recaptcha-response", form: "g-recaptcha-response", json: "gRecaptchaResponse",}For reCAPTCHA v3 and JSON APIs, execute the widget on the client and send the returned token in the configured JSON property.
await fetch("/api/signup", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email, gRecaptchaResponse: token, }),})Use tokenFrom when the frontend uses different names.
captcha.recaptcha({ name: "signup", sitekey: "6Lc...", secret: { kind: "secret_ref", name: "RECAPTCHA_SIGNUP_SECRET" }, tokenFrom: { header: "x-captcha-token", json: "captchaToken", },})Use Manual Verification
Section titled “Use Manual Verification”Use manual verification when the handler controls the response shape or token location.
app.use(signupCaptcha)
app.post("/api/signup", async (request) => { const body = await request.json() const result = await signupCaptcha.verifyToken(body.captchaToken, { request, hostname: "app.example.com", action: "signup", minScore: 0.8, })
if (!result.ok) { return result.response() }
return { ok: true, score: result.score, }})Manual validation can override hostname, action, and minScore for a single
call.
Score Validation
Section titled “Score Validation”minScore accepts a number from 0 to 1. reCAPTCHA v3 returns a score where
higher values mean stronger confidence.
const checkoutCaptcha = captcha.recaptcha({ name: "checkout", sitekey: "6Lc...", secret: { kind: "secret_ref", name: "RECAPTCHA_CHECKOUT_SECRET" }, action: "checkout", minScore: 0.8,})Use different Captcha instances for flows with different risk levels. A checkout flow can use a higher threshold than a newsletter signup flow.
Production Checklist
Section titled “Production Checklist”- Set
domainsfor every public host that can submit tokens. - Set
actionfor each reCAPTCHA v3 flow and keep it aligned with the client action value. - Set
minScorefor reCAPTCHA v3 flows that should reject low-confidence traffic. - Keep the Google provider secret in a Layeron secret reference.
- Use separate Captcha instances for signup, contact, checkout, and password reset flows.
- Keep
failure.bodystable for clients.
Troubleshooting
Section titled “Troubleshooting”missing_token means the route did not receive g-recaptcha-response or the
configured token field.
hostname_mismatch means the returned hostname does not match domains or the
request host.
action_mismatch means the returned action does not match
captcha.recaptcha({ action }).
score_missing means Layeron expected a reCAPTCHA score and Google returned a
response without score.
score_below_threshold means Google returned a score below minScore.
invalid-input-response means Google rejected the response token.