CAPTCHA Protection
Fluxbase supports CAPTCHA verification to protect authentication endpoints from bots and automated abuse. Multiple providers are supported including hCaptcha, reCAPTCHA v3, Cloudflare Turnstile, and the self-hosted Cap provider.
Overview
Section titled “Overview”CAPTCHA protection can be enabled on specific authentication endpoints:
- signup - New user registration
- login - User authentication
- password_reset - Password reset requests
- magic_link - Magic link authentication
When enabled, clients must include a valid CAPTCHA token with their authentication requests.
Supported Providers
Section titled “Supported Providers”| Provider | Type | Self-Hosted | Best For |
|---|---|---|---|
| hCaptcha | Visual challenge | No | Privacy-focused applications |
| reCAPTCHA v3 | Invisible (score-based) | No | Seamless user experience |
| Cloudflare Turnstile | Invisible | No | Cloudflare users, free tier |
| Cap | Proof-of-work | Yes | Self-hosted, privacy-first |
Configuration
Section titled “Configuration”YAML Configuration
Section titled “YAML Configuration”security: captcha: enabled: true provider: hcaptcha # hcaptcha, recaptcha_v3, turnstile, cap site_key: "your-site-key" secret_key: "your-secret-key" score_threshold: 0.5 # reCAPTCHA v3 only (0.0-1.0) endpoints: - signup - login - password_reset - magic_linkEnvironment Variables
Section titled “Environment Variables”| Variable | Description |
|---|---|
FLUXBASE_SECURITY_CAPTCHA_ENABLED | Enable CAPTCHA verification (true/false) |
FLUXBASE_SECURITY_CAPTCHA_PROVIDER | Provider name |
FLUXBASE_SECURITY_CAPTCHA_SITE_KEY | Public site key |
FLUXBASE_SECURITY_CAPTCHA_SECRET_KEY | Secret key for verification |
FLUXBASE_SECURITY_CAPTCHA_SCORE_THRESHOLD | Score threshold (reCAPTCHA v3 only) |
FLUXBASE_SECURITY_CAPTCHA_ENDPOINTS | Comma-separated list of endpoints |
Cap Provider Configuration
Section titled “Cap Provider Configuration”For the self-hosted Cap provider, use different configuration options:
security: captcha: enabled: true provider: cap cap_server_url: "http://localhost:3000" # Your Cap server URL cap_api_key: "your-api-key" endpoints: - signup - login| Variable | Description |
|---|---|
FLUXBASE_SECURITY_CAPTCHA_CAP_SERVER_URL | URL of your Cap server |
FLUXBASE_SECURITY_CAPTCHA_CAP_API_KEY | Cap API key |
Provider Setup
Section titled “Provider Setup”hCaptcha
Section titled “hCaptcha”- Sign up at hcaptcha.com
- Add your domain to get your site key and secret key
- Configure Fluxbase:
security: captcha: enabled: true provider: hcaptcha site_key: "10000000-ffff-ffff-ffff-000000000001" # Test key secret_key: "0x0000000000000000000000000000000000000000" # Test keyreCAPTCHA v3
Section titled “reCAPTCHA v3”- Register at Google reCAPTCHA
- Select reCAPTCHA v3 and add your domains
- Configure with your keys:
security: captcha: enabled: true provider: recaptcha_v3 site_key: "your-recaptcha-site-key" secret_key: "your-recaptcha-secret-key" score_threshold: 0.5 # Reject scores below this (0.0 = bot, 1.0 = human)Cloudflare Turnstile
Section titled “Cloudflare Turnstile”- Access Cloudflare Turnstile dashboard
- Create a widget for your domain
- Configure Fluxbase:
security: captcha: enabled: true provider: turnstile site_key: "your-turnstile-site-key" secret_key: "your-turnstile-secret-key"Cap (Self-Hosted)
Section titled “Cap (Self-Hosted)”Cap is a proof-of-work CAPTCHA that runs entirely on your infrastructure.
- Run the Cap server:
docker run -p 3000:3000 ghcr.io/tiagozip/cap:latest- Configure Fluxbase:
security: captcha: enabled: true provider: cap cap_server_url: "http://localhost:3000" cap_api_key: "your-api-key"Frontend Integration
Section titled “Frontend Integration”Getting CAPTCHA Configuration
Section titled “Getting CAPTCHA Configuration”First, fetch the CAPTCHA configuration from your Fluxbase server:
const response = await fetch('http://localhost:8080/api/v1/auth/captcha/config')const config = await response.json()
// {// "enabled": true,// "provider": "hcaptcha",// "site_key": "your-site-key",// "endpoints": ["signup", "login"]// }hCaptcha Widget
Section titled “hCaptcha Widget”<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<form id="signup-form"> <input type="email" name="email" required /> <input type="password" name="password" required /> <div class="h-captcha" data-sitekey="YOUR_SITE_KEY"></div> <button type="submit">Sign Up</button></form>
<script>document.getElementById('signup-form').onsubmit = async (e) => { e.preventDefault(); const token = hcaptcha.getResponse();
await fetch('/api/v1/auth/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: e.target.email.value, password: e.target.password.value, captcha_token: token }) });};</script>reCAPTCHA v3 Widget
Section titled “reCAPTCHA v3 Widget”<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
<script>async function signUp(email, password) { const token = await grecaptcha.execute('YOUR_SITE_KEY', { action: 'signup' });
await fetch('/api/v1/auth/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password, captcha_token: token }) });}</script>Cloudflare Turnstile Widget
Section titled “Cloudflare Turnstile Widget”<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form id="signup-form"> <input type="email" name="email" required /> <input type="password" name="password" required /> <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div> <button type="submit">Sign Up</button></form>
<script>document.getElementById('signup-form').onsubmit = async (e) => { e.preventDefault(); const token = document.querySelector('[name="cf-turnstile-response"]').value;
await fetch('/api/v1/auth/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: e.target.email.value, password: e.target.password.value, captcha_token: token }) });};</script>Cap Widget
Section titled “Cap Widget”<!-- Load Cap widget from your self-hosted server --><script src="http://localhost:3000/widget.js"></script>
<form id="signup-form"> <input type="email" name="email" required /> <input type="password" name="password" required /> <cap-widget data-cap-url="http://localhost:3000"></cap-widget> <input type="hidden" name="captcha_token" id="captcha_token" /> <button type="submit">Sign Up</button></form>
<script>// Cap widget will populate the token when solvedwindow.onCapComplete = (token) => { document.getElementById('captcha_token').value = token;};</script>SDK Usage
Section titled “SDK Usage”TypeScript SDK
Section titled “TypeScript SDK”import { FluxbaseClient } from '@fluxbase/sdk'
const client = new FluxbaseClient({ url: 'http://localhost:8080' })
// Get CAPTCHA configurationconst { data: config } = await client.auth.getCaptchaConfig()
if (config?.enabled) { console.log('CAPTCHA provider:', config.provider) console.log('Site key:', config.site_key) console.log('Protected endpoints:', config.endpoints)}
// Sign up with CAPTCHA tokenconst { data, error } = await client.auth.signUp({ email: 'user@example.com', password: 'SecurePassword123', captchaToken: 'token-from-widget'})
// Sign in with CAPTCHA tokenconst { data: session, error } = await client.auth.signIn({ email: 'user@example.com', password: 'SecurePassword123', captchaToken: 'token-from-widget'})
// Request password reset with CAPTCHAawait client.auth.resetPassword({ email: 'user@example.com', captchaToken: 'token-from-widget'})React SDK
Section titled “React SDK”import { useCaptchaConfig, useCaptcha, useSignUp, isCaptchaRequiredForEndpoint} from '@fluxbase/sdk-react'
function SignUpForm() { const { data: config } = useCaptchaConfig() const captcha = useCaptcha(config?.provider) const signUp = useSignUp()
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault()
let captchaToken: string | undefined
// Check if CAPTCHA is required for signup if (isCaptchaRequiredForEndpoint(config, 'signup')) { captchaToken = await captcha.execute() }
await signUp.mutateAsync({ email, password, captchaToken }) }
return ( <form onSubmit={handleSubmit}> <input type="email" name="email" required /> <input type="password" name="password" required />
{config?.enabled && config.provider && ( <CaptchaWidget provider={config.provider} siteKey={config.site_key} onVerify={captcha.setToken} /> )}
<button type="submit" disabled={signUp.isPending}> Sign Up </button> </form> )}useCaptcha Hook
Section titled “useCaptcha Hook”The useCaptcha hook provides a unified interface for all CAPTCHA providers:
const captcha = useCaptcha(provider)
// Propertiescaptcha.token // Current token (string | null)captcha.isReady // Widget loaded and ready (boolean)captcha.isLoading // Widget is loading (boolean)captcha.error // Any error during loading (Error | null)
// Methodscaptcha.execute() // Execute CAPTCHA and get token (Promise<string>)captcha.reset() // Reset the widgetcaptcha.setToken() // Manually set token (for widget callbacks)REST API
Section titled “REST API”Get CAPTCHA Configuration
Section titled “Get CAPTCHA Configuration”GET /api/v1/auth/captcha/configReturns the public CAPTCHA configuration:
{ "enabled": true, "provider": "hcaptcha", "site_key": "10000000-ffff-ffff-ffff-000000000001", "endpoints": ["signup", "login", "password_reset", "magic_link"]}Protected Endpoints
Section titled “Protected Endpoints”When CAPTCHA is enabled for an endpoint, include the token in your request:
# Sign up with CAPTCHAcurl -X POST http://localhost:8080/api/v1/auth/signup \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "password": "SecurePassword123", "captcha_token": "10000000-aaaa-bbbb-cccc-000000000001" }'
# Sign in with CAPTCHAcurl -X POST http://localhost:8080/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "password": "SecurePassword123", "captcha_token": "10000000-aaaa-bbbb-cccc-000000000001" }'Error Responses
Section titled “Error Responses”| Status | Error | Description |
|---|---|---|
| 400 | captcha_required | CAPTCHA token is required but missing |
| 400 | captcha_invalid | CAPTCHA verification failed |
| 400 | captcha_expired | CAPTCHA token has expired |
| 400 | captcha_score_too_low | reCAPTCHA v3 score below threshold |
Troubleshooting
Section titled “Troubleshooting”CAPTCHA verification always fails
Section titled “CAPTCHA verification always fails”- Check your keys - Ensure site key and secret key match and are for the correct environment (test vs production)
- Domain mismatch - Verify your domain is registered with the CAPTCHA provider
- Clock skew - Ensure server time is synchronized (tokens expire)
reCAPTCHA v3 scores are too low
Section titled “reCAPTCHA v3 scores are too low”- Adjust
score_thresholdlower (e.g., 0.3 instead of 0.5) - reCAPTCHA v3 learns over time; scores improve with traffic
- Consider using action names that match your endpoint (
signup,login)
Cap widget not loading
Section titled “Cap widget not loading”- Verify Cap server is running and accessible
- Check browser console for CORS errors
- Ensure
cap_server_urlmatches your Cap server exactly
CAPTCHA not showing on frontend
Section titled “CAPTCHA not showing on frontend”// Always check if CAPTCHA is required before renderingconst { data: config } = await client.auth.getCaptchaConfig()
if (config?.enabled && config.endpoints?.includes('signup')) { // Render CAPTCHA widget}Security Best Practices
Section titled “Security Best Practices”- Never expose secret keys - Secret keys should only be on the server
- Use HTTPS - CAPTCHA tokens should be transmitted over HTTPS
- Combine with rate limiting - CAPTCHA doesn’t replace rate limiting
- Monitor verification failures - High failure rates may indicate attacks
- Token single-use - Each token should only be used once
Next Steps
Section titled “Next Steps”- Authentication - Authentication overview
- Rate Limiting - Rate limiting configuration
- Row-Level Security - Data access control