Documentation

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:

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:

Cloudflare Turnstile widget showing success checkmark on VitNode sign-up form

2. Protect Custom API Routes

Enable captcha verification on any custom Hono endpoint with withCaptcha: true:

plugins/blog/src/api/modules/comments/routes/create.route.ts
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:

plugins/blog/src/views/comment-form.tsx
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

Learn More