Google reCAPTCHA v3
Protect VitNode sign-up and password reset with Google reCAPTCHA v3 - register a score-based site, add your domains, and wire both keys.
reCAPTCHA v3 never asks the visitor to do anything. It watches the page, scores
the request from 0.0 to 1.0, and VitNode's API accepts anything at 0.5 or
above. Nothing to install - the provider lives in core - so the whole job is two
keys and one config block.
Quick start
If you already have a v3 site key and secret key, this is the entire integration.
RECAPTCHA_SITE_KEY=6Lc...
RECAPTCHA_SECRET_KEY=6Lc...import { buildApiConfig } from '@vitnode/core/vitnode.config'
export const vitNodeApiConfig = buildApiConfig({
plugins: [],
captcha: {
type: 'recaptcha_v3',
siteKey: process.env.RECAPTCHA_SITE_KEY,
secretKey: process.env.RECAPTCHA_SECRET_KEY,
},
})Restart the API and /register starts minting a token on submit. There is no
widget to look at - a reCAPTCHA badge in the corner of the page is the only
visible change.
Do not ship this block without both keys
An empty secretKey does not disable captcha - it breaks it. captchaMiddleware
steps aside only when the whole captcha block is missing, so a deployment with
the block and no keys rejects registration and password reset with 400.
Gate the block on the environment variable:
export const vitNodeApiConfig = buildApiConfig({
captcha: process.env.RECAPTCHA_SECRET_KEY
? {
type: 'recaptcha_v3',
siteKey: process.env.RECAPTCHA_SITE_KEY,
secretKey: process.env.RECAPTCHA_SECRET_KEY,
}
: undefined,
})reCAPTCHA v2 is not supported
type accepts 'cloudflare_turnstile' and 'recaptcha_v3', and nothing
else. VitNode loads Google's script with render=<siteKey> and mints the
token through grecaptcha.execute, which a v2 key cannot do - so a v2 pair in
this block ends as a 400, either because no token was minted or because the
verification came back without a score. Register a score based (v3) site,
or use Cloudflare Turnstile if you want a
visible widget.
Register the site
Open the reCAPTCHA admin console
Go to the reCAPTCHA admin console and sign in with the Google account that should own the keys.
Choose score based (v3) and add your domains
Give the registration a Label - internal, so your site's name will do - and pick Score based (v3) as the reCAPTCHA type.
Then list every domain the form is served from. Google allows first-level
subdomains automatically, so your-domain.com also covers
www.your-domain.com. For local development you have to add localhost
explicitly; it is not implied.
Copy the site key and the secret key
Google shows both on the next screen, and they stay available under the registration's Settings tab.
The site key is public - the API publishes it so the browser can load
Google's script. The secret key is not: it only ever travels from your API
to Google's siteverify endpoint.
Set the environment variables
These names are not magic - they are whatever you read in your config on the next step. What matters is that both are server-side variables, because the secret key must never reach a browser bundle.
RECAPTCHA_SITE_KEY=6Lc...
RECAPTCHA_SECRET_KEY=6Lc...Register the provider
Add the captcha block to your API config with type: 'recaptcha_v3'.
import { buildApiConfig } from '@vitnode/core/vitnode.config'
export const vitNodeApiConfig = buildApiConfig({
plugins: [],
captcha: {
type: 'recaptcha_v3',
siteKey: process.env.RECAPTCHA_SITE_KEY,
secretKey: process.env.RECAPTCHA_SECRET_KEY,
},
})The config is evaluated when the process boots, so restart the API rather than waiting for a hot reload to notice.
Verify it works
Ask the API what it thinks it has configured. The site key comes back on the public middleware route:
curl http://localhost:3000/api/@vitnode/core/middleware
# {"isEmail":false,"sso":[],"captcha":{"siteKey":"6Lc...","type":"recaptcha_v3"}}Then open /register. Google's badge appears in the bottom corner of the page,
the Register button is enabled as soon as the script loads, and a completed
registration means the score cleared 0.5.
The AdminCP confirms it from the other side: AdminCP → System →
Integrations (/admin/core/system/integrations) shows the Captcha card as
Active, with reCAPTCHA v3 underneath.
Google's own console is the third view. Under the registration's analytics you should see the request appear, with the score it was given.
What VitNode sends and checks
Useful when the console's numbers and your form's behaviour disagree.
| Thing | Value |
|---|---|
| Script | https://www.google.com/recaptcha/api.js?hl=<locale>&render=<siteKey> |
| Token minted by | grecaptcha.execute(siteKey, { action: 'submit' }) |
| Action name | submit - always, and the API does not verify it |
| Verify endpoint | https://www.google.com/recaptcha/api/siteverify |
| Accepted when | success === true and score >= 0.5 |
| Threshold | 0.5, hardcoded in captchaMiddleware |
| Token travels as | the header x-vitnode-captcha-token |
The visitor's IP is forwarded as remoteip, so the score reflects the person
filling the form rather than your server. It is read from the first proxy header
that has a value - x-forwarded-for first, then x-real-ip,
cf-connecting-ip and a dozen more - and falls back to 127.0.0.1 when a
request arrives with none of them.
Gotchas
The 0.5 threshold is not configurable
It is a constant in the middleware, not a config option, so you cannot loosen it for a site that keeps scoring low or tighten it for one under attack. If you need a different number, the honest options are a pull request or Cloudflare Turnstile, which has no score in the first place.
A low score looks exactly like a wrong key
Both come back as 400 Captcha validation failed, because the middleware does
not pass Google's error-codes through to the response. Check the
registration's analytics in the admin console: a request that arrived with a
low score is a scoring problem, and no request at all is a key or domain
problem.
A rejected submit leaves the button disabled
AutoForm calls onReset() after every submission, which clears isReady -
and with v3 there is no widget callback to set it back. A form that stays on
screen after a failure ("email already exists") therefore has a dead submit
button until it is remounted. Turnstile does not have this problem, because
turnstile.reset() makes the widget call back again.
Env changes need a restart
The API config reads process.env when the process starts, so a new key in
.env only takes effect after you restart the dev server.
Next
Cloudflare Turnstile
Protect VitNode sign-up and password reset with Cloudflare Turnstile - create a widget, copy both keys, and test locally with the dummy keys.
Custom forms
Solve a captcha challenge in a form you built yourself - the useCaptcha hook's isReady, getToken and onReset, and where to mount the widget.
