Skip to content

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 the widget in Google reCAPTCHA first. Then provide the public sitekey and a Layeron secret reference.

Terminal window
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:

Terminal window
LAYERON_CAPTCHA_DEFAULT_SIGNUP_SECRET
LAYERON_CAPTCHA_DEFAULT_SIGNUP_SITEKEY

The public sitekey can be rendered in the browser. The secret is injected only into the runtime surface that performs Siteverify.

Add the guard to the route use list for automatic verification.

Terminal window
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.

Google writes g-recaptcha-response for normal reCAPTCHA form submissions.

Terminal window
<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:

Terminal window
{
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.

Terminal window
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.

Terminal window
captcha.recaptcha({
name: "signup",
sitekey: "6Lc...",
secret: { kind: "secret_ref", name: "RECAPTCHA_SIGNUP_SECRET" },
tokenFrom: {
header: "x-captcha-token",
json: "captchaToken",
},
})

Use manual verification when the handler controls the response shape or token location.

Terminal window
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.

minScore accepts a number from 0 to 1. reCAPTCHA v3 returns a score where higher values mean stronger confidence.

Terminal window
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.

  • Set domains for every public host that can submit tokens.
  • Set action for each reCAPTCHA v3 flow and keep it aligned with the client action value.
  • Set minScore for 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.body stable for clients.

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.