@onderwijsin/nuxt-turnstile
| Install | |
|---|---|
npm install @onderwijsin/nuxt-turnstile |
|
| Latest Version: | 0.3.4 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 18, 2026 |
| Links: | GitHub · npm |
@onderwijsin/nuxt-turnstile
Nuxt 4 integration for action-aware Cloudflare Turnstile protection. The module registers
@nuxtjs/turnstile and @nuxt/ui, exposes the auto-imported useTurnstile() composable, and
provides server helpers for validating single-use Turnstile tokens before a protected operation.
Installation and configuration
pnpm add @onderwijsin/nuxt-turnstile @nuxt/ui vue
The application supplies @nuxt/ui, vue as peer dependencies so module runtime code shares the
application's instances. Nuxt module registration remains automatic.
export default defineNuxtConfig({
modules: ["@onderwijsin/nuxt-turnstile"],
turnstile: {
siteKey: process.env.TURNSTILE_SITE_KEY ?? "",
secretKey: process.env.TURNSTILE_SECRET_KEY ?? ""
}
});
The module's options are:
| Option | Default | Purpose |
|---|---|---|
enabled |
true |
Enables or disables module setup and its dependencies |
siteKey |
"" |
Public key used by the Turnstile widget |
secretKey |
"" |
Server-only key used for verification |
adminToken |
"" |
Optional trusted token that bypasses verification |
adminHeaderName |
"x-admin-token" |
Header accepted for adminToken |
siteKey is public and is also passed to @nuxtjs/turnstile. Keep secretKey and adminToken
private. In production, prefer NUXT_TURNSTILE_SECRET_KEY for the private runtime value. The
module's dependency registration also requires the consuming app's normal Nuxt UI stylesheet setup.
Protecting a form
Render NuxtTurnstile, choose a stable action for the operation, wait for a token, and forward it
in x-turnstile-token. useTurnstile() is auto-imported and provides token lifecycle helpers plus
Nuxt UI toast feedback.
<script setup lang="ts">
import { TURNSTILE_TOKEN_HEADER } from "@onderwijsin/nuxt-turnstile/runtime";
const token = ref<string>();
const widget = useTemplateRef<{ reset: () => void }>("turnstile");
const { getTokenWithRetry, isEnabled, showMissingTokenErrorHint, captureTurnstileError } =
useTurnstile();
async function onSubmit() {
const currentToken = await getTokenWithRetry();
if (isEnabled.value && !currentToken) {
showMissingTokenErrorHint();
return;
}
try {
await $fetch("/api/user/session", {
method: "POST",
body: { email: "user@example.com" },
headers: currentToken ? { [TURNSTILE_TOKEN_HEADER]: currentToken } : undefined
});
} catch (error) {
if (!captureTurnstileError(error)) throw error;
} finally {
widget.value?.reset();
}
}
</script>
<template>
<form @submit.prevent="onSubmit">
<NuxtTurnstile
ref="turnstile"
v-model="token"
:options="{ action: 'magic-link', appearance: 'interaction-only' }"
/>
<UButton type="submit" label="Send magic link" />
</form>
</template>
Use getToken() when the widget is already ready, or getTokenWithRetry() when submission may race
with widget initialization. Reset the widget after every processed submission because Turnstile
tokens are single-use.
Protecting a server route
Call assertTurnstileToken before application validation, persistence, delivery, or other protected
work. The expected action must match the widget action.
import { z } from "zod";
import { assertTurnstileToken } from "@onderwijsin/nuxt-turnstile/runtime";
export default defineEventHandler(async (event) => {
await assertTurnstileToken(event, "magic-link");
const body = await readValidatedBody(event, z.object({ email: z.email() }).parse);
return await sendMagicLink(body.email);
});
The helper reads x-turnstile-token and verifies it directly with Cloudflare's Turnstile siteverify
endpoint. It rejects missing, failed, unavailable, or mismatched-action tokens. In development, an
absent secret leaves local forms usable; in production, it produces
TURNSTILE_SERVER_MISCONFIGURED.
Cloudflare's official test credentials return metadata.result_with_testing_key: true without an
action. The helper accepts only that verified test-key response without action matching, so test
credentials can exercise protected flows while normal credentials remain action-aware.
Trusted administrator bypass
Configure adminToken for trusted server-to-server or administrative requests. The configured
adminHeaderName and Authorization: Bearer <token> are both accepted. This bypass is checked
before Turnstile verification; do not expose the token in public runtime config, client code, logs,
or application aliases.
Runtime exports
The @onderwijsin/nuxt-turnstile/runtime subpath is the explicit runtime API and does not require
loading the Nuxt module entrypoint. It exports:
TURNSTILE_TOKEN_HEADER("x-turnstile-token")assertTurnstileTokencreateTurnstileErrorcreateTurnstileErrorDataisErrorWithStatusCodeTurnstileErrorCodeandTurnstileErrorDatatypes
Stable error codes are TURNSTILE_TOKEN_MISSING, TURNSTILE_VALIDATION_FAILED,
TURNSTILE_ACTION_MISMATCH, TURNSTILE_VALIDATION_UNAVAILABLE, and
TURNSTILE_SERVER_MISCONFIGURED. Apply authorization and business validation in the consuming route
after Turnstile succeeds; the module does not replace the route's existing submit or business logic.
verifyTurnstileToken is also auto-imported for server-only use when the raw Cloudflare
verification response is needed. Prefer assertTurnstileToken for protected application routes.
Compatibility
- Nuxt 4
- Node.js 24+; Node.js 22 may work but is untested and unsupported.
- Node and Cloudflare Workers-compatible server runtime
- No Sentry dependency or telemetry is included
Developed and tested against Node.js 24 and Nuxt 4.5.x. Versions outside the current CI matrix are not continuously tested. Nuxt 3 is not guaranteed.
Related Packages
Server-safe Directus REST integration for Nuxt applications.
Nuxt module for runtime theme selection and custom color editing in Nuxt UI.
Shared runtime and Nuxt module utilities for Onderwijs in modules.
Provider-independent newsletter signup endpoint for Nuxt.
Nuxt module that exposes configurable system health endpoints.
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.3.4 | ^3.5.0 | MIT | |
| 0.3.3 | ^3.0 | MIT | |
| 0.3.2 | ^3.0 | MIT | |
| 0.3.1 | ^3.0 | MIT | |
| 0.3.0 | ^3.0 | MIT | |
| 0.2.10 | ^3.0 | MIT | |
| 0.2.9 | ^3.0 | MIT | |
| 0.2.8 | ^3.0 | MIT | |
| 0.2.7 | ^3.0 | MIT | |
| 0.2.6 | ^3.0 | MIT | |
| 0.2.5 | ^3.0 | MIT | |
| 0.2.4 | ^3.0 | MIT | |
| 0.2.3 | ^3.0 | MIT | |
| 0.2.2 | ^3.0 | MIT | |
| 0.2.1 | ^3.0 | MIT | |
| 0.2.0 | ^3.0 | MIT |