Configure server verification and companion form actions for Turnstile, including the password-reset path difference in the pinned releases.

BTST Better Auth UI can collect a Cloudflare Turnstile token on its auth forms, while Better Auth checks that token before processing the request. Configure both sides: a visible widget alone does not protect an API endpoint.
This guide targets @btst/better-auth-ui@2.0.1 and better-auth@1.6.16, with @btst/stack@3.1.2. It assumes a working email/password integration. Start with the auth integration guide and companion setup if your sign-in route does not work yet. The examples extend your existing configuration; they are not a complete auth server.
Create a Turnstile widget for your application and configure its allowed hostnames. The site key is public and belongs in the browser configuration. The secret key belongs only in the server environment. Do not pass the secret through StackProvider, public environment variables, or client modules.
Use separate production and test configuration. Cloudflare supplies test keys for local checks; those prove your integration can handle a response, not that production traffic receives a real challenge. Retain Better Auth's origin checks, secure-cookie configuration, and rate limiting.
Add the returned plugin to the plugins array of your existing betterAuth(...) instance. Pass the secret from your server environment:
import { captcha } from "better-auth/plugins";
export function turnstileProtection(secretKey: string) {
if (!secretKey) throw new Error("TURNSTILE_SECRET_KEY is required");
return captcha({
provider: "cloudflare-turnstile",
secretKey,
endpoints: [
"/sign-up/email",
"/sign-in/email",
"/request-password-reset",
],
});
}
These paths are relative to the Better Auth API mount, commonly /api/auth. They are not BTST page routes such as /pages/auth/sign-in. Specifying an endpoint list replaces the plugin's default selection: preserve every endpoint you intend to protect when editing it. Other authentication methods need their own deliberate configuration and verification.
Merge this result into your existing overrides.auth; retain its authClient, navigation, and session-refresh configuration:
import type { AuthPluginOverrides } from "@btst/better-auth-ui/client";
export function turnstileUI(
siteKey: string,
): Pick<AuthPluginOverrides, "captcha"> {
if (!siteKey) throw new Error("A public Turnstile site key is required");
return {
captcha: {
provider: "cloudflare-turnstile",
siteKey,
endpoints: [
"/sign-up/email",
"/sign-in/email",
"/forget-password",
],
},
};
}
The password-reset names intentionally differ in these pinned versions. The companion's forgot-password form asks its CAPTCHA hook for the action /forget-password, then calls authClient.requestPasswordReset(). Better Auth receives that request at /request-password-reset.
Consequently, copying the server list verbatim into the UI can omit the reset form's token. Copying the UI list into the server can omit protection from the actual reset endpoint. Keep the UI action and server URL paired, and recheck this boundary when upgrading. The companion sends the resulting token in x-captcha-response; never log that header or add its value to analytics.
In a disposable environment, call each protected endpoint without a token. It should fail before creating an account, session, or password-reset delivery. Repeat with an invalid token. Then complete the widget and confirm that a valid request can reach the underlying auth flow.
For password reset, inspect the browser request to /api/auth/request-password-reset and confirm that it includes the CAPTCHA header. Verify the intended email delivery in your test mailbox. Exercise a failed password attempt followed by a retry: the form must obtain a usable token again rather than repeatedly sending an expired one.
Check keyboard operation and the challenge at a narrow viewport. A blocked third-party script or restrictive Content Security Policy can prevent completion; diagnose the failed widget request rather than disabling server verification. A CAPTCHA success is not proof that the user owns an email address or may access a protected resource.
Use the password-reset guide for the mail callback and return route. Review dependency compatibility before upgrading the companion or Better Auth independently.
Sources: Better Auth CAPTCHA configuration, Cloudflare server validation, and the released companion's CAPTCHA hook, widget, and forgot-password form. The runnable configuration was checked against the pinned versions above; current upstream documentation may describe newer behavior.