How to Protect Auth Forms with Cloudflare, Supabase SSR, and Astro Actions


When building authentication into your app, consider how to secure it from bots and malicious actors. This blog discusses how to do that using Cloudflare Turnstile in a Supabase and Astro project.
add cloudflare
branch.Prerequisites
This blog assumes that you:
are following from this tutorial
are familiar with basic web development concepts
understand basic authentication concepts
understand Backend-as-a-Service (BaaS) concepts
What is Cloudflare Turnstile?
Turnstile is Cloudflare’s replacement for CAPTCHAs, which are visual puzzles you must solve to differentiate between genuine users and bots.
CAPTCHAs are visually clunky, annoying, and sometimes really hard to solve. They reduce the quality of the user experience and are generally inaccessible.
In comparison, Turnstile does the same job of detecting malicious activity without requiring users to solve puzzles. And Turnstile looks prettier, in my opinion 😅.
What is Supabase?
Supabase is an open-source Backend-as-a-Service that builds upon Postgres. It provides common key features such as authentication, real-time, edge functions, storage, and more.
Supabase offers a hosted version that makes building and scaling production-ready apps supa easy and a self-hostable version that gives users full control.
What is Astro?
Astro is a UI-agnostic web framework. It renders server-first by default and can be used with any UI framework, including Astro client components.
Why Should You Protect Auth Forms?
Authentication protects sensitive resources from unauthorized access, making them the primary target that bots and malicious actors attempt to breach. Therefore, you should take extra precautions to deter them from successfully compromising your application.
Setting Up Supabase
To get started, you will need a Supabase account. Then, follow the prompts to create a project. Go to the Authentication tab in the sidebar, click the Sign In / Up tab under Configuration, and enable user sign-ups.
Next, go to the Auth Protection tab under the Configuration on the same page, turn on Captcha protection, and choose Cloudflare as the provider.
Next, you will need to set up a Cloudflare account to get Cloudflare credentials.
Setting Up Cloudflare Turnstile
Log in or register a Cloudflare account, click the Turnstile tab in the sidebar, and then click the “Add widget” button.
Next, name your widget, add “localhost” as the hostname, and leave all other settings as is. Then, create it. Cloudflare will then show you your site key and secret key.
Copy the secret key and add it to the Supabase dashboard under the Auth protection section.
Connect the Frontend
In your project’s .env
file, add the cloudflare site key provided from the step above like so:
TURNSTILE_SITE_KEY=<YOUR_TURNSTILE_SITE_KEY>
Then in the Layout.astro
file, add this script just above the closing </head>
tag to initiate Cloudflare’s turnstile:
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer></script>
You can read more about Cloudflare Turnstile to understand the flow better.
Updating the index page
Then update the index.astro
page to include this div right above the submit button:
<div class="cf-turnstile" data-sitekey={apiKey}></div>
Now add this line within the frontmatter at the top of the same page:
const apiKey = import.meta.env.TURNSTILE_SITE_KEY;
Finally, adjust the script tag to account for the turnstile logic. It should look like this:
<script>
import { actions } from "astro:actions";
declare global {
interface Window {
turnstile?: {
reset: () => void;
};
}
}
const signInForm = document.querySelector("#signin-form") as HTMLFormElement;
const formSubmitBtn = document.getElementById("sign-in") as HTMLButtonElement;
signInForm?.addEventListener("submit", async (e) => {
e.preventDefault();
formSubmitBtn.disabled = true;
formSubmitBtn.textContent = "Signing in...";
try {
const turnstileToken = (
document.querySelector(
"[name='cf-turnstile-response']"
) as HTMLInputElement
)?.value;
if (!turnstileToken) {
throw new Error("verification_missing");
}
const formData = new FormData(signInForm);
formData.append("captchaToken", turnstileToken);
const results = await actions.signIn(formData);
if (!results.data?.success) {
if (results.data?.message?.includes("captcha protection")) {
alert("Verification failed. Please try again.");
if (window.turnstile) {
window.turnstile.reset();
}
formSubmitBtn.disabled = false;
formSubmitBtn.textContent = "Sign In";
return;
} else {
alert("Oops! Could not sign in. Please try again");
formSubmitBtn.disabled = false;
formSubmitBtn.textContent = "Sign In";
return;
}
}
formSubmitBtn.textContent = "Sign In";
alert("Please check your email to sign in");
} catch (error) {
if (window.turnstile) {
window.turnstile.reset();
}
formSubmitBtn.disabled = false;
formSubmitBtn.textContent = "Sign In";
console.log(error);
alert("Something went wrong. Please try again");
}
});
</script>
After waiting for the token and handling potential errors, the most important part for this new flow to work is to then pass the token to the astro action that handles the sign-in. This allows Supabase to perform token verification on our behalf, eliminating the need to call Cloudflare’s verify APIs directly.
We do that in this part of the code:
const formData = new FormData(signInForm);
formData.append("captchaToken", turnstileToken);
const results = await actions.signIn(formData);
Adjusting the Auth Astro Action
Now, we need to adjust the emailSignUp
action in the index.ts
file in the actions
folder. The action now needs to accept the Turnstile token and pass it back to the Supabase method.
Replace the signIn function definition with this:
signIn: defineAction({
accept: "form",
input: z.object({
email: z.string().email(),
captchaToken: z.string(),
}),
handler: async (input, context) => {
return emailSignUp(input, context);
},
}),
The only change here is adding the captchaToken to the expected input object. Then adjust the emailSignUp
action to include the captcha. In the end, it should look like this:
const emailSignUp = async (
{
email,
captchaToken,
}: {
email: string;
captchaToken: string;
},
context: ActionAPIContext
) => {
console.log("Sign up action");
try {
const supabase = createClient({
request: context.request,
cookies: context.cookies,
});
console.log("Request cookies:", context.request.headers.get("Cookie"));
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
captchaToken,
emailRedirectTo: "http://localhost:4321/api/exchange",
},
});
if (error) {
console.error("Sign up error", error);
return {
success: false,
message: error.message,
};
} else {
console.log("Sign up success", data);
return {
success: true,
message: "Successfully logged in",
};
}
} catch (err) {
console.error("SignUp action other error", err);
return {
success: false,
message: "Unexpected error",
};
}
};
Testing the Turnstile
All that is left is to verify that everything still works as expected and that the Turnstile works. Open an integrated terminal and run: npm run dev
, then open the provided localhost URL.
You should see the same screen as before, but with the Turnstile addition:
You should see that all works by attempting to sign in. You should receive an email and be correctly redirected to the protected
page.
With that, you have successfully protected your auth form in an Astro project using Supabase auth and Cloudflare’s Turnstile.
Notes and Resources
Here are some useful resources on Cloudflare’s Turnstile, Astro actions, and Supabase auth:
Subscribe to my newsletter
Read articles from Fatuma Abdullahi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Fatuma Abdullahi
Fatuma Abdullahi
I'm a software engineer, a go-getter, a writer and tiny youTuber. I like teaching what I learn and encouraging others in this journey.