BTST
PluginsQuickstartDocs
Live Blog

From research to product evaluation

Evaluating a publishing workflow for an app you already own?

See what the BTST Blog plugin adds to an existing React or Next.js app
BTST

Open-source TypeScript features for the React application, data, and deployment you already own.

Released plugins

  • Blog
  • AI Chat
  • CMS
  • Form Builder
  • UI Builder
  • Kanban
  • Comments
  • Media
  • Route Docs
  • OpenAPI
  • Better Auth UI

Resources

  • Quickstart
  • Documentation
  • All plugins
  • Live Blog
  • GitHub (opens in a new tab)
© 2026 BTST. Open source under the MIT License.
AI Chat
September 22, 2026Better Auth UIAuthReact

Add Turnstile CAPTCHA to BTST Better Auth UI

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

Add Turnstile CAPTCHA to BTST Better Auth UI

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.

Keep the site key and secret separate#

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.

Protect the server endpoints#

Add the returned plugin to the plugins array of your existing betterAuth(...) instance. Pass the secret from your server environment:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
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.

Configure the companion's form actions#

Merge this result into your existing overrides.auth; retain its authClient, navigation, and session-refresh configuration:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
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.

Test rejection as well as success#

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.

Continue the setup#

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.

In This Post

Keep the site key and secret separateProtect the server endpointsConfigure the companion's form actionsTest rejection as well as successContinue the setup