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:

  1. Extracts the Turnstile response token from formData.
  2. Sends it to Cloudflare for verification.
  3. 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:

  1. After a successful submission, showCaptcha = false hides the Turnstile component.
  2. setTimeout(() => (showCaptcha = true), 0); forces a re-render, generating a new token.
  3. 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