CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) is a security feature that helps protect your application from automated abuse, spam, and malicious bots.
We use Cloudflare Turnstile CAPTCHA to protect forms from automated abuse and spam.
Setup
Get Cloudflare Turnstile keys at cloudflare.com/products/turnstile
Add environment variables:
# Client-side (safe to expose)
NEXT_PUBLIC_CAPTCHA_SITE_KEY=your_site_key
# Server-side (keep secret)
CAPTCHA_SECRET_KEY=your_secret_keyClient Setup
Provider
Wrap your app with CaptchaProvider:
import { CaptchaProvider } from '@kit/auth/www/captcha/client';
export function RootLayout({ children }) {
return <CaptchaProvider siteKey={process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY}>{children}</CaptchaProvider>;
}Options
Customize CAPTCHA appearance:
<CaptchaProvider
siteKey={process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY}
options={{
options: {
size: 'normal', // 'normal', 'invisible', 'compact'
theme: 'light', // 'light', 'dark'
language: 'en',
},
}}
>
{children}
</CaptchaProvider>Hook usage
Use the hook in forms:
'use client'; import { useCaptchaToken } from '@kit/auth/www/captcha/client'; export function ContactForm() { const { captchaToken, resetCaptchaToken } = useCaptchaToken(); const handleSubmit = async (formData) => { try { await submitAction({ ...formData, captchaToken }); resetCaptchaToken(); } catch (error) { resetCaptchaToken(); // Allow retry } }; return ( <form onSubmit={handleSubmit}> {/* Your form fields */} <button type="submit">Submit</button> </form> ); }
Server Setup
Use captchaActionClient for protected actions:
'use server'; import { captchaActionClient } from '@kit/auth/www/captcha/server'; import { z } from 'zod'; const schema = z.object({ email: z.string().email(), message: z.string().min(10), // captchaToken is optional and verified automatically }); export const submitContact = captchaActionClient .metadata({ actionName: 'submitContact' }) .schema(schema) .action(async ({ parsedInput: { email, message } }) => { // CAPTCHA already verified if token provided // Your logic here console.log(`Contact: ${email} - ${message}`); return { success: true, message: 'Sent!' }; });
For authenticated actions with CAPTCHA:
import { authCaptchaActionClient } from '@kit/auth/www/captcha/server';
export const protectedAction = authCaptchaActionClient
.metadata({ actionName: 'protectedAction' })
.schema(schema)
.action(async ({ ctx: { db }, parsedInput }) => {
// Has CAPTCHA verification + authenticated database access
const user = await db.user.get();
return { user };
});Manual Verification
For custom server logic:
import { verifyCaptcha } from '@kit/auth/www/captcha/server';
export async function customAction(formData: FormData) {
const token = formData.get('captchaToken');
try {
await verifyCaptcha(token);
// Proceed with action
return { success: true };
} catch (error) {
throw new Error('CAPTCHA verification failed');
}
}Structure
How the folder structure and files are designed.
Content Table
A content table component to display a table of content.
How is this guide?
Last updated on 2/27/2026