Captcha
Add bot protection to Solid/Zaidan sign-up, sign-in, and password reset forms.
The captcha plugin adds a widget to the copied Solid/Zaidan authentication forms. It sends the resolved token in the x-captcha-response header.
The plugin supports Cloudflare Turnstile, hCaptcha, CaptchaFox, and reCAPTCHA. Provide a Solid render component that connects the provider callbacks to the plugin.
It contributes:
- A captcha widget rendered above the submit button on sign-in, sign-up, and forgot-password forms
- Automatic header management. The plugin clears the token after an error or expiration, or when the component unmounts.
- Automatic widget refresh after an unsuccessful submission. Captcha tokens are single-use, so each retry requires a new token.
Social sign-in
Provider buttons forward the current CAPTCHA token to /sign-in/social. Add this endpoint to the server CAPTCHA configuration to protect social sign-in. Failed requests clear the token and reset the widget before another attempt.
Use socialSignInMode="redirect" for CAPTCHA-protected social sign-in. Better Auth 1.7's experimental popup API does not accept fetchOptions or forward CAPTCHA headers. Popup failures reset the widget, but the popup flow cannot send the token.
Setup
The copied authentication forms already contain the captcha widget slot. Configure the Better Auth server plugin and register the Solid UI plugin with your widget.
Install the Better Auth plugin
Add the captcha plugin to your Better Auth server config and choose a provider:
import { betterAuth } from "better-auth"
import { captcha } from "better-auth/plugins"
export const auth = betterAuth({
// ...
plugins: [
captcha({
provider: "cloudflare-turnstile", // or "hcaptcha", "captchafox", "google-recaptcha"
secretKey: process.env.TURNSTILE_SECRET_KEY as string
})
]
})By default, Better Auth protects /sign-up/email, /sign-in/email, and /request-password-reset.
Better Auth 1.7 and later matches complete authentication paths. Use an exact endpoint or an explicit wildcard such as /sign-in/*.
Do not use a partial prefix such as /sign-in. To protect username and social sign-in, add their endpoints explicitly:
captcha({
provider: "cloudflare-turnstile",
secretKey: process.env.TURNSTILE_SECRET_KEY as string,
endpoints: [
"/sign-up/email",
"/sign-in/email",
"/sign-in/username",
"/sign-in/social",
"/request-password-reset"
]
})Register the Solid UI plugin
Pass captchaPlugin({ render }) to <AuthProvider>. render is a Solid component that receives setToken, clearToken, and setReset and is responsible for mounting your provider widget.
import { captchaPlugin } from "@better-auth-ui/solid/plugins/captcha"
import { AuthProvider } from "@/components/auth/auth-provider"
import { authClient } from "@/lib/auth-client"
import { CaptchaWidget } from "@/components/captcha-widget"
<AuthProvider
authClient={authClient}
plugins={[
captchaPlugin({ render: CaptchaWidget })
]}
>
{children}
</AuthProvider>The render component is mounted as a real Solid component, so Solid primitives and context work inside it. See the Providers section below for ready-to-use widget patterns.
Providers
The examples use @better-captcha/solidjs. This library supports the providers that Better Auth can verify.
You can use another Solid widget. It must call setToken after success, call clearToken after an error, and register reset with setReset.
The server-side Better Auth provider names are the same as shadcn. Only the client-side widget package changes from React to Solid:
| Provider | Better Auth provider | shadcn React widget | Solid/Zaidan widget |
|---|---|---|---|
| Cloudflare Turnstile | "cloudflare-turnstile" | @marsidev/react-turnstile | @better-captcha/solidjs/provider/turnstile |
| hCaptcha | "hcaptcha" | @hcaptcha/react-hcaptcha | @better-captcha/solidjs/provider/hcaptcha |
| CaptchaFox | "captchafox" | @captchafox/react | @better-captcha/solidjs/provider/captcha-fox |
Cloudflare Turnstile
npm install @better-captcha/solidjsimport type { CaptchaRenderProps } from "@better-auth-ui/solid/plugins/captcha"
import {
createCaptchaController,
Turnstile,
type TurnstileHandle
} from "@better-captcha/solidjs/provider/turnstile"
import { onCleanup, onMount } from "solid-js"
export function TurnstileWidget({
setToken,
clearToken,
setReset
}: CaptchaRenderProps) {
const controller = createCaptchaController<TurnstileHandle>()
onMount(() => {
setReset(() => controller.handle()?.reset())
})
onCleanup(() => setReset(null))
return (
<Turnstile
sitekey={import.meta.env.VITE_TURNSTILE_SITE_KEY}
controller={controller}
onSolve={setToken}
onError={clearToken}
options={{ size: "flexible" }}
/>
)
}import { captchaPlugin } from "@better-auth-ui/solid/plugins/captcha"
import { AuthProvider } from "@/components/auth/auth-provider"
import { TurnstileWidget } from "@/components/turnstile-widget"
<AuthProvider
authClient={authClient}
plugins={[captchaPlugin({ render: TurnstileWidget })]}
>
{children}
</AuthProvider>hCaptcha
npm install @better-captcha/solidjsimport type { CaptchaRenderProps } from "@better-auth-ui/solid/plugins/captcha"
import {
createCaptchaController,
HCaptcha,
type HCaptchaHandle
} from "@better-captcha/solidjs/provider/hcaptcha"
import { onCleanup, onMount } from "solid-js"
export function HCaptchaWidget({
setToken,
clearToken,
setReset
}: CaptchaRenderProps) {
const controller = createCaptchaController<HCaptchaHandle>()
onMount(() => {
setReset(() => controller.handle()?.reset())
})
onCleanup(() => setReset(null))
return (
<HCaptcha
sitekey={import.meta.env.VITE_HCAPTCHA_SITE_KEY}
controller={controller}
onSolve={setToken}
onError={clearToken}
options={{ theme: "auto" }}
/>
)
}import { captchaPlugin } from "@better-auth-ui/solid/plugins/captcha"
import { AuthProvider } from "@/components/auth/auth-provider"
import { HCaptchaWidget } from "@/components/hcaptcha-widget"
<AuthProvider
authClient={authClient}
plugins={[captchaPlugin({ render: HCaptchaWidget })]}
>
{children}
</AuthProvider>CaptchaFox
npm install @better-captcha/solidjsimport type { CaptchaRenderProps } from "@better-auth-ui/solid/plugins/captcha"
import {
CaptchaFox,
type CaptchaFoxHandle,
createCaptchaController
} from "@better-captcha/solidjs/provider/captcha-fox"
import { onCleanup, onMount } from "solid-js"
export function CaptchaFoxWidget({
setToken,
clearToken,
setReset
}: CaptchaRenderProps) {
const controller = createCaptchaController<CaptchaFoxHandle>()
onMount(() => {
setReset(() => controller.handle()?.reset())
})
onCleanup(() => setReset(null))
return (
<CaptchaFox
sitekey={import.meta.env.VITE_CAPTCHAFOX_SITE_KEY}
controller={controller}
onSolve={setToken}
onError={clearToken}
options={{ theme: "auto" }}
/>
)
}import { captchaPlugin } from "@better-auth-ui/solid/plugins/captcha"
import { AuthProvider } from "@/components/auth/auth-provider"
import { CaptchaFoxWidget } from "@/components/captchafox-widget"
<AuthProvider
authClient={authClient}
plugins={[captchaPlugin({ render: CaptchaFoxWidget })]}
>
{children}
</AuthProvider>Options
Prop
Type
Render props
The render component receives:
Prop
Type
-
Connect the provider's success callback to
setToken. It adds thex-captcha-responseheader to the next Better Auth request. -
Connect the error and expiration callbacks to
clearToken. It removes the header before the application sends a stale token. -
Connect the widget's
reset()function tosetReset. Better Auth consumes the token through/siteverifybefore the authentication handler completes.A rejected request still consumes the token. Each protected form calls the registered
reset()function fromonErrorand clears the old token.
The plugin also clears the header when the component unmounts. The application does not need additional cleanup.
Last updated on