DEV Community

Kevin Coto๐Ÿš€๐Ÿ’ก
Kevin Coto๐Ÿš€๐Ÿ’ก

Posted on โ€ข Edited on

Using Cloudflare Turnstile with SvelteKit: A Simple Guide

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' };
    }
};
Enter fullscreen mode Exit fullscreen mode

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)

<script 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;
        }
    });

</script>

<form method="POST" use:enhance>
    {#if showCaptcha}
        <Turnstile siteKey={import.meta.env.VITE_TURNSTILE_SITEKEY}  />
    {/if}
    <button type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”น 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

Image of Quadratic

AI, code, and data connections in a familiar spreadsheet UI

Simplify data analysis by connecting directly to your database or API, writing code, and using the latest LLMs.

Try Quadratic free

Top comments (0)

Jetbrains image

Is Your CI/CD Server a Prime Target for Attack?

57% of organizations have suffered from a security incident related to DevOps toolchain exposures. It makes senseโ€”CI/CD servers have access to source code, a highly valuable asset. Is yours secure? Check out nine practical tips to protect your CI/CD.

Learn more

๐Ÿ‘‹ Kindness is contagious

If you found this post useful, consider leaving a โค๏ธ or a nice comment!

Got it