Captcha
Configure Turnstile, Google reCAPTCHA, or hCaptcha for sign-in, sign-up, and magic link requests.
ShipNext captcha support has two layers:
src/modules/captcha/- client component anduseCaptchahook.src/integrations/captcha/- server-side verification adapters for Cloudflare Turnstile, Google reCAPTCHA, and hCaptcha.
Captcha currently protects Better Auth requests for email sign-in, magic link sign-in, and email sign-up. It does not automatically protect arbitrary APIs.
Configuration
In config/website.ts:
const authConfig = {
captcha: {
enable: true,
provider: "turnstile",
scenarios: {
signIn: true,
signUp: true,
},
},
};| Field | Description |
|---|---|
enable | Global captcha switch |
provider | turnstile, google-recaptcha, or hcaptcha |
scenarios.signIn | Protects email sign-in and magic links |
scenarios.signUp | Protects email sign-up |
If provider keys are not configured yet, set enable: false for local testing.
Environment variables
| Provider | Browser variable | Server variable |
|---|---|---|
| Turnstile | NEXT_PUBLIC_TURNSTILE_SITE_KEY | TURNSTILE_SECRET_KEY |
| Google reCAPTCHA | NEXT_PUBLIC_GOOGLE_RECAPTCHA_SITE_KEY | GOOGLE_RECAPTCHA_SECRET_KEY |
| hCaptcha | NEXT_PUBLIC_HCAPTCHA_SITE_KEY | HCAPTCHA_SECRET_KEY |
Example:
NEXT_PUBLIC_TURNSTILE_SITE_KEY=''
TURNSTILE_SECRET_KEY=''NEXT_PUBLIC_* values are visible to the browser. Secret keys must stay server-only.
Protected request flow
app/api/auth/[...all]/route.ts verifies captcha before POST requests enter Better Auth:
getCaptchaScenarioForAuthRequest(request)maps the auth path to a scenario.- The client sends the token in
x-captcha-response. verifyCaptchaRequest()calls the selected provider's siteverify API.- Failed verification returns JSON immediately and does not call Better Auth.
Current mapping:
| Scenario | Better Auth path |
|---|---|
signIn | /api/auth/sign-in/email, /api/auth/sign-in/magic-link |
signUp | /api/auth/sign-up/email |
Client usage
const captcha = useCaptcha("signIn");
if (!captcha.validate()) {
return;
}
await signIn.email({
email,
password,
captchaToken: captcha.captchaToken,
});CaptchaWidget selects the provider component from websiteConfig.auth.captcha.provider.
Error codes
| code | HTTP | Description |
|---|---|---|
CAPTCHA_REQUIRED | 400 | Captcha is enabled but no token was sent |
CAPTCHA_VERIFICATION_FAILED | 403 | Provider rejected the token |
CAPTCHA_MISCONFIGURED | 500 | Server secret key is missing |
CAPTCHA_SERVICE_UNAVAILABLE | 500 | Provider verification failed unexpectedly |
Checklist
- Provider in
website.tsmatches the configured environment variables. - Sign-in and sign-up pages render the captcha widget.
- Protected requests without
x-captcha-responsereturnCAPTCHA_REQUIRED. - Successful captcha lets auth proceed.
- Secret keys are not prefixed with
NEXT_PUBLIC_.