Captcha Protection for your API Routes
Learn how to set up captcha protection for your API routes.
For captcha protection, we use Cloudflare Turnstile.
How to set up captcha protection for your API routes
Learn how to set up captcha protection for your API routes
Setting up the environment variables
To enable it, you need to set the following environment variables:
CAPTCHA_SECRET_TOKEN=VITE_CAPTCHA_SITE_KEY=VITE_CAPTCHA_WIDGET_SIZE=invisibleYou can find the CAPTCHA_SECRET_TOKEN in the Turnstile configuration. The VITE_CAPTCHA_SITE_KEY is public and safe to share. Instead, the CAPTCHA_SECRET_TOKEN should be kept secret.
VITE_CAPTCHA_WIDGET_SIZE controls the visual appearance of the Turnstile widget. The default is invisible, which is correct for Non-interactive and Invisible site key types. If you use a Managed site key (where Cloudflare may require the user to interact with a checkbox), set this to normal or compact so the widget is visible.
This guide assumes you have correctly set up your Turnstile configuration. If you haven't, please refer to the https://developers.cloudflare.com/turnstile.
Enabling the captcha protection
When you set the token in the environment variables, the kit will automatically protect your API routes with captcha.
Important: You also need to set the token in the Supabase Dashboard!
Using Captcha in Your Components
The kit provides two clean APIs for captcha integration depending on your use case.
Option 1: Using the useCaptcha Hook
For auth containers and simple forms, use the useCaptcha hook for zero-boilerplate captcha integration:
import { useCaptcha } from '@kit/auth/captcha/client';function MyComponent({ captchaSiteKey }) { const captcha = useCaptcha({ siteKey: captchaSiteKey }); const handleSubmit = async (data) => { try { await myServerFunction({ ...data, captchaToken: captcha.token, }); } finally { // Always reset after submission captcha.reset(); } }; return ( <form onSubmit={handleSubmit}> {captcha.field} <button type="submit">Submit</button> </form> );}The useCaptcha hook returns:
token- The current captcha tokenreset()- Function to reset the captcha widgetfield- The captcha component to render
Option 2: React Hook Form Integration
For forms using react-hook-form, use the CaptchaField component with automatic form integration:
import { useForm } from 'react-hook-form';import { CaptchaField } from '@kit/auth/captcha/client';function MyForm() { const form = useForm({ defaultValues: { message: '', captchaToken: '', }, }); const handleSubmit = async (data) => { try { await myServerFunction(data); form.reset(); // Automatically resets captcha too } catch (error) { // Handle error } }; return ( <Form {...form}> <form onSubmit={form.handleSubmit(handleSubmit)}> {/* Your form fields */} <CaptchaField siteKey={config.captchaSiteKey} control={form.control} name="captchaToken" /> <button type="submit">Submit</button> </form> </Form> );}When using React Hook Form integration:
- The captcha token is automatically set in the form state
- Calling form.reset() automatically resets the captcha
- No manual state management needed
Using with Server Functions
Include the captchaToken in your server function schema, then verify it with verifyCaptchaToken from @kit/auth/captcha/server at the top of the handler. Gate the check on CAPTCHA_SECRET_TOKEN so the function still works locally when no captcha is configured:
import { createServerFn } from '@tanstack/react-start';import * as z from 'zod';import { verifyCaptchaToken } from '@kit/auth/captcha/server';import { errorMiddleware } from '@kit/function-middleware/server';const MySchema = z.object({ message: z.string(), captchaToken: z.string(),});export const myServerFunction = createServerFn({ method: 'POST' }) // Normalizes failures so raw captcha errors are not serialized to the client .middleware([errorMiddleware]) .validator(MySchema) .handler(async ({ data }) => { // When a CAPTCHA is configured, verify the token before doing any work. // Throws if the token is missing or invalid. No-op when unconfigured. if (process.env.CAPTCHA_SECRET_TOKEN) { await verifyCaptchaToken(data.captchaToken); } // Your handler code - safe to run once the captcha passes console.log(data.message); });verifyCaptchaToken:
- Sends the
captchaTokento Cloudflare Turnstile'ssiteverifyendpoint - Uses your
CAPTCHA_SECRET_TOKENto authenticate the verification - Throws an error if verification fails
Call the function from the client with useServerFn and a TanStack Query mutation, passing the token collected by useCaptcha or CaptchaField.
Important Notes
- Token Validity: A captcha token is valid for one request only
- Always Reset: Always call captcha.reset() (or form.reset() with RHF) after submission, whether successful or not
- Automatic Renewal: The library automatically renews tokens when needed, but you must reset after consumption
Verifying the Token Manually
If you need to verify the captcha token manually server-side (e.g., in API routes), use:
import { verifyCaptchaToken } from '@kit/auth/captcha/server';async function myApiHandler(request: Request) { const token = request.headers.get('x-captcha-token'); // Throws an error if invalid await verifyCaptchaToken(token); // Your API logic}Note: The same verifyCaptchaToken helper is used inside server functions and server routes. Call it once at the top of your handler (gated on CAPTCHA_SECRET_TOKEN) before performing any work.