This post explains how to integrate Cloudflare Turnstile with a SvelteKit form using use:enhance
, ensuring multiple submissions work correctly.
🔹 Step 1: Validate Turnstile Tokens on the Server
To prevent spam, we must verify Turnstile tokens on the backend before accepting form submissions.
Backend (+page.server.ts
or +server.ts
)
import { SECRET_TURNSTILE_KEY } from '$env/static/private';
async function validateToken(token: string): Promise<boolean> {
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
secret: SECRET_TURNSTILE_KEY,
response: token
})
});
const data = await response.json();
return data.success;
}
export const actions = {
default: async ({ request }) => {
const formData = await request.formData();
const token = formData.get('cf-turnstile-response')?.toString() || '';
if (!token || !(await validateToken(token))) {
return { success: false, message: 'Invalid CAPTCHA' };
}
return { success: true, message: 'Form submitted successfully' };
}
};
What This Does:
- Extracts the Turnstile response token from
formData
. - Sends it to Cloudflare for verification.
- Rejects the request if the token is invalid.
🔹 Step 2: Integrate Turnstile in the Svelte Frontend
Frontend (+page.svelte
)
<span class="na">lang="ts">
import { Turnstile } from 'svelte-turnstile';
import { enhance } from '$app/forms';
let showCaptcha = $state(true)
;
let { form } = $props();
$effect(() => {
if (form) {
// Hide and re-show the CAPTCHA to allow multiple submissions
showCaptcha = false;
setTimeout(() => (showCaptcha = true), 0);
form = null;
}
});
method="POST" use:enhance>
{#if showCaptcha}
siteKey={import.meta.env.VITE_TURNSTILE_SITEKEY} />
{/if}
type="submit">Submit
🔹 Why $effect(() => { showCaptcha = false; setTimeout(() => (showCaptcha = true), 0); })
?
Problem:
When using use:enhance
, SvelteKit does not reload the page after form submission. However, Cloudflare Turnstile only allows a token to be used once. If you try submitting the form again without refreshing, Turnstile will reject the request.
Solution:
- After a successful submission,
showCaptcha = false
hides the Turnstile component. -
setTimeout(() => (showCaptcha = true), 0);
forces a re-render, generating a new token. - This allows multiple form submissions without a full page refresh.
🎯 Summary
✅ Server-side token validation prevents spam.
✅ Frontend integration with svelte-turnstile
ensures security.
✅ showCaptcha
reset trick allows multiple submissions when using use:enhance
.
Now your SvelteKit form is secure, user-friendly, and supports multiple submissions seamlessly! 🚀
Check out the full source code on GitHub
For more look into thekoto.dev/blog