ShipNext

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 and useCaptcha hook.
  • 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,
    },
  },
};
FieldDescription
enableGlobal captcha switch
providerturnstile, google-recaptcha, or hcaptcha
scenarios.signInProtects email sign-in and magic links
scenarios.signUpProtects email sign-up

If provider keys are not configured yet, set enable: false for local testing.

Environment variables

ProviderBrowser variableServer variable
TurnstileNEXT_PUBLIC_TURNSTILE_SITE_KEYTURNSTILE_SECRET_KEY
Google reCAPTCHANEXT_PUBLIC_GOOGLE_RECAPTCHA_SITE_KEYGOOGLE_RECAPTCHA_SECRET_KEY
hCaptchaNEXT_PUBLIC_HCAPTCHA_SITE_KEYHCAPTCHA_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:

  1. getCaptchaScenarioForAuthRequest(request) maps the auth path to a scenario.
  2. The client sends the token in x-captcha-response.
  3. verifyCaptchaRequest() calls the selected provider's siteverify API.
  4. Failed verification returns JSON immediately and does not call Better Auth.

Current mapping:

ScenarioBetter 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

codeHTTPDescription
CAPTCHA_REQUIRED400Captcha is enabled but no token was sent
CAPTCHA_VERIFICATION_FAILED403Provider rejected the token
CAPTCHA_MISCONFIGURED500Server secret key is missing
CAPTCHA_SERVICE_UNAVAILABLE500Provider verification failed unexpectedly

Checklist

  • Provider in website.ts matches the configured environment variables.
  • Sign-in and sign-up pages render the captcha widget.
  • Protected requests without x-captcha-response return CAPTCHA_REQUIRED.
  • Successful captcha lets auth proceed.
  • Secret keys are not prefixed with NEXT_PUBLIC_.

On this page