Captcha
Captcha
Protect registration, password reset, and custom API routes against bots with Cloudflare Turnstile or Google reCAPTCHA v3.
VitNode provides built-in bot protection. When enabled in your API configuration, gated routes automatically demand and verify a challenge token before handler execution.
Quick start
1. Configure Captcha in API Config
Add the captcha configuration to apps/api/src/vitnode.api.config.ts:
import { buildApiConfig } from "@vitnode/core/vitnode.config"
export const vitNodeApiConfig = buildApiConfig({
captcha: process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY
? {
type: "cloudflare_turnstile",
siteKey: process.env.CLOUDFLARE_TURNSTILE_SITE_KEY,
secretKey: process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY,
}
: undefined,
})Built-in user registration and password recovery routes are protected immediately:

2. Protect Custom API Routes
Enable captcha verification on any custom Hono endpoint with withCaptcha: true:
import { buildRoute } from "@/api/builders"
export const createCommentRoute = buildRoute({
route: {
withCaptcha: true,
method: "post",
path: "/",
responses: { 201: { description: "Comment created" } },
},
handler: async (c) => {
// Execution only reaches here if the captcha token is valid
},
})3. Render the Frontend Widget
Pass the app's captcha config to AutoForm. It loads the widget, keeps the submit button disabled until the captcha is ready, and hands onSubmit a fresh token to forward as the fetcher's captchaToken:
import { AutoForm } from "@vitnode/core/components/form/auto-form"
import { AutoFormTextarea } from "@vitnode/core/components/form/fields/textarea"
import { useMiddlewareConfigQuery } from "@vitnode/core/tanstack/auth"
import { fetcher } from "@vitnode/core/tanstack/fetcher"
import { z } from "zod"
const formSchema = z.object({ text: z.string().min(1) })
export const CommentForm = () => {
const { data: config } = useMiddlewareConfigQuery()
return (
<AutoForm
captcha={config.captcha}
fields={[
{
id: "text",
component: (props) => <AutoFormTextarea {...props} label="Comment" />,
},
]}
formSchema={formSchema}
onSubmit={async (values, form, { captchaToken }) => {
await fetcher({
plugin: "@acme/blog",
method: "post",
module: "comments",
path: "/",
captchaToken,
args: { body: values },
})
}}
/>
)
}useMiddlewareConfigQuery suspends until the config loads, so render the form under a Suspense boundary. Outside AutoForm, use the useCaptcha hook from @vitnode/core/hooks/use-captcha: it returns isReady, getToken() and onReset(), and for Turnstile your form must contain an element with id="vitnode_captcha".
Supported Providers
Cloudflare Turnstile
Privacy-friendly visible widget with dummy test keys for local dev
Google reCAPTCHA v3
Invisible background score verification (0.5 default threshold)