Skip to main content
aapp-hacks
← Back to articles
Author
app-hacks
Published
Updated
Reviewed

Cloudflare Turnstile / Cloudflare Pages / Security

Validating Cloudflare Turnstile Server-Side for a Contact Form

An implementation record of explicitly rendering Turnstile and confirming its token with Siteverify in a Pages Function.

The contact page renders Turnstile explicitly and keeps its submit button disabled until an authentication token is received. A visible widget alone is not treated as permission to send a message. Because browser state can be modified, the same token is sent to the Function and email delivery proceeds only when server-side verification succeeds.

Limiting the browser-side state

src/pages/contact.astro loads the script with render=explicit, then calls turnstile.render() with action: 'contact'. The callback stores the token and enables submission. Expiration, an error, and completed submission reset the widget and disable the button again. A ten-second timeout treats a script that never loads as a failure rather than leaving the form waiting indefinitely.

widgetId = api.render(widgetContainer, {
  sitekey: turnstileSiteKey,
  action: 'contact',
  callback: (token) => { turnstileToken = token; },
  'expired-callback': resetTurnstile,
});

The site key is passed to the page because the widget needs it. The secret key, by contrast, is used only by the Function and remains a Secret; it is not emitted into HTML or browser JavaScript. Their exposure requirements differ, so they are not grouped in the same settings file or log output.

Confirming the token in the Function

functions/api/contact.ts POSTs form-encoded data to Siteverify with a ten-second timeout. In addition to the success flag, it checks that the hostname matches the request destination. On the production host it also requires the action to be contact, preventing a token intended for another screen from being reused for form delivery.

if (!response.ok || result.success !== true || result.hostname !== expectedHostname) {
  return false;
}
return expectedHostname !== 'app-hacks.net' || result.action === 'contact';

An expired token or a loading failure does not send email; the user is asked to authenticate again. Turnstile is not a replacement for input validation or a honeypot, so both remain on the Function side.

Common mistakes and how to check them

Do not allow sending based only on a client callback, and do not treat a timeout exception as a successful send. After release, test normal submission, token expiration, and an authentication-loading failure separately, and confirm that verification failure prevents delivery.

Users can edit the form after obtaining a token, so avoid retaining an authenticated-looking screen state for too long. This implementation resets the token after every submission and does not reuse the same token for repeated failed sends. The user sees a short explanation that reauthentication is needed, without exposing internal details of the verification service.

This verification is not extra machinery for its own sake; it connects the authentication result to the permission to send correctly.