nuxt4-turnstile
v1.0.4
Published
Cloudflare Turnstile integration for Nuxt 4 - A privacy-focused CAPTCHA alternative
Maintainers
Readme
Nuxt4 Turnstile
Cloudflare Turnstile integration for Nuxt 4 - A privacy-focused CAPTCHA alternative.
✨ Features
- 🔒 Privacy-focused - No tracking, no cookies, no fingerprinting
- 🚀 Nuxt 4 Compatible - Built specifically for Nuxt 4
- 📦 Auto-imported - Components and composables ready to use
- 🛡️ Server Validation - Built-in server-side token verification
- 🎨 Customizable - Theme, size, appearance options
- ♻️ Auto-refresh - Automatically refreshes tokens before expiry
- 📝 TypeScript - Full TypeScript support
📦 Installation
# npm
npm install nuxt4-turnstile
# pnpm
pnpm add nuxt4-turnstile
# bun
bun add nuxt4-turnstile⚙️ Configuration
Add nuxt4-turnstile to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['nuxt4-turnstile'],
turnstile: {
siteKey: 'your-site-key', // Get from Cloudflare Dashboard
},
runtimeConfig: {
turnstile: {
// Override with NUXT_TURNSTILE_SECRET_KEY env variable
secretKey: '',
},
},
})Get Your Keys
- Go to Cloudflare Turnstile
- Create a new site
- Copy your Site Key (public) and Secret Key (server-side)
Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| siteKey | string | '' | Your Turnstile site key (required) |
| secretKey | string | '' | Your Turnstile secret key (server-side) |
| addValidateEndpoint | boolean | false | Add /_turnstile/validate endpoint |
| appearance | 'always' \| 'execute' \| 'interaction-only' | 'always' | Widget visibility |
| theme | 'light' \| 'dark' \| 'auto' | 'auto' | Widget theme |
| size | 'normal' \| 'compact' \| 'flexible' | 'normal' | Widget size |
| retry | 'auto' \| 'never' | 'auto' | Retry behavior |
| retryInterval | number | 8000 | Retry interval in ms |
| refreshExpired | number | 250 | Auto-refresh before expiry (seconds) |
| language | string | 'auto' | Widget language |
🚀 Usage
Component
Use the auto-imported <NuxtTurnstile> component:
<template>
<form @submit.prevent="onSubmit">
<NuxtTurnstile v-model="token" />
<button type="submit" :disabled="!token">Submit</button>
</form>
</template>
<script setup>
const token = ref('')
async function onSubmit() {
// Send token to your server for verification
await $fetch('/api/contact', {
method: 'POST',
body: { token, message: '...' }
})
}
</script>[!TIP] If you are using a form component that restricts content (like Nuxt UI Pro's
<AuthForm>), make sure to place<NuxtTurnstile>in a supported slot (e.g.,#validationor#footer) instead of the default slot.
Component Props
| Prop | Type | Description |
|------|------|-------------|
| v-model | string | Two-way binding for the token |
| element | string | HTML element to use (default: 'div') |
| options | TurnstileOptions | Override module options |
| action | string | Custom action for analytics |
| cData | string | Custom data payload |
Component Events
| Event | Payload | Description |
|-------|---------|-------------|
| @verify | token: string | Token generated |
| @expire | - | Token expired |
| @error | error: Error | Error occurred |
| @before-interactive | - | Before challenge |
| @after-interactive | - | After challenge |
| @unsupported | - | Browser unsupported |
Component Methods (via ref)
<template>
<NuxtTurnstile ref="turnstile" v-model="token" />
<button @click="turnstile?.reset()">Reset</button>
</template>
<script setup>
const turnstile = ref()
const token = ref('')
</script>| Method | Description |
|--------|-------------|
| reset() | Reset widget for re-verification |
| remove() | Remove widget from DOM |
| getResponse() | Get current token |
| isExpired() | Check if token is expired |
| execute() | Execute invisible challenge |
🛡️ Server Verification
Using the Built-in Endpoint
Enable the validation endpoint in your config:
export default defineNuxtConfig({
turnstile: {
siteKey: '...',
addValidateEndpoint: true,
},
})Then call it from your client:
const { success } = await $fetch('/_turnstile/validate', {
method: 'POST',
body: { token }
})Using the Helper Function
In your server routes:
// server/api/contact.post.ts
export default defineEventHandler(async (event) => {
const { token, message } = await readBody(event)
// Verify the token
const result = await verifyTurnstileToken(token)
if (!result.success) {
throw createError({
statusCode: 400,
message: 'Invalid captcha'
})
}
// Continue with your logic...
return { success: true }
})Verification Options
await verifyTurnstileToken(token, {
secretKey: 'override-secret', // Override config secret
remoteip: '1.2.3.4', // Client IP for security
action: 'login', // Validate expected action
cdata: 'user-123', // Validate expected cdata
})🔧 Composable
Use the useTurnstile composable for programmatic access:
const {
isAvailable, // Is Turnstile loaded?
siteKey, // Get site key
verify, // Verify token via endpoint
render, // Render widget programmatically
reset, // Reset widget
remove, // Remove widget
getResponse, // Get token
isExpired, // Check expiry
execute, // Execute invisible challenge
} = useTurnstile()🌐 Environment Variables
Override configuration with environment variables:
NUXT_PUBLIC_TURNSTILE_SITE_KEY=your-site-key
NUXT_TURNSTILE_SECRET_KEY=your-secret-key📝 TypeScript
Types are automatically available:
import type {
TurnstileInstance,
TurnstileOptions,
TurnstileVerifyResponse,
} from 'nuxt4-turnstile'🧪 Testing
For testing, use Cloudflare's test keys:
| Key | Behavior |
|-----|----------|
| 1x00000000000000000000AA | Always passes |
| 2x00000000000000000000AB | Always blocks |
| 3x00000000000000000000FF | Forces interactive challenge |
Secret test keys:
| Key | Behavior |
|-----|----------|
| 1x0000000000000000000000000000000AA | Always passes |
| 2x0000000000000000000000000000000AA | Always fails |
| 3x0000000000000000000000000000000AA | Yields token spend error |
📄 License
MIT License - see LICENSE for details.
