Web Form to Discord With CloudFlare Turnstile Captcha
So I set up a TF2 server a while back, and password protected it. I use it mainly for my own enjoyment, but figured sometimes friends or family would want to play. It's got bots that auto-bail when real players join, keeping it full at 24 players at all times. Anyhow, I figured I would set up a site about it for a number of reasons, and did some other cool stuff, like the kill log scrolls, a list of maps show, etc.
One of the things I implemented was a small HTML form on the index page to allow users to request a password to the server. The form is filled out and submitted, and the info is sent to a channel in my Discord server. Natrually, I also had to protect it, and I opted to use somethign a little more complicated to protect the form: CloudFlare Turnstile. Here is how I did it:
General Information
This method avoids exposing sensitive information like your Discord webhook URL on the frontend. It uses a Cloudflare Worker as a secure backend to process the data, and Cloudflare Turnstile as a free, user-friendly CAPTCHA to prevent spam.
Prerequisites
Before you begin, you'll need:
-
A website that is managed through a Cloudflare account.
-
A Discord server where you have permission to create webhooks.
-
An HTML form on your website that you want to connect.
Create a Discord Webhook
This webhook is the destination for your form submissions.
-
In your Discord server, go to Server Settings > Integrations.
-
Click on Webhooks > New Webhook.
-
Give the webhook a name (e.g., "Website Submissions") and choose the channel it should post to.
-
Click Copy Webhook URL.
-
Important: Keep this URL safe and private. It's a secret key.
Set Up Cloudflare Turnstile (CAPTCHA)
Turnstile will protect your form from bots without annoying challenges.
-
In your Cloudflare Dashboard, go to Turnstile in the left sidebar.
-
Click Add site.
-
Give your site a name, enter your website's domain, and choose the "Managed" widget type.
-
Click Create.
-
Cloudflare will give you a Site Key and a Secret Key. Copy both of these—you'll need them soon.
Step 3: Create the Cloudflare Worker (The Backend)
The worker is a serverless function that will act as the secure middleman. It receives data from your form, validates the CAPTCHA, and then sends the data to Discord.
-
In the Cloudflare Dashboard, go to Workers & Pages > Create application.
-
Give your worker a name (e.g.,
form-processor) and click Deploy. -
Click Edit code and paste the entire script below, replacing the default code.
Worker Script (index.js)
// Cloudflare Worker to process form submissions
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
// Set CORS headers to allow requests ONLY from your website
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://YOUR_WEBSITE_DOMAIN.com', // <-- IMPORTANT: Change this!
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
// Handle CORS preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
// Only allow POST requests
if (request.method !== 'POST') {
return new Response(JSON.stringify({ ok: false, error: 'method_not_allowed' }), { status: 405, headers: corsHeaders });
}
try {
const payload = await request.json();
const ip = request.headers.get('CF-Connecting-IP');
// --- 1. Verify the Turnstile CAPTCHA Token ---
let formData = new FormData();
formData.append('secret', TURNSTILE_SECRET_KEY); // This is a secret variable, not plain text
formData.append('response', payload['cf-turnstile-response']);
formData.append('remoteip', ip);
const turnstileUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
const turnstileResult = await fetch(turnstileUrl, { body: formData, method: 'POST' });
const turnstileOutcome = await turnstileResult.json();
if (!turnstileOutcome.success) {
return new Response(JSON.stringify({ ok: false, error: 'captcha_invalid' }), { status: 403, headers: corsHeaders });
}
// --- 2. If CAPTCHA is valid, format and send the data to Discord ---
const discordPayload = {
embeds: [{
title: '✅ New Form Submission',
color: 0x5865F2, // Discord blurple color
fields: [
// This example assumes 'name' and 'message' fields in the form.
// Customize this section to match your form's fields.
{ name: 'Submitter Name', value: payload.name || 'N/A', inline: true },
{ name: 'Submitter Email', value: payload.email || 'N/A', inline: true },
{ name: 'Message', value: "```" + (payload.message || 'None') + "```", inline: false },
{ name: 'IP Address', value: ip || 'N/A', inline: false },
],
timestamp: new Date().toISOString(),
}],
};
const discordResponse = await fetch(DISCORD_WEBHOOK_URL, { // This is a secret variable
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(discordPayload),
});
if (!discordResponse.ok) {
return new Response(JSON.stringify({ ok: false, error: 'discord_error' }), { status: 500, headers: corsHeaders });
}
// --- 3. Return a success response to the website ---
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: corsHeaders });
} catch (error) {
return new Response(JSON.stringify({ ok: false, error: 'internal_server_error' }), { status: 500, headers: corsHeaders });
}
}
Add Secrets to the Worker
Never paste secrets directly into your code.
-
In your worker's dashboard, go to Settings > Variables.
-
Under Environment Variables, click Add variable and create two secrets:
-
DISCORD_WEBHOOK_URL: Paste the webhook URL from Step 1. -
TURNSTILE_SECRET_KEY: Paste the Secret Key from Step 2.
-
-
Click Save and deploy.
Configure the Frontend (HTML & JavaScript)
Now, update your website's HTML and add the JavaScript that sends the form data to your new worker.
HTML Form
Ensure your HTML includes:
-
The Cloudflare Turnstile script in the
<head>. -
A
<div>for the Turnstile widget inside your form. -
An element to display status messages (success or error).
-
An
onsubmitattribute on the<form>tag to call our JavaScript function.
<!DOCTYPE html>
<html>
<head>
<title>Contact Us</title>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>
<body>
<form id="contact-form" onsubmit="handleFormSubmit(event)">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<label for="message">Message:</label>
<textarea id="message" name="message" required></textarea>
<div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITE_KEY_HERE"></div>
<button type="submit" id="submit-btn">Send Message</button>
<p id="form-status" aria-live="polite"></p>
</form>
<script src="/path/to/your/form-handler.js"></script>
</body>
</html>
Client-Side JavaScript (form-handler.js)
This script handles the submission process in the browser.
async function handleFormSubmit(event) {
event.preventDefault(); // Prevent the default browser form submission
const form = event.target;
const statusEl = document.getElementById('form-status');
const submitBtn = document.getElementById('submit-btn');
// Disable button and show sending message
submitBtn.disabled = true;
statusEl.textContent = 'Sending...';
statusEl.style.color = 'gray';
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// The URL for our worker, which we will route in the next step
const workerUrl = '/api/submit-form';
try {
const response = await fetch(workerUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (result.ok) {
statusEl.textContent = 'Success! Your message has been sent.';
statusEl.style.color = 'green';
form.reset(); // Clear the form
// Reset the CAPTCHA widget
if (window.turnstile) window.turnstile.reset();
} else {
// Display specific errors from the worker
let errorMessage = 'An error occurred. Please try again.';
if (result.error === 'captcha_invalid') {
errorMessage = 'CAPTCHA validation failed. Please try again.';
}
statusEl.textContent = errorMessage;
statusEl.style.color = 'red';
}
} catch (error) {
statusEl.textContent = 'A network error occurred. Please try again later.';
statusEl.style.color = 'red';
} finally {
// Re-enable the button after a short delay
setTimeout(() => { submitBtn.disabled = false; }, 2000);
}
}
Connect Everything with a Worker Route
The final step is to tell Cloudflare that any requests to a specific URL path on your site should be handled by your worker.
-
In the main Cloudflare dashboard, select your domain.
-
On the left sidebar, click on Workers Routes.
-
Click Add route.
-
Fill out the form:
-
Route:
*your.website.com/api/submit-form(Match the domain and the path used in your JavaScript'sfetchcall). -
Service: Select the
form-processorworker you created in Step 3. -
Environment:
production
-
-
Click Save.
And you're done! Your form is now securely connected to your Discord channel. Submissions will appear in near real-time, protected from spam bots.